diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d983ca7e10..58cceae0c7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: '' -labels: 'bug' +labels: 'ctg-bug' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/bug_report_easy.md b/.github/ISSUE_TEMPLATE/bug_report_easy.md new file mode 100644 index 0000000000..ee17afeec0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_easy.md @@ -0,0 +1,44 @@ +--- +name: Bug report without tips +about: Template without examples (for experienced QA engineers) +title: '' +labels: 'ctg-bug' +assignees: '' + +--- + +**Description** + + + +**To Reproduce** + +1. Install UnitTestBot plugin built from main in IntelliJ IDEA +2. Open +3. Generate tests for + +**Expected behavior** + + + +**Actual behavior** + + + +**Screenshots, logs** + + + +~~~java + +~~~ + +**Environment** + +IntelliJ IDEA version - +Project - +JDK - + +**Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7cc9d64348..0cb037bdd0 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest an idea for the UTBot project title: '' -labels: 'enhancement' +labels: 'ctg-enhancement' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/test_request.md b/.github/ISSUE_TEMPLATE/test_request.md index ff1d8069c3..60f1ed48d8 100644 --- a/.github/ISSUE_TEMPLATE/test_request.md +++ b/.github/ISSUE_TEMPLATE/test_request.md @@ -2,21 +2,20 @@ name: Manual testing checklist about: Checklist of testing process title: 'Manual testing of build#' -labels: 'qa' +labels: 'ctg-qa' assignees: '' - --- **Initial set-up** *Check that the IntelliJ Idea UTBot plugin can be successfully installed* -- [ ] Choose appropriate workflow from the list (by default, filter by main branch and take the latest one) https://github.com/UnitTestBot/UTBotJava/actions/workflows/publish-plugin-and-cli.yml +- [ ] Choose appropriate [workflow from the list](https://github.com/UnitTestBot/UTBotJava/actions/workflows/build-and-run-tests.yml?query=branch%3Amain) - [ ] Download plugin - [ ] Check downloaded zip-file size < 100 MB -- [ ] Open IntelliJ IDE +- [ ] Open IntelliJ IDEA - [ ] Remove previously installed UTBot plugin -- [ ] Clone or reuse UTBot project (https://github.com/UnitTestBot/UTBotJava.git) +- [ ] Clone or reuse [UTBot project](https://github.com/UnitTestBot/UTBotJava.git) - [ ] Open the project in the IDE - [ ] Install the downloaded plugin @@ -29,27 +28,35 @@ assignees: '' - [ ] Generate tests for the class - [ ] Remove results - [ ] Generate and Run test for a method +- [ ] Check only expected tests are red (failing on exceptions) +- [ ] Check there are no yellow tests (failing on asserts) **Manual scenario #2** - [ ] Use default plugin settings - [ ] Open the utbot-sample/src/main/java/org/utbot/examples/mock/CommonMocksExample.java file -- [ ] Generate tests with different Mocking options combinations +- [ ] Generate and Run tests with different Mocking options +- [ ] Check only expected tests are red (failing on exceptions) +- [ ] Check there are no yellow tests (failing on asserts) +- [ ] Check generated tests consistency, layout, naming, correct mocking **Manual scenario #3** -- [ ] Create a new Gradle project with JDK 8 -- [ ] Add a simple java file to test +- [ ] Create a new Gradle project with JDK 17 +- [ ] Add a sample java file to test - [ ] Generate a test in the existing test root +- [ ] Check generated tests consistency, layout, naming **Manual scenario #4** - [ ] Create a new Maven project with JDK 8 -- [ ] Add a simple java file to test +- [ ] Add a sample java file to test - [ ] Generate a test with a new test root +- [ ] Check generated tests consistency, layout, naming **Manual scenario #5** -- [ ] Create a new Idea project with JDK 11 -- [ ] Add a simple java file to test +- [ ] Create a new IntelliJ project with JDK 11 +- [ ] Add a sample java file to test - [ ] Generate tests for several classes +- [ ] Check generated tests consistency, layout, naming diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..90ca761682 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "gradle" # See documentation for possible values + directories: + - "/utbot-intellij" + - "/utbot-framework" + schedule: + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2590811bdd..afc8194fb8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,39 +1,61 @@ -# Description +## Labels (hint) -Substitute this text with a concise description of the proposed change. Emphasize, why particular solution was chosen. +Choose the obligatory labels: +- "ctg" (category): _bug-fix_, _enhancement_, _refactoring_, etc. +- "comp" (component): _symbolic-engine_, _fuzzing_, _codegen_, etc. + +Feel free to apply more labels to your PR, e.g.: _lang-java_, _priority-minor_, _spec-performance_ + +## Title (hint) + +Describe what you've changed or added in terms of functionality. + +For example: + +> Add summaries for the nested classes + +> Support test generation for paths with spaces in JavaScript + +> Remove packageName property not defined in Java 8 + +Check that the title contains +* no branch name +* no GitHub nickname +* no copy-pasted issue title + +## Description Fixes # (issue) -## Type of Change +Add more info _if needed_: +* context/purpose for implementing changes +* detailed description of the changes made -Please delete options that are not relevant. +## How to test -- Minor bug fix (non-breaking small changes) -- Bug fix (non-breaking change which fixes an issue) -- Refactoring (typos and non-functional changes) -- New feature (non-breaking change which adds functionality) -- Breaking change (fix or feature that would cause existing functionality to not work as expected) +### Automated tests -# How Has This Been Tested? +Please specify the _automated tests_ for your code changes: you should either mention the existing tests or add the new ones. -## Automated Testing +For example: -Specify tests that help to verify the change automatically. +> The proposed changes are verified with tests: +> `utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt` -_Example:_ org.utbot.examples.algorithms.BinarySearchTest +### Manual tests -## Manual Scenario +If it is impossible to provide the automated tests, please reason why. Usually, it is relevant only for UI- or documentation-related PRs. +If this is your case, share the detailed _manual scenarios_ that help to verify your changes. -Please, provide several scenarios that you went through to verify that the change worked as expected. +## Self-check list -# Checklist (remove irrelevant options): +Check off the item if the statement is true. Hint: [x] is a marked item. -_This is the author self-check list_ +Please do not delete the list or its items. -- [ ] The change followed the style guidelines of the UTBot project -- [ ] Self-review of the code is passed -- [ ] The change contains enough commentaries, particularly in hard-to-understand areas -- [ ] New documentation is provided or existed one is altered -- [ ] No new warnings -- [ ] New tests have been added -- [ ] All tests pass locally with my changes +- [ ] I've set the proper **labels** for my PR (at least, for category and component). +- [ ] PR **title** and **description** are clear and intelligible. +- [ ] I've added enough **comments** to my code, particularly in hard-to-understand areas. +- [ ] The functionality I've repaired, changed or added is covered with **automated tests**. +- [ ] **Manual tests** have been provided optionally. +- [ ] The **documentation** for the functionality I've been working on is up-to-date. \ No newline at end of file diff --git a/.github/workflows/build-and-run-tests-from-branch.yml b/.github/workflows/build-and-run-tests-from-branch.yml index 8d320ae17d..f93be3be31 100644 --- a/.github/workflows/build-and-run-tests-from-branch.yml +++ b/.github/workflows/build-and-run-tests-from-branch.yml @@ -1,5 +1,7 @@ name: "[M] UTBot Java: build and run tests" +permissions: read-all + on: workflow_dispatch: inputs: @@ -19,10 +21,30 @@ env: IMAGE_NAME: utbot_java_cli DOCKERFILE_PATH: docker/Dockerfile_java_cli # Environment variable setting gradle options. - GRADLE_OPTS: "-XX:MaxHeapSize=2048m -Dorg.gradle.jvmargs='-XX:MaxHeapSize=2048m -XX:MaxPermSize=512m -Dorg.gradle.daemon=false' -Dorg.gradle.daemon=false" + # + # When configuring Gradle behavior you can use these methods, + # listed in order of highest to lowest precedence (first one wins): + # - Command-line flags such as --build-cache. + # These have precedence over properties and environment variables. + # - System properties such as systemProp.http.proxyHost=somehost.org + # stored in a gradle.properties file in a root project directory. + # - Gradle properties such as org.gradle.caching=true that are + # typically stored in a gradle.properties file in a project + # directory or in the GRADLE_USER_HOME. + # - Environment variables such as GRADLE_OPTS sourced by the + # environment that executes Gradle. + # + # read more at: https://docs.gradle.org/current/userguide/build_environment.html + # + # example of GRADLE_OPTS: +# GRADLE_OPTS: "-XX:MaxHeapSize=2048m -Dorg.gradle.daemon=false -Dorg.gradle.parallel=false -Dkotlin.compiler.execution.strategy=in-process" + PUSHGATEWAY_HOSTNAME: monitoring.utbot.org + ELK_HOSTNAME: logs.utbot.org + FILEBEAT_DIR: /tmp/filebeat jobs: prepare-matrices: + permissions: read-all runs-on: ubuntu-latest # Outputs are used for passing data to dependent jobs. outputs: @@ -46,11 +68,14 @@ jobs: run: | FRAMEWORK_TESTS=$(echo $(cat .github/workflows/framework-tests-matrix.json)) COMBINED_PROJECTS=$(echo $(cat .github/workflows/combined-projects-matrix.json)) - echo "::set-output name=framework-tests-matrix::$FRAMEWORK_TESTS" - echo "::set-output name=combined-projects-matrix::$COMBINED_PROJECTS" + echo "framework-tests-matrix=$FRAMEWORK_TESTS" >> $GITHUB_OUTPUT + echo "combined-projects-matrix=$COMBINED_PROJECTS" >> $GITHUB_OUTPUT echo $FRAMEWORK_TESTS echo $COMBINED_PROJECTS + + framework-tests: + permissions: read-all needs: prepare-matrices # Using matrices let create multiple jobs runs based on the combinations of the variables from matrices. # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs @@ -59,7 +84,11 @@ jobs: fail-fast: false matrix: ${{ fromJson(needs.prepare-matrices.outputs.framework-tests-matrix) }} runs-on: ubuntu-20.04 - container: unittestbot/java-env:java11-zulu-jdk-gradle7.4.2-kotlinc1.7.0 + container: + image: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 + volumes: + - "/home/runner/runners:/home/runner/runners" + - "/tmp/filebeat:/tmp/filebeat" steps: - name: Print environment variables run: printenv @@ -74,10 +103,20 @@ jobs: git checkout ${{ github.event.inputs.commit_sha }} - name: Run monitoring + # secret uploaded using base64 encoding to have one-line output: + # cat file | base64 -w 0 + continue-on-error: true run: | - echo Find your Prometheus metrics using label {instance=\"${GITHUB_RUN_ID}-${HOSTNAME}\"} chmod +x ./scripts/project/monitoring.sh - ./scripts/project/monitoring.sh ${{ secrets.PUSHGATEWAY_HOSTNAME }} ${{ secrets.PUSHGATEWAY_USER }} ${{ secrets.PUSHGATEWAY_PASSWORD }} + ./scripts/project/monitoring.sh "${PUSHGATEWAY_HOSTNAME}" "${{ secrets.PUSHGATEWAY_USER }}" "${{ secrets.PUSHGATEWAY_PASSWORD }}" + echo "Please visit Grafana to check metrics: https://${PUSHGATEWAY_HOSTNAME}/d/rYdddlPWk/node-exporter-full?orgId=1&from=now-1h&to=now&var-service=github&var-instance=${GITHUB_RUN_ID}-${HOSTNAME}&refresh=1m" + echo --- + printf ${{ secrets.CA_CERT }} | base64 -d > ${{ env.FILEBEAT_DIR }}/ca.crt + printf ${{ secrets.CLIENT_CRT }} | base64 -d > ${{ env.FILEBEAT_DIR }}/client.crt + printf ${{ secrets.CLIENT_KEY }} | base64 -d > ${{ env.FILEBEAT_DIR }}/client.key + chmod +x ./scripts/project/logging.sh + ./scripts/project/logging.sh "${FILEBEAT_DIR}" "${{ secrets.ELK_HOST }}:5044" + echo "Please visit ELK to check logs https://logs.utbot.org/app/discover#/ using the following search pattern: github.env.HOSTNAME:\"${HOSTNAME}\" and github.env.GITHUB_RUN_ID:\"${GITHUB_RUN_ID}\" and not github.log_level:\"INFO\"" # cache will use the key you provided and contains the files you specify in path. # @@ -97,9 +136,10 @@ jobs: # hashFiles returns a single hash for the set of files that matches the path pattern key: ${{ runner.os }}-gradle-framework-${{ hashFiles('./*.gradle*', './utbot-framework*/*.gradle*') }} restore-keys: ${{ runner.os }}-gradle-framework + - name: Run tests run: | - gradle --build-cache --no-daemon :utbot-framework-test:test ${{ matrix.project.TESTS_TO_RUN }} + gradle --no-daemon --build-cache --no-parallel -PprojectType=Ultimate -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} -Dorg.gradle.jvmargs=-Xmx6g -Dkotlin.daemon.jvm.options=-Xmx4g :utbot-framework-test:test ${{ matrix.project.TESTS_TO_RUN }} - name: Upload logs if: ${{ always() }} @@ -115,7 +155,7 @@ jobs: name: utbot_temp ${{ matrix.project.PART_NAME }} path: | /tmp/UTBot/generated*/* - /tmp/UTBot/utbot-childprocess-errors/* + /tmp/UTBot/utbot-instrumentedprocess-errors/* - name: Upload test report if tests have failed if: ${{ failure() }} uses: actions/upload-artifact@v3 @@ -123,19 +163,92 @@ jobs: name: test_report ${{ matrix.project.PART_NAME }} path: utbot-framework-test/build/reports/tests/test/* + + spring-tests: + permissions: read-all + runs-on: ubuntu-20.04 + container: + image: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 + volumes: + - "/home/runner/runners:/home/runner/runners" + - "/tmp/filebeat:/tmp/filebeat" + steps: + - name: Print environment variables + run: printenv + + - name: Checkout repository + uses: actions/checkout@v3 + - name: Check out ${{ github.event.inputs.commit_sha }} commit + if: github.event.inputs.commit_sha != '' + run: | + git config --global --add safe.directory ${GITHUB_WORKSPACE} + git fetch + git checkout ${{ github.event.inputs.commit_sha }} + + - name: Run monitoring + continue-on-error: true + run: | + chmod +x ./scripts/project/monitoring.sh + ./scripts/project/monitoring.sh "${PUSHGATEWAY_HOSTNAME}" "${{ secrets.PUSHGATEWAY_USER }}" "${{ secrets.PUSHGATEWAY_PASSWORD }}" + echo "Please visit Grafana to check metrics: https://${PUSHGATEWAY_HOSTNAME}/d/rYdddlPWk/node-exporter-full?orgId=1&from=now-1h&to=now&var-service=github&var-instance=${GITHUB_RUN_ID}-${HOSTNAME}&refresh=1m" + echo --- + printf ${{ secrets.CA_CERT }} | base64 -d > ${{ env.FILEBEAT_DIR }}/ca.crt + printf ${{ secrets.CLIENT_CRT }} | base64 -d > ${{ env.FILEBEAT_DIR }}/client.crt + printf ${{ secrets.CLIENT_KEY }} | base64 -d > ${{ env.FILEBEAT_DIR }}/client.key + chmod +x ./scripts/project/logging.sh + ./scripts/project/logging.sh "${FILEBEAT_DIR}" "${{ secrets.ELK_HOST }}:5044" + echo "Please visit ELK to check logs https://logs.utbot.org/app/discover#/ using the following search pattern: github.env.HOSTNAME:\"${HOSTNAME}\" and github.env.GITHUB_RUN_ID:\"${GITHUB_RUN_ID}\" and not github.log_level:\"INFO\"" + + - uses: actions/cache@v3 + with: + path: /root/.gradle/caches + key: ${{ runner.os }}-gradle-spring-${{ hashFiles('./*.gradle*', './utbot-spring*/*.gradle*') }} + restore-keys: ${{ runner.os }}-gradle-spring + + - name: Run tests + run: | + cd utbot-spring-test + gradle --no-daemon --build-cache --no-parallel -PprojectType=Community -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} -Dorg.gradle.jvmargs=-Xmx6g -Dkotlin.daemon.jvm.options=-Xmx4g :utbot-spring-test:test + + - name: Upload logs + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: logs utbot-spring-test + path: utbot-spring-test/logs/* + - name: Upload UTBot temp directory content + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: utbot_temp utbot-spring-test + path: | + /tmp/UTBot/generated*/* + /tmp/UTBot/utbot-instrumentedprocess-errors/* + - name: Upload test report if tests have failed + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: test_report utbot-spring-test + path: utbot-spring-test/build/reports/tests/test/* + + combined-projects: # This job does not need to wait for 'prepare-tests-matrix' result. - # GitHub allocates runners portionally. Framework tests are time consuming. That's why we want to force them + # GitHub allocates runners portionally. Framework tests are time consuming. That's why we want to force them # to start execution early. needs: prepare-matrices - # Using matrices let create multiple jobs runs based on the combinations of the variables from matrices. + # Using matrices let create multiple jobs runs based on the combinations of the variables from matrices. # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs strategy: # The option forces to execute all jobs even though some of them have failed. fail-fast: false matrix: ${{ fromJson(needs.prepare-matrices.outputs.combined-projects-matrix) }} runs-on: ubuntu-20.04 - container: unittestbot/java-env:java11-zulu-jdk-gradle7.4.2-kotlinc1.7.0 + container: + image: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 + volumes: + - "/home/runner/runners:/home/runner/runners" + - "/tmp/filebeat:/tmp/filebeat" steps: - name: Print environment variables run: printenv @@ -151,9 +264,16 @@ jobs: - name: Run monitoring run: | - echo Find your Prometheus metrics using label {instance=\"${GITHUB_RUN_ID}-${HOSTNAME}\"} chmod +x ./scripts/project/monitoring.sh - ./scripts/project/monitoring.sh ${{ secrets.PUSHGATEWAY_HOSTNAME }} ${{ secrets.PUSHGATEWAY_USER }} ${{ secrets.PUSHGATEWAY_PASSWORD }} + ./scripts/project/monitoring.sh "${PUSHGATEWAY_HOSTNAME}" "${{ secrets.PUSHGATEWAY_USER }}" "${{ secrets.PUSHGATEWAY_PASSWORD }}" + echo "Please visit Grafana to check metrics: https://${PUSHGATEWAY_HOSTNAME}/d/rYdddlPWk/node-exporter-full?orgId=1&from=now-1h&to=now&var-service=github&var-instance=${GITHUB_RUN_ID}-${HOSTNAME}&refresh=1m" + echo --- + printf ${{ secrets.CA_CERT }} | base64 -d > ${{ env.FILEBEAT_DIR }}/ca.crt + printf ${{ secrets.CLIENT_CRT }} | base64 -d > ${{ env.FILEBEAT_DIR }}/client.crt + printf ${{ secrets.CLIENT_KEY }} | base64 -d > ${{ env.FILEBEAT_DIR }}/client.key + chmod +x ./scripts/project/logging.sh + ./scripts/project/logging.sh "${FILEBEAT_DIR}" "${{ secrets.ELK_HOST }}:5044" + echo "Please visit ELK to check logs https://logs.utbot.org/app/discover#/ using the following search pattern: github.env.HOSTNAME:\"${HOSTNAME}\" and github.env.GITHUB_RUN_ID:\"${GITHUB_RUN_ID}\" and not github.log_level:\"INFO\"" - uses: actions/cache@v3 with: @@ -164,13 +284,13 @@ jobs: id: first-project run: | cd ${{ matrix.projects.first }} - gradle build --build-cache --no-daemon + gradle build --no-daemon --build-cache --no-parallel -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} -Dorg.gradle.jvmargs=-Xmx6g -Dkotlin.daemon.jvm.options=-Xmx4g - name: Build project ${{ matrix.projects.second }} if: ${{ steps.first-project.outcome != 'cancelled' && steps.first-project.outcome != 'skipped' }} run: | cd ${{ matrix.projects.second }} - gradle build --build-cache --no-daemon + gradle build --no-daemon --build-cache --no-parallel -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} -Dorg.gradle.jvmargs=-Xmx6g -Dkotlin.daemon.jvm.options=-Xmx4g - name: Upload test report if tests have failed if: ${{ failure() }} @@ -185,22 +305,26 @@ jobs: with: name: test_report ${{ matrix.projects.second }} path: ${{ matrix.projects.second }}/build/reports/tests/test/* - + single-project: # This job does not need to wait for 'prepare-tests-matrix' result. - # GitHub allocates runners portionally. Framework tests are time consuming. That's why we want to force them + # GitHub allocates runners portionally. Framework tests are time consuming. That's why we want to force them # to start execution early. needs: prepare-matrices - # Using matrices let create multiple jobs runs based on the combinations of the variables from matrices. + # Using matrices let create multiple jobs runs based on the combinations of the variables from matrices. # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs strategy: # The option forces to execute all jobs even though some of them have failed. fail-fast: false matrix: - project: [utbot-core, utbot-fuzzers, utbot-gradle, utbot-junit-contest, utbot-sample] + project: [utbot-core, utbot-java-fuzzing, utbot-gradle, utbot-junit-contest, utbot-sample] runs-on: ubuntu-20.04 - container: unittestbot/java-env:java11-zulu-jdk-gradle7.4.2-kotlinc1.7.0 + container: + image: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 + volumes: + - "/home/runner/runners:/home/runner/runners" + - "/tmp/filebeat:/tmp/filebeat" steps: - name: Print environment variables run: printenv @@ -216,9 +340,16 @@ jobs: - name: Run monitoring run: | - echo Find your Prometheus metrics using label {instance=\"${GITHUB_RUN_ID}-${HOSTNAME}\"} chmod +x ./scripts/project/monitoring.sh - ./scripts/project/monitoring.sh ${{ secrets.PUSHGATEWAY_HOSTNAME }} ${{ secrets.PUSHGATEWAY_USER }} ${{ secrets.PUSHGATEWAY_PASSWORD }} + ./scripts/project/monitoring.sh "${PUSHGATEWAY_HOSTNAME}" "${{ secrets.PUSHGATEWAY_USER }}" "${{ secrets.PUSHGATEWAY_PASSWORD }}" + echo "Please visit Grafana to check metrics: https://${PUSHGATEWAY_HOSTNAME}/d/rYdddlPWk/node-exporter-full?orgId=1&from=now-1h&to=now&var-service=github&var-instance=${GITHUB_RUN_ID}-${HOSTNAME}&refresh=1m" + echo --- + printf ${{ secrets.CA_CERT }} | base64 -d > ${{ env.FILEBEAT_DIR }}/ca.crt + printf ${{ secrets.CLIENT_CRT }} | base64 -d > ${{ env.FILEBEAT_DIR }}/client.crt + printf ${{ secrets.CLIENT_KEY }} | base64 -d > ${{ env.FILEBEAT_DIR }}/client.key + chmod +x ./scripts/project/logging.sh + ./scripts/project/logging.sh "${FILEBEAT_DIR}" "${{ secrets.ELK_HOST }}:5044" + echo "Please visit ELK to check logs https://logs.utbot.org/app/discover#/ using the following search pattern: github.env.HOSTNAME:\"${HOSTNAME}\" and github.env.GITHUB_RUN_ID:\"${GITHUB_RUN_ID}\" and not github.log_level:\"INFO\"" - uses: actions/cache@v3 with: @@ -228,7 +359,7 @@ jobs: - name: Run tests run: | cd ${{ matrix.project }} - gradle build --build-cache --no-daemon + gradle build --no-daemon --build-cache --no-parallel -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} -Dorg.gradle.jvmargs=-Xmx6g -Dkotlin.daemon.jvm.options=-Xmx4g - name: Upload test report if tests have failed if: ${{ failure() }} diff --git a/.github/workflows/build-and-run-tests.yml b/.github/workflows/build-and-run-tests.yml index b3c62a1559..51d5a8b330 100644 --- a/.github/workflows/build-and-run-tests.yml +++ b/.github/workflows/build-and-run-tests.yml @@ -1,5 +1,7 @@ name: "UTBot Java: build and run tests" +permissions: read-all + on: push: branches: @@ -10,83 +12,29 @@ on: - 'main' - 'unit-test-bot/r**' -env: - REGISTRY: ghcr.io - IMAGE_NAME: utbot_java_cli - DOCKERFILE_PATH: docker/Dockerfile_java_cli - # Environment variable setting gradle options. - GRADLE_OPTS: "-XX:MaxHeapSize=2048m -Dorg.gradle.jvmargs='-XX:MaxHeapSize=2048m -XX:MaxPermSize=512m -Dorg.gradle.daemon=false' -Dorg.gradle.daemon=false" - jobs: build-and-run-tests: uses: ./.github/workflows/build-and-run-tests-from-branch.yml secrets: inherit + publish_plugin: + needs: build-and-run-tests + uses: ./.github/workflows/publish-plugin-from-branch.yml + with: + # upload artifacts on push action to main only + upload-artifact: ${{ github.event_name == 'push' }} + secrets: inherit + + publish_cli: + needs: build-and-run-tests + uses: ./.github/workflows/publish-cli-from-branch.yml + with: + # upload artifacts on push action to main only + upload-artifact: ${{ github.event_name == 'push' }} + secrets: inherit publish-cli-image: needs: build-and-run-tests if: ${{ github.event_name == 'push' }} - runs-on: ubuntu-20.04 - container: unittestbot/java-env:java11-zulu-jdk-gradle7.4.2-kotlinc1.7.0 - steps: - - name: Print environment variables - run: printenv - - - uses: actions/checkout@v3 - - - name: Set environment variables - run: | - # "You can make an environment variable available to any subsequent steps in a workflow job by - # defining or updating the environment variable and writing this to the GITHUB_ENV environment file." - echo VERSION="$(date +%Y).$(date +%-m)" >> $GITHUB_ENV - - - name: Build UTBot Java CLI - run: | - cd utbot-cli - gradle build --no-daemon -x test -PsemVer=${{ env.VERSION }} - - name: Set docker tag - run: - # "You can make an environment variable available to any subsequent steps in a workflow job by - # defining or updating the environment variable and writing this to the GITHUB_ENV environment file." - echo DOCKER_TAG="$(date +%Y).$(date +%-m).$(date +%-d)-${{ github.sha }}" >> $GITHUB_ENV - - - name: Log in to the Container registry - uses: docker/login-action@v2 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Cache Docker layers - uses: actions/cache@v3 - 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 UTBOT_JAVA_CLI=utbot-cli/build/libs/utbot-cli-${{ env.VERSION }}.jar \ - --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 + uses: ./.github/workflows/publish-cli-image-from-branch.yml + secrets: inherit diff --git a/.github/workflows/collect-statistics.yml b/.github/workflows/collect-statistics.yml index 52696b4c9a..e330927f95 100644 --- a/.github/workflows/collect-statistics.yml +++ b/.github/workflows/collect-statistics.yml @@ -1,4 +1,6 @@ -name: "UTBot Java: collect statistics" +name: "[M] UTBot Java: collect statistics" + +permissions: read-all on: workflow_call: @@ -9,7 +11,7 @@ on: default: '1' type: string run_number: - description: 'Number of run tries per runner' + description: 'Number of run tries per runner (values greater than 1 are not supported with grafana)' required: false default: '1' type: string @@ -18,8 +20,13 @@ on: required: false default: manual-run type: string - aggregate: - description: 'Aggregate data' + push_results: + description: 'Push metrics into github' + required: false + default: false + type: boolean + send_to_grafana: + description: 'Send metrics to grafana' required: false default: false type: boolean @@ -32,7 +39,7 @@ on: default: '1' type: string run_number: - description: 'Number of run tries per runner' + description: 'Number of run tries per runner (values greater than 1 are not supported with grafana)' required: false default: '1' type: string @@ -41,8 +48,13 @@ on: required: false default: manual-run type: string - aggregate: - description: 'Aggregate data' + push_results: + description: 'Push metrics into github' + required: false + default: false + type: boolean + send_to_grafana: + description: 'Send metrics to grafana' required: false default: false type: boolean @@ -50,36 +62,52 @@ on: env: data_branch: monitoring-data data_path: monitoring/data - aggregated_data_branch: monitoring-aggregated-data - aggregated_data_path: monitoring/aggregated_data - monitoring_properties: monitoring/monitoring.properties + monitoring_projects: monitoring/projects/ push_script: monitoring/push_with_rebase.sh + PUSHGATEWAY_HOSTNAME: monitoring.utbot.org + PUSHGATEWAY_ADDITIONAL_PATH: /pushgateway-custom + PROM_ADDITIONAL_LABELS: /service/github jobs: setup_matrix: runs-on: ubuntu-latest outputs: + projects: ${{ steps.set-matrix.outputs.projects }} matrix: ${{ steps.set-matrix.outputs.matrix }} steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Create matrix id: set-matrix + shell: bash run: | + read -r -a projects <<< $(ls --format=horizontal --indicator-style=none $monitoring_projects) + projects=(${projects[@]/#/\"}) + projects=(${projects[@]/%/\"}) + printf -v projects '%s,' "${projects[@]}" + projects=$(echo [${projects%,}]) + echo "projects=$projects" >> $GITHUB_OUTPUT + echo $projects + arr=$(echo [$(seq -s , ${{ inputs.runners }})]) - echo "::set-output name=matrix::$arr" + echo "matrix=$arr" >> $GITHUB_OUTPUT echo $arr build_and_collect_statistics: needs: setup_matrix continue-on-error: true strategy: + # temporary commented, remove completely after 10.23 if all pipelines worked well + #max-parallel: 3 matrix: + project: ${{ fromJson(needs.setup_matrix.outputs.projects) }} value: ${{ fromJson(needs.setup_matrix.outputs.matrix) }} runs-on: ubuntu-20.04 - container: unittestbot/java-env:java11-zulu-jdk-fx-gradle7.4.2-kotlinc1.7.0 + container: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 steps: - name: Install git run: | - apt-get upgrade -y apt-get update -y apt-get install git -y git config --global --add safe.directory $(pwd) @@ -93,6 +121,25 @@ jobs: ref: ${{ env.data_branch }} path: ${{ env.data_path }} + - name: Expand system swap + shell: bash + # trying to configure swap on host from running container + run: | + docker run -d --rm --name busybox --privileged --net=host --pid=host --ipc=host --volume /:/host busybox sleep infinity + docker exec busybox /bin/sh -c 'chroot /host /bin/bash -c "swapoff /mnt/swapfile"' + docker exec busybox /bin/sh -c 'chroot /host /bin/bash -c "dd if=/dev/zero of=/mnt/swapfile bs=1M count=8192 oflag=append conv=notrunc"' + docker exec busybox /bin/sh -c 'chroot /host /bin/bash -c "mkswap /mnt/swapfile"' + docker exec busybox /bin/sh -c 'chroot /host /bin/bash -c "swapon /mnt/swapfile"' + + - name: Run system monitoring + # secret uploaded using base64 encoding to have one-line output: + # cat file | base64 -w 0 + continue-on-error: true + run: | + chmod +x ./scripts/project/monitoring.sh + ./scripts/project/monitoring.sh "${PUSHGATEWAY_HOSTNAME}" "${{ secrets.PUSHGATEWAY_USER }}" "${{ secrets.PUSHGATEWAY_PASSWORD }}" + echo "Please visit Grafana to check metrics: https://${PUSHGATEWAY_HOSTNAME}/d/rYdddlPWk/node-exporter-full?orgId=1&from=now-1h&to=now&var-service=github&var-instance=${GITHUB_RUN_ID}-${HOSTNAME}&refresh=1m" + - uses: actions/setup-python@v4 with: python-version: '3.9' @@ -103,7 +150,57 @@ jobs: for i in $(seq ${{ inputs.run_number }}) do java -jar \ - -Dutbot.monitoring.settings.path=$monitoring_properties \ + --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED \ + --add-opens java.base/java.lang.invoke=ALL-UNNAMED \ + --add-opens java.base/java.util.concurrent=ALL-UNNAMED \ + --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED \ + --add-opens java.base/java.text=ALL-UNNAMED \ + --add-opens java.base/java.time=ALL-UNNAMED \ + --add-opens java.base/java.io=ALL-UNNAMED \ + --add-opens java.base/java.nio=ALL-UNNAMED \ + --add-opens java.base/java.nio.file=ALL-UNNAMED \ + --add-opens java.base/java.net=ALL-UNNAMED \ + --add-opens java.base/sun.security.util=ALL-UNNAMED \ + --add-opens java.base/sun.reflect.generics.repository=ALL-UNNAMED \ + --add-opens java.base/sun.net.util=ALL-UNNAMED \ + --add-opens java.base/sun.net.fs=ALL-UNNAMED \ + --add-opens java.base/java.security=ALL-UNNAMED \ + --add-opens java.base/java.lang.ref=ALL-UNNAMED \ + --add-opens java.base/java.math=ALL-UNNAMED \ + --add-opens java.base/java.util.stream=ALL-UNNAMED \ + --add-opens java.base/java.util=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.misc=ALL-UNNAMED \ + --add-opens java.base/java.lang=ALL-UNNAMED \ + --add-opens java.base/java.lang.reflect=ALL-UNNAMED \ + --add-opens java.base/sun.security.provider=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.event=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.jimage=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.jimage.decompressor=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.jmod=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.jtrfs=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.logger=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.math=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.misc=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.module=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.org.objectweb.asm.commons=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.org.objectweb.asm.signature=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.org.objectweb.asm.tree=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.org.objectweb.asm.tree.analysis=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.org.objectweb.asm.util=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.org.xml.sax=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.org.xml.sax.helpers=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.perf=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.platform=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.ref=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.util=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.util.xml=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.util.xml.impl=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.vm=ALL-UNNAMED \ + --add-opens java.base/jdk.internal.vm.annotation=ALL-UNNAMED \ + -Dutbot.monitoring.settings.path=$monitoring_projects/${{ matrix.project }}/monitoring.properties \ utbot-junit-contest/build/libs/monitoring.jar \ stats-$i.json mv logs/utbot.log logs/utbot-$i.log @@ -112,27 +209,31 @@ jobs: - name: Get current date id: date run: | - echo "::set-output name=date::$(date +'%Y-%m-%d')" - echo "::set-output name=timestamp::$(date +%s)" - echo "::set-output name=last_month::$(date --date='last month' +%s)" + echo "date=$(date +'%Y-%m-%d-%H-%M-%S')" >> $GITHUB_OUTPUT + echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT + echo "last_month=$(date --date='last month' +%s)" >> $GITHUB_OUTPUT - name: Get metadata id: metadata run: | - echo "::set-output name=commit::$(git rev-parse HEAD)" - echo "::set-output name=short_commit::$(git rev-parse --short HEAD)" - echo "::set-output name=branch::$(git name-rev --name-only HEAD)" - echo "::set-output name=build::$(date +'%Y.%-m')" + echo "commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "short_commit=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "branch=$(git name-rev --name-only HEAD)" >> $GITHUB_OUTPUT + echo "build=$(date +'%Y.%-m')" >> $GITHUB_OUTPUT - name: Insert metadata + id: insert shell: bash run: | - OUT_FILE="$data_path/data-$branch-$date-$timestamp-$short_commit-${{ matrix.value }}.json" + OUT_FILE="$data_path/$date-$short_commit-${{ matrix.project }}-${{ matrix.value }}.json" + echo "output=$OUT_FILE" >> $GITHUB_OUTPUT + INPUTS=($(seq ${{ inputs.run_number }})) INPUTS=(${INPUTS[@]/#/stats-}) INPUTS=(${INPUTS[@]/%/.json}) INPUTS=${INPUTS[@]} echo $INPUTS + python monitoring/insert_metadata.py \ --stats_file $INPUTS \ --output_file "$OUT_FILE" \ @@ -151,7 +252,14 @@ jobs: build: ${{ steps.metadata.outputs.build }} run_id: ${{ github.run_id }}-${{ matrix.value }} + - name: Upload statistics + uses: actions/upload-artifact@v3 + with: + name: statistics-${{ matrix.value }}-${{ matrix.project }} + path: ${{ steps.insert.outputs.output }} + - name: Commit and push statistics + if: ${{ inputs.push_results }} run: | chmod +x $push_script ./$push_script @@ -161,63 +269,27 @@ jobs: message: ${{ inputs.message_prefix }}-${{ steps.date.outputs.date }} github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Send metrics to grafana + if: ${{ inputs.send_to_grafana }} + run: | + python monitoring/prepare_metrics.py --stats_file $stats_file --output_file grafana_metrics.json + chmod +x scripts/project/json_to_prometheus.py + python3 scripts/project/json_to_prometheus.py grafana_metrics.json | curl -u "${{ secrets.PUSHGATEWAY_USER }}:${{ secrets.PUSHGATEWAY_PASSWORD }}" --data-binary @- "https://${PUSHGATEWAY_HOSTNAME}${PUSHGATEWAY_ADDITIONAL_PATH}/metrics/job/pushgateway-custom/instance/run-${{ matrix.value }}-${{ matrix.project }}${PROM_ADDITIONAL_LABELS}" + echo "Please visit Grafana to check metrics: https://monitoring.utbot.org/d/m6bagaD4z/utbot-nightly-statistic" + env: + stats_file: ${{ steps.insert.outputs.output }} + - name: Upload logs if: ${{ always() }} uses: actions/upload-artifact@v3 with: - name: logs-${{ matrix.value }} + name: logs-${{ matrix.value }}-${{ matrix.project }} path: logs/ - aggregate: - needs: build_and_collect_statistics - if: ${{ inputs.aggregate }} - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Checkout monitoring data - uses: actions/checkout@v3 - with: - ref: ${{ env.data_branch }} - path: ${{ env.data_path }} - - - name: Checkout aggregated monitoring data - uses: actions/checkout@v3 - with: - ref: ${{ env.aggregated_data_branch }} - path: ${{ env.aggregated_data_path }} - - - uses: actions/setup-python@v4 + - name: Upload artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v3 with: - python-version: '3.9' - - - name: Get current date - id: date - run: | - echo "::set-output name=date::$(date +'%Y-%m-%d')" - echo "::set-output name=timestamp::$(date +%s)" - echo "::set-output name=last_month::$(date --date='last month' +%s)" - - - name: Build aggregated data (last month) - run: | - OUT_FILE=$aggregated_data_path/aggregated-data-$date.json - python monitoring/build_aggregated_data.py \ - --input_data_dir $data_path \ - --output_file $OUT_FILE \ - --timestamp_from $timestamp_from \ - --timestamp_to $timestamp - env: - date: ${{ steps.date.outputs.date }} - timestamp: ${{ steps.date.outputs.timestamp }} - timestamp_from: ${{ steps.date.outputs.last_month }} - - - name: Commit and push aggregated statistics - run: | - chmod +x $push_script - ./$push_script - env: - target_branch: ${{ env.aggregated_data_branch }} - target_directory: ${{ env.aggregated_data_path }} - message: ${{ inputs.message_prefix }}-${{ steps.date.outputs.date }} - github_token: ${{ secrets.GITHUB_TOKEN }} + name: generated-${{ matrix.value }}-${{ matrix.project }} + path: | + /tmp/UTBot/generated*/* diff --git a/.github/workflows/framework-tests-matrix.json b/.github/workflows/framework-tests-matrix.json index 67448bca1c..121eed2a0d 100644 --- a/.github/workflows/framework-tests-matrix.json +++ b/.github/workflows/framework-tests-matrix.json @@ -1,32 +1,64 @@ { "project": [ { - "PART_NAME": "composite", - "TESTS_TO_RUN": "--tests \"org.utbot.examples.manual.*\" --tests \"org.utbot.examples.stream.*\" --tests \"org.utbot.engine.*\" --tests \"org.utbot.framework.*\" --tests \"org.utbot.sarif.*\"" + "PART_NAME": "composite-part1", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.manual.*\"" }, { - "PART_NAME": "collections-part1", - "TESTS_TO_RUN": "--tests \"org.utbot.examples.collections.CustomerExamplesTest\" --tests \"org.utbot.examples.collections.GenericListsExampleTest\" --tests \"org.utbot.examples.collections.LinkedListsTest\" --tests \"org.utbot.examples.collections.ListAlgorithmsTest\" --tests \"org.utbot.examples.collections.ListIteratorsTest\" --tests \"org.utbot.examples.collections.ListWrapperReturnsVoidTest\" --tests \"org.utbot.examples.collections.MapEntrySetTest\" --tests \"org.utbot.examples.collections.MapKeySetTest\"" + "PART_NAME": "composite-part2", + "TESTS_TO_RUN": "--tests \"org.utbot.engine.*\" --tests \"org.utbot.framework.*\" --tests \"org.utbot.sarif.*\" --tests \"org.utbot.examples.taint.*\"" }, { - "PART_NAME": "collections-part2", - "TESTS_TO_RUN": "--tests \"org.utbot.examples.collections.MapValuesTest\" --tests \"org.utbot.examples.collections.OptionalsTest\" --tests \"org.utbot.examples.collections.SetIteratorsTest\"" + "PART_NAME": "composite-part3", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.stream.*\"" + }, + { + "PART_NAME": "collections-part1", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.collections.MapValuesTest\" --tests \"org.utbot.examples.collections.OptionalsTest\" --tests \"org.utbot.examples.collections.SetIteratorsTest\" --tests \"org.utbot.examples.collections.CustomerExamplesTest\" --tests \"org.utbot.examples.collections.GenericListsExampleTest\" --tests \"org.utbot.examples.collections.LinkedListsTest\" --tests \"org.utbot.examples.collections.ListAlgorithmsTest\"" }, { - "PART_NAME": "collections-part3", - "TESTS_TO_RUN": "--tests \"org.utbot.examples.collections.SetsTest\"" + "PART_NAME": "collections-part2", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.collections.SetsTest\" --tests \"org.utbot.examples.collections.ListIteratorsTest\" --tests \"org.utbot.examples.collections.ListWrapperReturnsVoidTest\" --tests \"org.utbot.examples.collections.MapEntrySetTest\" --tests \"org.utbot.examples.collections.MapKeySetTest\"" }, { "PART_NAME": "examples-part1", - "TESTS_TO_RUN": "--tests \"org.utbot.examples.algorithms.*\" --tests \"org.utbot.examples.annotations.*\" --tests \"org.utbot.examples.arrays.*\" --tests \"org.utbot.examples.casts.*\" --tests \"org.utbot.examples.codegen.*\" --tests \"org.utbot.examples.controlflow.*\" --tests \"org.utbot.examples.enums.*\"" + "TESTS_TO_RUN": "--tests \"org.utbot.examples.annotations.*\" --tests \"org.utbot.examples.arrays.*\" --tests \"org.utbot.examples.casts.*\" --tests \"org.utbot.examples.controlflow.*\" --tests \"org.utbot.examples.enums.*\"" }, { "PART_NAME": "examples-part2", - "TESTS_TO_RUN": "--tests \"org.utbot.examples.exceptions.*\" --tests \"org.utbot.examples.invokes.*\" --tests \"org.utbot.examples.lambda.*\" --tests \"org.utbot.examples.make.symbolic.*\" --tests \"org.utbot.examples.math.*\" --tests \"org.utbot.examples.mixed.*\" --tests \"org.utbot.examples.mock.*\" --tests \"org.utbot.examples.models.*\" --tests \"org.utbot.examples.natives.*\" --tests \"org.utbot.examples.objects.*\"" + "TESTS_TO_RUN": "--tests \"org.utbot.examples.invokes.*\" --tests \"org.utbot.examples.lambda.*\" --tests \"org.utbot.examples.make.symbolic.*\" --tests \"org.utbot.examples.math.*\" --tests \"org.utbot.examples.mixed.*\"" }, { "PART_NAME": "examples-part3", - "TESTS_TO_RUN": "--tests \"org.utbot.examples.primitives.*\" --tests \"org.utbot.examples.recursion.*\" --tests \"org.utbot.examples.statics.substitution.*\" --tests \"org.utbot.examples.stdlib.*\" --tests \"org.utbot.examples.strings.*\" --tests \"org.utbot.examples.structures.*\" --tests \"org.utbot.examples.thirdparty.numbers.*\" --tests \"org.utbot.examples.types.*\" --tests \"org.utbot.examples.unsafe.*\" --tests \"org.utbot.examples.wrappers.*\"" + "TESTS_TO_RUN": "--tests \"org.utbot.examples.primitives.*\"" + }, + { + "PART_NAME": "examples-part4", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.thirdparty.numbers.*\" --tests \"org.utbot.examples.types.*\" --tests \"org.utbot.examples.unsafe.*\" --tests \"org.utbot.examples.wrappers.*\" --tests \"org.utbot.examples.recursion.*\" --tests \"org.utbot.examples.statics.substitution.*\" --tests \"org.utbot.examples.stdlib.*\" --tests \"org.utbot.examples.structures.*\"" + }, + { + "PART_NAME": "examples-part5", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.strings.*\"" + }, + { + "PART_NAME": "examples-part6", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.strings11.*\"" + }, + { + "PART_NAME": "examples-part7", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.algorithms.*\"" + }, + { + "PART_NAME": "examples-part8", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.codegen.*\"" + }, + { + "PART_NAME": "examples-part9", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.mock.*\" --tests \"org.utbot.examples.models.*\" --tests \"org.utbot.examples.natives.*\" --tests \"org.utbot.examples.objects.*\" --tests \"org.utbot.examples.reflection.*\" --tests \"org.utbot.examples.threads.*\"" + }, + { + "PART_NAME": "examples-part10", + "TESTS_TO_RUN": "--tests \"org.utbot.examples.exceptions.*\"" }, { "PART_NAME": "examples-lists", diff --git a/.github/workflows/issue-to-project.yml b/.github/workflows/issue-to-project.yml index 07f44137e2..fc53eb2da5 100644 --- a/.github/workflows/issue-to-project.yml +++ b/.github/workflows/issue-to-project.yml @@ -1,5 +1,7 @@ name: Add issues to UTBot Java project +permissions: read-all + on: issues: types: @@ -14,9 +16,3 @@ jobs: 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/night-statistics-monitoring.yml b/.github/workflows/night-statistics-monitoring.yml index c7cda2307b..b0b134521f 100644 --- a/.github/workflows/night-statistics-monitoring.yml +++ b/.github/workflows/night-statistics-monitoring.yml @@ -1,5 +1,7 @@ name: "UTBot Java: night statistics monitoring" +permissions: read-all + on: schedule: - cron: '0 0 * * *' @@ -12,4 +14,5 @@ jobs: runners: 3 run_number: 1 message_prefix: night-monitoring - aggregate: true + push_results: true + send_to_grafana: true diff --git a/.github/workflows/public-rider-plugin.yml b/.github/workflows/public-rider-plugin.yml new file mode 100644 index 0000000000..587a7cf7bb --- /dev/null +++ b/.github/workflows/public-rider-plugin.yml @@ -0,0 +1,86 @@ +# This is a basic workflow that is manually triggered + +name: Publish Rider plugin + +permissions: read-all + +# Controls when the action will run. Workflow runs when manually triggered using the UI +# or API. +on: + workflow_dispatch: + # Inputs the workflow accepts. + inputs: + minor-release: + type: choice + description: "It adds minor release indicator to version." + required: true + default: 'none' + options: + - 'none' + - '1' + - '2' + - '3' + - '4' + + version-postfix: + type: choice + description: "It adds alpha or beta postfix to version." + required: true + default: no-postfix-prod + options: + - no-postfix-prod + - no-postfix + - alpha + - beta + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "greet" + public_rider_plugin: + # The type of runner that the job will run on + runs-on: ubuntu-20.04 + container: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Runs a single command using the runners shell + - name: Print environment variables + run: printenv + + - uses: actions/checkout@v3 + + - name: Set environment variables + run: | + # "You can make an environment variable available to any subsequent steps in a workflow job by + # defining or updating the environment variable and writing this to the GITHUB_ENV environment file." + echo "VERSION="$(date +%Y).$(date +%-m).${GITHUB_RUN_NUMBER}"" >> $GITHUB_ENV + echo "POSTFIX=${{ github.event.inputs.version-postfix }}" >> $GITHUB_ENV + + - name: Set production version + if: ${{ github.event.inputs.version-postfix == 'no-postfix-prod' || github.event.inputs.version-postfix == 'alpha' || github.event.inputs.version-postfix == 'beta' }} + run: | + echo "VERSION="$(date +%Y).$(date +%-m)"" >> $GITHUB_ENV + + - name: Set version for minor release + if: ${{ github.event.inputs.minor-release != 'none' }} + run: | + echo "VERSION=${{ env.VERSION }}.${{ github.event.inputs.minor-release }}" >> $GITHUB_ENV + + - name: Create version with postfix + if: ${{ (env.POSTFIX == 'alpha') || (env.POSTFIX == 'beta') }} + run: + echo "VERSION=${{ env.VERSION }}-${{ env.POSTFIX }}" >> $GITHUB_ENV + + - name: Build UTBot Rider plugin + run: | + gradle clean :utbot-rider:buildPlugin --no-daemon --build-cache --no-parallel -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} -Dorg.gradle.jvmargs=-Xmx2g -Dkotlin.daemon.jvm.options=-Xmx4g -PsemVer=${{ env.VERSION }} -PincludeRiderInBuild=true + cd utbot-rider/build/distributions + unzip utbot-rider-${{ env.VERSION }}.zip + rm utbot-rider-${{ env.VERSION }}.zip + + - name: Archive UTBot Rider plugin + uses: actions/upload-artifact@v3 + with: + name: utbot-rider-${{ env.VERSION }} + path: utbot-rider/build/distributions/* + diff --git a/.github/workflows/publish-cli-from-branch.yml b/.github/workflows/publish-cli-from-branch.yml new file mode 100644 index 0000000000..217407d92e --- /dev/null +++ b/.github/workflows/publish-cli-from-branch.yml @@ -0,0 +1,101 @@ +name: "[M] CLI: publish as archive" + +permissions: read-all + +on: + workflow_call: + inputs: + upload-artifact: + type: string + description: "Upload artifacts or not" + required: false + default: false + commit_sha: + required: false + type: string + description: "(optional) Commit SHA" + custom_version: + type: string + description: "Custom version" + required: false + default: "" + + workflow_dispatch: + inputs: + upload-artifact: + type: choice + description: "Upload artifacts or not" + required: false + default: true + options: + - true + - false + commit_sha: + required: false + type: string + description: "(optional) Commit SHA" + custom_version: + type: string + description: "Custom version" + required: false + default: "" + +jobs: + publish_cli: + strategy: + fail-fast: false # force to execute all jobs even though some of them have failed + matrix: + configuration: + - plugin_type: IC + extra_options: "-PideType=IC" + lang: java + dir: utbot-cli + - plugin_type: IC + extra_options: "-PideType=IC" + lang: python + dir: utbot-cli-python + - plugin_type: IU + extra_options: "-PideType=IU" + lang: go + dir: utbot-cli-go + - plugin_type: IU + extra_options: "-PideType=IU" + lang: js + dir: utbot-cli-js + runs-on: ubuntu-20.04 + container: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 + steps: + - uses: actions/checkout@v3 + - name: Check out ${{ github.event.inputs.commit_sha }} commit + if: github.event.inputs.commit_sha != '' + run: | + git config --global --add safe.directory ${GITHUB_WORKSPACE} + git fetch + git checkout ${{ github.event.inputs.commit_sha }} + + # "You can make an environment variable available to any subsequent steps in a workflow job by + # defining or updating the environment variable and writing this to the GITHUB_ENV environment file." + - name: Setup custom version + if: ${{ github.event.inputs.custom_version != '' }} + run: | + echo "VERSION=${{ github.event.inputs.custom_version }}" >> $GITHUB_ENV + - name: Setup version + if: ${{ github.event.inputs.custom_version == '' }} + shell: bash + run: | + echo "VERSION=${GITHUB_REF_NAME:0:4}-$(date +%Y).$(date +%-m).${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV + + - name: Print environment variables + run: printenv + + - name: Build UTBot CLI + run: | + cd "${{ matrix.configuration.dir }}" + gradle clean build --no-daemon --build-cache --no-parallel ${{ matrix.configuration.extra_options }} -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} -Dorg.gradle.jvmargs=-Xmx2g -Dkotlin.daemon.jvm.options=-Xmx4g -PsemVer=${{ env.VERSION }} + + - name: Archive UTBot CLI + if: ${{ inputs.upload-artifact == 'true' }} + uses: actions/upload-artifact@v3 + with: + name: utbot-cli-${{ matrix.configuration.lang }}-${{ env.VERSION }} + path: ${{ matrix.configuration.dir }}/build/libs/${{ matrix.configuration.dir }}-${{ env.VERSION }}.jar diff --git a/.github/workflows/publish-cli-image-from-branch.yml b/.github/workflows/publish-cli-image-from-branch.yml new file mode 100644 index 0000000000..d53b47dd87 --- /dev/null +++ b/.github/workflows/publish-cli-image-from-branch.yml @@ -0,0 +1,99 @@ +name: "[M] CLI: publish docker image" + +permissions: read-all + +on: + workflow_call: + workflow_dispatch: + +env: + REGISTRY: ghcr.io + +jobs: + publish-cli-image: + strategy: + fail-fast: false # force to execute all jobs even though some of them have failed + matrix: + configuration: + - image_name: utbot_java_cli + directory: utbot-cli + extra_options: "" + - image_name: utbot_js_cli + directory: utbot-cli-js + extra_options: "-PbuildType=ALL" + # we can't use utbot_python_cli image name because of the bug while pushing image + # ERROR: unexpected status: 403 Forbidden + - image_name: utbot_py_cli + directory: utbot-cli-python + extra_options: "" + runs-on: ubuntu-20.04 + container: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 + steps: + - uses: actions/checkout@v3 + + # "You can make an environment variable available to any subsequent steps in a workflow job by + # defining or updating the environment variable and writing this to the GITHUB_ENV environment file." + - name: Set environment variables + run: | + echo VERSION="$(date +%Y).$(date +%-m)" >> $GITHUB_ENV + echo DOCKER_TAG="$(date +%Y).$(date +%-m).$(date +%-d)-$(echo -n ${GITHUB_SHA} | cut -c 1-7)" >> $GITHUB_ENV + + - name: Print environment variables + run: printenv + + - name: Build UTBot CLI + run: | + cd ${{ matrix.configuration.directory }} + gradle build --no-daemon --build-cache --no-parallel ${{ matrix.configuration.extra_options }} -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} -Dorg.gradle.jvmargs=-Xmx2g -Dkotlin.daemon.jvm.options=-Xmx4g -x test -PsemVer=${{ env.VERSION }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + # fix of containerd issue, see https://github.com/containerd/containerd/issues/7972 + # could be removed as soon as new containerd version will be released and included in buildkit + driver-opts: | + image=moby/buildkit:v0.10.6 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ matrix.configuration.image_name }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-${{ matrix.configuration.image_name }}- + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ github.repository }}/${{ matrix.configuration.image_name }} + tags: | + type=raw,value=${{ env.DOCKER_TAG }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: | + ${{ steps.meta.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new + file: ${{ matrix.configuration.directory }}/Dockerfile + build-args: | + ARTIFACT_PATH=${{ matrix.configuration.directory }}/build/libs/${{ matrix.configuration.directory }}-${{ env.VERSION }}.jar + + # 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 index 69fd73e0bc..31bbbfd47f 100644 --- a/.github/workflows/publish-on-github-packages.yml +++ b/.github/workflows/publish-on-github-packages.yml @@ -1,5 +1,7 @@ name: "[M] Publish on GitHub Packages" +permissions: read-all + on: workflow_dispatch: inputs: @@ -7,10 +9,6 @@ on: type: string required: true description: "commit SHA: e.g. cab4799c" - -env: - # Environment variable setting gradle options. - GRADLE_OPTS: "-XX:MaxHeapSize=2048m -Dorg.gradle.jvmargs='-XX:MaxHeapSize=2048m -XX:MaxPermSize=512m -Dorg.gradle.daemon=false' -Dorg.gradle.daemon=false" jobs: build-and-run-tests: @@ -37,7 +35,7 @@ jobs: git fetch git checkout ${{ github.event.inputs.commit_sha }} - - uses: gradle/gradle-build-action@v2 + - uses: gradle/gradle-build-action@v2.9.0 with: gradle-version: 7.4.2 arguments: publish diff --git a/.github/workflows/publish-plugin-and-cli-from-branch.yml b/.github/workflows/publish-plugin-and-cli-from-branch.yml deleted file mode 100644 index e9bdb161d6..0000000000 --- a/.github/workflows/publish-plugin-and-cli-from-branch.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: "[M] Plugin and CLI: publish as archives" - -on: - workflow_call: - inputs: - version-postfix: - type: string - description: "It adds postfix (alpha or beta) to version (optional)." - required: false - default: no-postfix - - workflow_dispatch: - inputs: - version-postfix: - type: choice - description: "It adds alpha or beta postfix to version." - required: true - default: no-postfix - options: - - no-postfix - - no-postfix-prod - - alpha - - beta - -env: - # Environment variable setting gradle options. - GRADLE_OPTS: "-XX:MaxHeapSize=2048m -Dorg.gradle.jvmargs='-XX:MaxHeapSize=2048m -XX:MaxPermSize=512m -Dorg.gradle.daemon=false' -Dorg.gradle.daemon=false" - -jobs: - publish_plugin_and_cli: - runs-on: ubuntu-20.04 - container: unittestbot/java-env:java11-zulu-jdk-gradle7.4.2-kotlinc1.7.0 - - steps: - - name: Print environment variables - run: printenv - - - uses: actions/checkout@v3 - - - name: Set environment variables - run: | - # "You can make an environment variable available to any subsequent steps in a workflow job by - # defining or updating the environment variable and writing this to the GITHUB_ENV environment file." - echo "VERSION="$(date +%Y).$(date +%-m).${GITHUB_RUN_NUMBER}"" >> $GITHUB_ENV - echo "POSTFIX=${{ github.event.inputs.version-postfix }}" >> $GITHUB_ENV - - - name: Set production version - if: ${{ github.event.inputs.version-postfix == 'no-postfix-prod' || github.event.inputs.version-postfix == 'alpha' || github.event.inputs.version-postfix == 'beta' }} - run: | - echo "VERSION="$(date +%Y).$(date +%-m)"" >> $GITHUB_ENV - - - name: Create version with postfix - if: ${{ (env.POSTFIX == 'alpha') || (env.POSTFIX == 'beta') }} - run: - echo "VERSION=${{ env.VERSION }}-${{ env.POSTFIX }}" >> $GITHUB_ENV - - - name: Build UTBot IntelliJ IDEA plugin - run: | - gradle clean 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@v3 - with: - name: utbot-intellij-${{ env.VERSION }} - path: utbot-intellij/build/distributions/* - - - name: Build UTBot CLI - run: | - cd utbot-cli - gradle clean build --no-daemon -PsemVer=${{ env.VERSION }} - - - name: Archive UTBot CLI - uses: actions/upload-artifact@v3 - with: - name: utbot-cli-${{ env.VERSION }} - path: utbot-cli/build/libs/utbot-cli-${{ env.VERSION }}.jar diff --git a/.github/workflows/publish-plugin-and-cli.yml b/.github/workflows/publish-plugin-and-cli.yml deleted file mode 100644 index 0b1ea41ad7..0000000000 --- a/.github/workflows/publish-plugin-and-cli.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: "Plugin and CLI: publish as archives" -on: - push: - branches: - - 'main' - - 'unit-test-bot/r**' - -jobs: - publish_plugin_and_cli: - uses: ./.github/workflows/publish-plugin-and-cli-from-branch.yml - secrets: inherit diff --git a/.github/workflows/publish-plugin-from-branch.yml b/.github/workflows/publish-plugin-from-branch.yml new file mode 100644 index 0000000000..7fcb8c56c6 --- /dev/null +++ b/.github/workflows/publish-plugin-from-branch.yml @@ -0,0 +1,98 @@ +name: "[M] Plugin: publish as archive" + +permissions: read-all + +on: + workflow_call: + inputs: + upload-artifact: + type: string + description: "Upload artifacts or not" + required: false + default: false + commit_sha: + required: false + type: string + description: "(optional) Commit SHA" + custom_version: + type: string + description: "Custom version" + required: false + default: "" + + workflow_dispatch: + inputs: + upload-artifact: + type: choice + description: "Upload artifacts or not" + required: false + default: true + options: + - true + - false + commit_sha: + required: false + type: string + description: "(optional) Commit SHA" + custom_version: + type: string + description: "Custom version" + required: false + default: "" + +jobs: + publish_plugin: + strategy: + fail-fast: false # force to execute all jobs even though some of them have failed + matrix: + configuration: + - plugin_type: IC + extra_options: "-PideType=IC -PprojectType=Community" + directory: utbot-intellij-main + - plugin_type: IU + extra_options: "-PideType=IU -PprojectType=Ultimate" + directory: utbot-intellij-main + - plugin_type: PY + extra_options: "-PideType=PY -PprojectType=Ultimate" + directory: utbot-python-pycharm + runs-on: ubuntu-20.04 + container: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 + steps: + - uses: actions/checkout@v3 + - name: Check out ${{ github.event.inputs.commit_sha }} commit + if: github.event.inputs.commit_sha != '' + run: | + git config --global --add safe.directory ${GITHUB_WORKSPACE} + git fetch + git checkout ${{ github.event.inputs.commit_sha }} + + # "You can make an environment variable available to any subsequent steps in a workflow job by + # defining or updating the environment variable and writing this to the GITHUB_ENV environment file." + - name: Setup custom version + if: ${{ github.event.inputs.custom_version != '' }} + run: | + echo "VERSION=${{ github.event.inputs.custom_version }}" >> $GITHUB_ENV + echo "VERSION_ARCHIVE=${{ github.event.inputs.custom_version }}" >> $GITHUB_ENV + - name: Setup version + if: ${{ github.event.inputs.custom_version == '' }} + shell: bash + run: | + echo "VERSION=$(date +%Y).$(date +%-m).${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV + echo "VERSION_ARCHIVE=${GITHUB_REF_NAME:0:4}-$(date +%Y).$(date +%-m).${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV + + - name: Print environment variables + run: printenv + + - name: Build UTBot IntelliJ IDEA plugin + run: | + gradle clean buildPlugin --no-daemon --build-cache --no-parallel -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} ${{ matrix.configuration.extra_options }} -Dorg.gradle.jvmargs=-Xmx2g -Dkotlin.daemon.jvm.options=-Xmx4g -PsemVer=${{ env.VERSION }} + cd ${{ matrix.configuration.directory }}/build/distributions + unzip ${{ matrix.configuration.directory }}-${{ env.VERSION }}.zip + rm ${{ matrix.configuration.directory }}-${{ env.VERSION }}.zip + + - name: Archive UTBot IntelliJ IDEA plugin + if: ${{ inputs.upload-artifact == 'true' }} + uses: actions/upload-artifact@v3 + with: + name: utbot-intellij-${{ matrix.configuration.plugin_type }}-${{ env.VERSION_ARCHIVE }} + path: ${{ matrix.configuration.directory }}/build/distributions/* diff --git a/.github/workflows/run-chosen-tests-from-branch.yml b/.github/workflows/run-chosen-tests-from-branch.yml index e419ea0e0f..2e24f6fbd1 100644 --- a/.github/workflows/run-chosen-tests-from-branch.yml +++ b/.github/workflows/run-chosen-tests-from-branch.yml @@ -1,5 +1,7 @@ name: "[M] Run chosen tests" +permissions: read-all + on: workflow_dispatch: inputs: @@ -14,7 +16,7 @@ on: - utbot-core - utbot-framework-api - utbot-framework - - utbot-fuzzers + - utbot-java-fuzzing - utbot-gradle - utbot-instrumentation-tests - utbot-instrumentation @@ -28,13 +30,12 @@ on: description: "{package-name}.{class-name-optional}.{test-name-optional}" env: - # Environment variable setting gradle options. - GRADLE_OPTS: "-XX:MaxHeapSize=2048m -Dorg.gradle.jvmargs='-XX:MaxHeapSize=2048m -XX:MaxPermSize=512m -Dorg.gradle.daemon=false' -Dorg.gradle.daemon=false" + PUSHGATEWAY_HOSTNAME: monitoring.utbot.org jobs: run-chosen-tests: runs-on: ubuntu-20.04 - container: unittestbot/java-env:java11-zulu-jdk-gradle7.4.2-kotlinc1.7.0 + container: unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 steps: - name: Print environment variables @@ -46,11 +47,11 @@ jobs: run: | echo Find your Prometheus metrics using label {instance=\"${GITHUB_RUN_ID}-${HOSTNAME}\"} chmod +x ./scripts/project/monitoring.sh - ./scripts/project/monitoring.sh ${{ secrets.PUSHGATEWAY_HOSTNAME }} ${{ secrets.PUSHGATEWAY_USER }} ${{ secrets.PUSHGATEWAY_PASSWORD }} + ./scripts/project/monitoring.sh ${PUSHGATEWAY_HOSTNAME} ${{ secrets.PUSHGATEWAY_USER }} ${{ secrets.PUSHGATEWAY_PASSWORD }} - name: Run chosen tests run: | - gradle :${{ github.event.inputs.project-name }}:test --no-daemon --tests ${{ github.event.inputs.tests-bunch-name }} + gradle :${{ github.event.inputs.project-name }}:test -PprojectType=Ultimate --no-daemon --build-cache --no-parallel -PgithubActor=${{ github.actor }} -PgithubToken=${{ secrets.PACKAGES_RO_TOKEN }} -Dorg.gradle.jvmargs=-Xmx2g -Dkotlin.daemon.jvm.options=-Xmx4g --tests ${{ github.event.inputs.tests-bunch-name }} - name: Upload ${{ github.event.inputs.project-name }} tests report if tests have failed if: ${{ failure() }} @@ -66,7 +67,7 @@ jobs: name: generated-tests path: | /tmp/UTBot/generated*/* - /tmp/UTBot/utbot-childprocess-errors/* + /tmp/UTBot/utbot-instrumentedprocess-errors/* - name: Upload utbot-framework logs if: ${{ always() && github.event.inputs.project-name == 'utbot-framework' }} uses: actions/upload-artifact@v3 diff --git a/.gitignore b/.gitignore index 7db9331a69..5b11d761cb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ target/ .idea/ .gradle/ *.log +*.rdgen +utbot-intellij/src/main/resources/settings.properties +__pycache__ +.dmypy.json diff --git a/.run/Debug All.run.xml b/.run/Debug All.run.xml new file mode 100644 index 0000000000..a0cf8bd64d --- /dev/null +++ b/.run/Debug All.run.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.run/Debug Engine Process.run.xml b/.run/Debug Engine Process.run.xml new file mode 100644 index 0000000000..6a2c301a76 --- /dev/null +++ b/.run/Debug Engine Process.run.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.run/Debug Instrumented Process.run.xml b/.run/Debug Instrumented Process.run.xml new file mode 100644 index 0000000000..b1c4187629 --- /dev/null +++ b/.run/Debug Instrumented Process.run.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.run/Debug Spring Analyzer Process.run.xml b/.run/Debug Spring Analyzer Process.run.xml new file mode 100644 index 0000000000..91c80ebf73 --- /dev/null +++ b/.run/Debug Spring Analyzer Process.run.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.run/Listen for Engine Process.run.xml b/.run/Listen for Engine Process.run.xml new file mode 100644 index 0000000000..292a84b296 --- /dev/null +++ b/.run/Listen for Engine Process.run.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.run/Listen for Instrumented Process.run.xml b/.run/Listen for Instrumented Process.run.xml new file mode 100644 index 0000000000..d3ef5650cf --- /dev/null +++ b/.run/Listen for Instrumented Process.run.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.run/Listen for Spring Analyzer Process.run.xml b/.run/Listen for Spring Analyzer Process.run.xml new file mode 100644 index 0000000000..7b9505048f --- /dev/null +++ b/.run/Listen for Spring Analyzer Process.run.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.run/Run IDEA.run.xml b/.run/Run IDEA.run.xml new file mode 100644 index 0000000000..4c6116ceb2 --- /dev/null +++ b/.run/Run IDEA.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/.run/Run Rider.run.xml b/.run/Run Rider.run.xml new file mode 100644 index 0000000000..2ab584d596 --- /dev/null +++ b/.run/Run Rider.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f60e4a0eb0..9ef2a4bb5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,28 +17,28 @@ We welcome absolutely everyone. With one big and kind request: please follow the There are so many ways to contribute. Choose yours and find the relevant short guide below. -|(1) Choose what you like and check the guideline:| (2) Contribute: | -|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -|Reporting a bug| Create a [bug reporting issue](https://github.com/UnitTestBot/UTBotJava/issues/new?assignees=&labels=&template=bug_report.md&title=) | -|Suggesting a feature| Create a [feature suggestion issue](https://github.com/UnitTestBot/UTBotJava/issues/new?assignees=&labels=&template=feature_request.md&title=) | -|Contributing the code (bug fix or feature implementation)| Create a pull request | -|Reproducing a reported bug| Comment on the issue | -|Testing a pull request| Comment on the pull request | -|Improving documentation| Create an issue
Create a pull request
Comment on issues and PRs | -|Sharing ideas| Start the [Discussion](https://github.com/UnitTestBot/UTBotJava/discussions) or join the existing one | +| (1) Choose what you like and check the guideline: | (2) Contribute: | +|-------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| [Reporting a bug](#Reporting-a-bug) | Create a [bug reporting issue](https://github.com/UnitTestBot/UTBotJava/issues/new?assignees=&labels=&template=bug_report.md&title=) | +| [Requesting a feature](#Requesting-a-feature) | Create a [feature suggestion issue](https://github.com/UnitTestBot/UTBotJava/issues/new?assignees=&labels=&template=feature_request.md&title=) | +| [Contributing the code (bug fix or feature implementation)](#Contributing-the-code-(bug-fix-or-feature-implementation)) | Create a pull request | +| [Reproducing a reported bug](#Reproducing-a-reported-bug) | Comment on the issue | +| [Testing a pull request](#Testing-a-pull-request) | Comment on the pull request | +| [Improving documentation](#Improving-documentation) | Create an issue
Create a pull request
Comment on issues and PRs | +| [Sharing ideas](#Sharing-ideas) | Start the [Discussion](https://github.com/UnitTestBot/UTBotJava/discussions) or join the existing one | # How to submit a contribution ## Reporting a bug 1. Check if the bug (a true bug!) has already been reported: search through [UnitTestBot issues](https://github.com/UnitTestBot/UTBotJava/issues). Please, don't duplicate. -2. Check with the [Naming and labeling conventions](Conventions.md). +2. Check with the [Naming and labeling conventions](docs/contributing/Conventions.md). 3. Make sure you have all the necessary information as per [template](https://github.com/UnitTestBot/UTBotJava/issues/new?assignees=&labels=&template=bug_report.md&title=) and create the bug reporting issue. ## Requesting a feature 1. Check if the feature has already been requested: search through [UnitTestBot issues](https://github.com/UnitTestBot/UTBotJava/issues). -2. Check with our [Naming and labeling conventions](Conventions.md). +2. Check with our [Naming and labeling conventions](docs/contributing/Conventions.md). 3. Make sure you are able to provide all the necessary information as per [template](https://github.com/UnitTestBot/UTBotJava/issues/new?assignees=&labels=&template=feature_request.md&title=) and create the feature request issue. ## Contributing the code (bug fix or feature implementation) @@ -63,10 +63,10 @@ Do you need to create an issue if you want to fix a bug? style** and **steps for building the project**. 5. Test your code: - * Before creating the pull request perform the tests you find necessary for your code changes. + * Please, provide regression or integration tests for your code changes. If you don't do that, the reviewer can and highly likely **_will reject_** the PR. It is the contributor's responsibility to provide such tests or to reason why they are missing. * When implementing something new, it's great to find real users and ask them to try out your feature — to prove the necessity and quality of your suggestion. -6. Check with the [Naming and labeling conventions](Conventions.md). +6. Check with the [Naming and labeling conventions](docs/contributing/Conventions.md). 7. Create the pull request, and you'll see if the automated tests pass on GitHub. Your reviewer will possibly recommend you more tests. @@ -87,7 +87,7 @@ is the same as for the code. If you'd like to improve the existing docs or to ad 2. Create your own fork of the code. 3. Clone the forked repository to your local machine. 4. Implement changes to docs (change the existing doc or create a new one). Usually, we create a new doc for a new feature, not for the small fixes. You are not obliged to write a detailed text about the feature you implement. You have to only describe it properly in both the related issue and the pull request, but it will be great if you still provide some docs. -6. Check with the [Naming and labeling conventions](Conventions.md). +6. Check with the [Naming and labeling conventions](docs/contributing/Conventions.md). 7. Create the pull request, and we'll review it. * You can request a new doc — create an issue, using the [guide for a feature request](#Requesting-a-feature). @@ -114,4 +114,4 @@ We do our best in reviewing, but we can hardly specify the exact timeout for it. By contributing, you agree that your contributions will be licensed under the [Apache License 2.0](https://github.com/UnitTestBot/UTBotJava/blob/main/LICENSE). -Feel free to [contact us directly](https://www.utbot.org/contact/) if that's a concern. \ No newline at end of file +Feel free to [contact us directly](https://www.utbot.org/about) if that's a concern. \ No newline at end of file diff --git a/COPYRIGHT_HEADER.txt b/COPYRIGHT_HEADER.txt new file mode 100644 index 0000000000..d405aac34e --- /dev/null +++ b/COPYRIGHT_HEADER.txt @@ -0,0 +1,15 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + * + * 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. + */ \ No newline at end of file diff --git a/DEVNOTE.md b/DEVNOTE.md index a509f5901c..a47be23490 100644 --- a/DEVNOTE.md +++ b/DEVNOTE.md @@ -4,7 +4,7 @@ When you have the forked repository on your local machine, you are almost ready to build your own version of UnitTestBot. -💡 Before you start coding, please check the [system requirements](SystemRequirements.md) and find instructions on +💡 Before you start coding, please check the [system requirements](https://github.com/UnitTestBot/UTBotJava/wiki/Check-system-requirements) and find instructions on configuring development environment. 💡 Get to know the [code style](https://github.com/saveourtool/diktat/blob/master/info/guide/diktat-coding-convention.md) we are used to. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000..15230c55f1 --- /dev/null +++ b/NOTICE @@ -0,0 +1,2 @@ +UnitTestBot +Copyright 2022 UnitTestBot contributors (utbot.org) \ No newline at end of file diff --git a/README.md b/README.md index a7ee463b78..405c00028b 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ 👉 Visit the [official UnitTestBot website](https://www.utbot.org/). # What is UnitTestBot? + UnitTestBot is the tool for **automated unit test generation** and **precise code analysis**. It produces ready-to-use test cases for @@ -17,7 +18,8 @@ The **symbolic execution engine** paired with a **smart fuzzing technique** cons UnitTestBot represents all the test summaries in a **human-readable format**. The intelligible test method names and comments help you to control the whole testing process. Test failed? The summary refers you to the related branch or the condition under test. # Get started -Try the **[online demo](https://www.utbot.org/utbot/)** to generate unit tests with one click. + +Try the **[online demo](https://www.utbot.org/demo)** to generate unit tests with one click. Get to know the **full version** of UnitTestBot plugin with this quick guide: @@ -25,13 +27,15 @@ Get to know the **full version** of UnitTestBot plugin with this quick guide: Install UnitTestBot plugin for IntelliJ IDEA Try the most straightforward path to start using UnitTestBot plugin. -1. Please check the [system requirements](SystemRequirements.md). +1. Please check the [system requirements](https://github.com/UnitTestBot/UTBotJava/wiki/Check-system-requirements). 2. Open your IntelliJ IDEA. -3. Go to **File → Settings... → Plugins → Marketplace**. +3. Go to **File > Settings... > Plugins > Marketplace**. 4. In the search field type *UnitTestBot* — you'll see the UnitTestBot plugin page. 5. Press the **Install** button and wait until it changes to **Installed**, then click **OK**. -Now you can find the UnitTestBot plugin enabled in the **File → Settings → Plugins** window. +Now you can find the UnitTestBot plugin enabled in the **File > Settings > Plugins** window. + +Do you want to manually choose the build or to update the plugin? Please refer to [Install or update plugin](https://github.com/UnitTestBot/UTBotJava/wiki/Install-or-update-plugin) in our user guide. ____________ @@ -42,11 +46,15 @@ ____________ Proceed to generating unit tests for the existing Java project. If you don't have one, create it using the [JetBrains tutorial](https://www.jetbrains.com/help/idea/creating-and-running-your-first-java-application.html). 1. Open your Java project in IntelliJ IDEA. -2. Right-click the required package or a file in the Project tool window, scroll the menu down to the bottom and choose **Create Tests with UTBot...** -3. In the **Generate tests with UTBot** window tick the classes or methods you'd like to cover with unit tests and press **OK**. +2. Right-click the required package or a file in the Project tool window, scroll the menu down to the bottom and + choose **Generate Tests with UnitTestBot...** +3. In the **Generate Tests with UnitTestBot** window tick the classes or methods you'd like to cover with unit tests and + press **Generate Tests** or **Generate and Run**. Now you can see the resulting test class or classes in the Editor tool window. +Need to configure testing framework, mocking strategy or parameterization? Please check all [configuration options](https://github.com/UnitTestBot/UTBotJava/wiki/Fine-tune-test-generation). + ____________ @@ -73,12 +81,15 @@ What can you do with the output? 3. To *view coverage*: -Right-click the test class, choose **More Run/Debug → Run ... with Coverage**. +Right-click the test class, choose **More Run/Debug > Run ... with Coverage**. + +Want to know more about test descriptions or SARIF reports? Please learn how to [Get use of test results](https://github.com/UnitTestBot/UTBotJava/wiki/Get-use-of-test-results). ____________ # Contribute to UnitTestBot + UnitTestBot is an open source project. We welcome everyone who wants to make UnitTestBot better — introduce a new feature or report a bug. We have only one kind request for our contributors: we expect you to prove the necessity and quality of the suggested changes. How can you do this? Refer to our [Contributing guide](https://github.com/UnitTestBot/UTBotJava/blob/main/CONTRIBUTING.md). @@ -88,9 +99,11 @@ Feel free to join the [Discussions](https://github.com/UnitTestBot/UTBotJava/dis And thank you for your time and effort! ⭐ # Find support -Having troubles with using UnitTestBot? Contact us [directly](https://www.utbot.org/contact). + +Having troubles with using UnitTestBot? Contact us [directly](https://www.utbot.org/about). # Find more UnitTestBot products + You can also try [UnitTestBot](https://github.com/UnitTestBot/UTBotCpp) developed especially for C/C++. You are welcome to [contribute](https://github.com/UnitTestBot/UTBotCpp/blob/main/CONTRIBUTING.md) to it too! diff --git a/SystemRequirements.md b/SystemRequirements.md deleted file mode 100644 index 2464bb53bd..0000000000 --- a/SystemRequirements.md +++ /dev/null @@ -1,31 +0,0 @@ -# System requirements - ---- - -### To generate tests with UnitTestBot: - -you have to install IntelliJ IDEA (versions from 2022.1 to 2022.1.4 are supported). - -### To contribute to UnitTestBot: - -you have to install - -- IntelliJ IDEA (versions from 2022.1 to 2022.1.4 are supported); - -- JDK 11; - -- Kotlin 1.7.0 or later. - -Please check your development environment: - -- ```JAVA_HOME``` environment variable should contain the path to JDK 11 installation directory; - -- ```PATH``` environment variable should contain the path to the ```bin``` folder of JDK 11 installation directory; - -- ```KOTLIN_HOME``` environment variable should contain the path to the ```kotlinc``` folder of Kotlin (1.7.0 or later) installation - directory; -- Project SDK (1) and Gradle SDK (2) should be set to JDK 11: -
(1) **IntelliJ IDEA → File → Project Structure → Project Settings → Project → SDK**, -
(2) **IntelliJ IDEA → File → Settings → Build, Execution, Deployment → Build Tools → Gradle**. - -Please note: if the environment variables lead to unsupported JDK or Kotlin versions, you won't be able to build the UnitTestBot project. diff --git a/build.gradle.kts b/build.gradle.kts index 88722424cb..823b6c22ba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ import java.text.SimpleDateFormat -import org.gradle.api.JavaVersion.VERSION_11 +import org.gradle.api.JavaVersion.* import org.jetbrains.kotlin.gradle.tasks.KotlinCompile group = "org.utbot" @@ -15,15 +15,10 @@ version = semVer ?: "$dateBasedVersion-SNAPSHOT" plugins { `java-library` - kotlin("jvm") version "1.7.10" + kotlin("jvm") version "1.8.0" `maven-publish` } -configure { - sourceCompatibility = VERSION_11 - targetCompatibility = VERSION_11 -} - allprojects { apply { @@ -41,23 +36,83 @@ allprojects { withType { kotlinOptions { jvmTarget = "1.8" - freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class", "-Xcontext-receivers") allWarningsAsErrors = false } } compileTestKotlin { kotlinOptions { jvmTarget = "1.8" - freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class", "-Xcontext-receivers") allWarningsAsErrors = false } } withType { + // uncomment if you want to see loggers output in console + // this is useful if you debug in docker + // testLogging.showStandardStreams = true + // testLogging.showStackTraces = true + // set heap size for the test JVM(s) minHeapSize = "128m" maxHeapSize = "3072m" - - jvmArgs = listOf("-XX:MaxHeapSize=3072m") + jvmArgs = listOf( + "-XX:MaxHeapSize=3072m", + "--add-opens", "java.base/java.util.concurrent.atomic=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.invoke=ALL-UNNAMED", + "--add-opens", "java.base/java.util.concurrent=ALL-UNNAMED", + "--add-opens", "java.base/java.util.concurrent.locks=ALL-UNNAMED", + "--add-opens", "java.base/java.text=ALL-UNNAMED", + "--add-opens", "java.base/java.time=ALL-UNNAMED", + "--add-opens", "java.base/java.io=ALL-UNNAMED", + "--add-opens", "java.base/java.nio=ALL-UNNAMED", + "--add-opens", "java.base/java.nio.file=ALL-UNNAMED", + "--add-opens", "java.base/java.net=ALL-UNNAMED", + "--add-opens", "java.base/sun.security.util=ALL-UNNAMED", + "--add-opens", "java.base/sun.reflect.generics.repository=ALL-UNNAMED", + "--add-opens", "java.base/sun.net.util=ALL-UNNAMED", + "--add-opens", "java.base/sun.net.fs=ALL-UNNAMED", + "--add-opens", "java.base/java.security=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.ref=ALL-UNNAMED", + "--add-opens", "java.base/java.math=ALL-UNNAMED", + "--add-opens", "java.base/java.util.stream=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED", + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens", "java.base/sun.security.provider=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.event=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.jimage=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.jimage.decompressor=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.jmod=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.jtrfs=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.loader=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.logger=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.math=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.module=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.commons=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.signature=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.tree=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.tree.analysis=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.util=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.xml.sax=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.xml.sax.helpers=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.perf=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.platform=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.ref=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.reflect=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.util=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.util.jar=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.util.xml=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.util.xml.impl=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.vm=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.vm.annotation=ALL-UNNAMED" + ) + + withType { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } useJUnitPlatform { excludeTags = setOf("slow", "IntegrationTest") @@ -68,6 +123,9 @@ allprojects { override fun beforeTest(testDescriptor: TestDescriptor) {} override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) { println("[$testDescriptor.classDisplayName] [$testDescriptor.displayName]: $result.resultType, length - ${(result.endTime - result.startTime) / 1000.0} sec") + if (result.resultType == TestResult.ResultType.FAILURE) { + println("Exception: " + result.exception?.stackTraceToString()) + } } override fun afterSuite(testDescriptor: TestDescriptor, result: TestResult) { @@ -82,6 +140,7 @@ allprojects { repositories { mavenCentral() maven("https://jitpack.io") + maven("https://s01.oss.sonatype.org/content/repositories/orgunittestbotsoot-1004/") maven("https://plugins.gradle.org/m2") maven("https://www.jetbrains.com/intellij-repository/releases") maven("https://cache-redirector.jetbrains.com/maven-central") @@ -131,7 +190,7 @@ configure( project(":utbot-core"), project(":utbot-framework"), project(":utbot-framework-api"), - project(":utbot-fuzzers"), + project(":utbot-java-fuzzing"), project(":utbot-instrumentation"), project(":utbot-rd"), project(":utbot-summary") diff --git a/buildSrc/src/main/java/SettingsTemplateHelper.java b/buildSrc/src/main/java/SettingsTemplateHelper.java new file mode 100644 index 0000000000..d55390eaac --- /dev/null +++ b/buildSrc/src/main/java/SettingsTemplateHelper.java @@ -0,0 +1,197 @@ +import org.gradle.api.*; + +import java.io.*; +import java.text.*; +import java.util.*; +import java.util.function.*; + +/** + * The purpose of this helper is to convert UtSettings.kt source code + * to template resource file settings.properties (top-level entry in plugin JAR file). + * There are two stages: parsing of input to build models and then rendering models to output file + */ + +public class SettingsTemplateHelper { + private static final String[] apacheLines = + ("Copyright (c) " + new SimpleDateFormat("yyyy").format(System.currentTimeMillis()) + " utbot.org\n" + + "\n" + + "Licensed under the Apache License, Version 2.0 (the \"License\");\n" + + "you may not use this file except in compliance with the License.\n" + + "You may obtain a copy of the License at\n" + + "\n" + + " http://www.apache.org/licenses/LICENSE-2.0\n" + + "\n" + + "Unless required by applicable law or agreed to in writing, software\n" + + "distributed under the License is distributed on an \"AS IS\" BASIS,\n" + + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" + + "See the License for the specific language governing permissions and\n" + + "limitations under the License.").split("\n"); + + public static void proceed(Project project) { + File settingsSourceDir = new File(project.getBuildDir().getParentFile().getParentFile(), "utbot-framework-api/src/main/kotlin/org/utbot/framework/"); + String sourceFileName = "UtSettings.kt"; + File settingsResourceDir = new File(project.getBuildDir().getParentFile().getParentFile(), "utbot-intellij-main/src/main/resources/"); + String settingsFileName = "settings.properties"; + + Map dictionary = new HashMap<>(); + dictionary.put("Int.MAX_VALUE", String.valueOf(Integer.MAX_VALUE)); + + List models = new ArrayList<>(); + List enums = new ArrayList<>(); + StringBuilder acc = new StringBuilder(); + List docAcc = new ArrayList<>(); + // Stage one: parsing sourcecode + try(BufferedReader reader = new BufferedReader(new FileReader(new File(settingsSourceDir, sourceFileName)))) { + for (String s = reader.readLine(); s != null; s = reader.readLine()) { + s = s.trim(); + if (s.startsWith("enum class ")) {//Enum class declaration + enums.add(new EnumInfo(s.substring(11, s.length() - 2))); + } else if (s.matches("[A-Z_]+,?") && !enums.isEmpty()) {//Enum value + String enumValue = s.substring(0, s.length()); + if (enumValue.endsWith(",")) enumValue = enumValue.substring(0, enumValue.length() - 1); + enums.get(enums.size() - 1).docMap.put(enumValue, new ArrayList<>(docAcc)); + } else if (s.startsWith("const val ")) {//Constand value to be substitute later if need + int pos = s.indexOf(" = "); + dictionary.put(s.substring(10, pos), s.substring(pos + 3)); + } else if (s.equals("/**")) {//Start of docuemntation block + docAcc.clear(); + } else if (s.startsWith("* ")) { + if (!s.contains("href")) {//Links are not supported, skip them + docAcc.add(s.substring(2)); + } + } else if (s.startsWith("var") || s.startsWith("val")) {//Restart accumulation + acc.delete(0, acc.length()); + acc.append(s); + } else if (s.isEmpty() && acc.length() > 0) {//Build model from accumulated lines + s = acc.toString(); + acc.delete(0, acc.length()); + if (s.startsWith("var") || s.startsWith("val")) { + int i = s.indexOf(" by ", 3); + if (i > 0) { + String key = s.substring(3, i).trim(); + if (key.contains(":")) { + key = key.substring(0, key.lastIndexOf(':')); + } + PropertyModel model = new PropertyModel(key); + models.add(model); + s = s.substring(i + 7); + i = s.indexOf("Property"); + if (i > 0) model.type = s.substring(0, i); + if (i == 0) { + i = s.indexOf('<', i); + if (i != -1) { + s = s.substring(i+1); + i = s.indexOf('>'); + if (i != -1) { + model.type = s.substring(0, i); + } + } + } + + i = s.indexOf('(', i); + if (i > 0) { + s = s.substring(i + 1); + String defaultValue = s.substring(0, s.indexOf(')')).trim(); + if (defaultValue.contains(",")) defaultValue = defaultValue.substring(0, defaultValue.indexOf(',')); + defaultValue = dictionary.getOrDefault(defaultValue, defaultValue); + if (defaultValue.matches("[\\d_]+L")) { + defaultValue = defaultValue.substring(0, defaultValue.length() - 1).replace("_", ""); + } + if (defaultValue.matches("^\".+\"$")) { + defaultValue = defaultValue.substring(1, defaultValue.length() - 1); + } + model.defaultValue = defaultValue; + model.docLines.addAll(docAcc); + } + } + } + } else if (acc.length() > 0) { + acc.append(" " + s); + } + } + } catch (IOException ioe) { + System.err.println("Unexpected error when processing " + sourceFileName); + ioe.printStackTrace(); + } + + // Stage two: properties file rendering + try (PrintWriter writer = new PrintWriter(new File(settingsResourceDir, settingsFileName))) { + for (String apacheLine : apacheLines) { + writer.println("# " + apacheLine); + } + for (PropertyModel model : models) { + if (model.type.equals("Enum")) { + String[] split = model.defaultValue.split("\\."); + if (split.length > 1) { + model.defaultValue = split[1]; + EnumInfo enumInfo = enums.stream().filter(new Predicate() { + @Override + public boolean test(EnumInfo enumInfo) { + return enumInfo.className.equals(split[0]); + } + }).findFirst().orElse(null); + if (enumInfo != null) { + model.docLines.add(""); + for (Map.Entry> entry : enumInfo.docMap.entrySet()) { + String enumValue = entry.getKey(); + if (entry.getValue().size() == 1) { + model.docLines.add(enumValue + ": " + entry.getValue().get(0)); + } else { + model.docLines.add(enumValue); + for (String line : entry.getValue()) { + model.docLines.add(line); + } + } + } + } + } + } + writer.println(); + writer.println("#"); + for (String docLine : model.docLines) { + if (docLine.isEmpty()) { + writer.println("#"); + } else { + writer.println("# " + docLine); + } + } + boolean defaultIsAlreadyMentioned = model.docLines.stream().anyMatch(new Predicate() { + @Override + public boolean test(String s) { + return s.toLowerCase(Locale.ROOT).contains("default"); + } + }); + if (!defaultIsAlreadyMentioned) { + writer.println("#"); + writer.println("# Default value is [" + model.defaultValue + "]"); + } + writer.println("#" + model.key + "=" + model.defaultValue); + } + writer.flush(); + writer.close(); + } catch (IOException ioe) { + System.err.println("Unexpected error when saving " + settingsFileName); + ioe.printStackTrace(); + } + } + + private static class PropertyModel { + final String key; + String type = ""; + String defaultValue = ""; + List docLines = new ArrayList<>(); + + PropertyModel(String key) { + this.key = key; + } + } + + private static class EnumInfo { + final String className; + Map> docMap = new LinkedHashMap<>(); + + public EnumInfo(String className) { + this.className = className; + } + } +} \ No newline at end of file diff --git a/docker/Dockerfile_java_cli b/docker/Dockerfile_java_cli deleted file mode 100644 index 552c436e3c..0000000000 --- a/docker/Dockerfile_java_cli +++ /dev/null @@ -1,12 +0,0 @@ -FROM azul/zulu-openjdk:11.0.15-11.56.19 - -ARG UTBOT_JAVA_CLI - -WORKDIR /usr/src/ - -# Install UTBot Java CLI - -COPY ${UTBOT_JAVA_CLI} . - -RUN UTBOT_JAVA_CLI_PATH="$(find /usr/src -type f -name 'utbot-cli*')" \ - && ln -s "${UTBOT_JAVA_CLI_PATH}" /usr/src/utbot-cli.jar diff --git a/docs/AndroidStudioSupport.md b/docs/AndroidStudioSupport.md index c051d53a62..6feabebca2 100644 --- a/docs/AndroidStudioSupport.md +++ b/docs/AndroidStudioSupport.md @@ -1,14 +1,22 @@ # Android Studio support -## Installing AS +## Installing Android Studio -> Install latest AS +> Install the latest version of Android Studio +> * ### Installing Lombok plugin -> Use the first advice from the following link +> Lombok plugin is not required to use UnitTest Bot. +> However, if this plugin is required for your own goals, do the following: > -> +> * go to https://plugins.jetbrains.com/plugin/6317-lombok/versions +> +> * download .zip with the latest version +> +> * unpack it to ~/android-studio/plugins (use your path to Android Studio) +> +> * restart IDE ## Prerequisites @@ -25,7 +33,7 @@ > > The reason for it is the Android Gradle Plugin, which requires Java 11 to build anything. -## Running in AS +## Running in Android Studio > For now, running Utbot is supported only for Kotlin libraries. You can > create one like this: diff --git a/docs/ChoosingLanguageSpecificIDE.md b/docs/ChoosingLanguageSpecificIDE.md new file mode 100644 index 0000000000..6512898ecc --- /dev/null +++ b/docs/ChoosingLanguageSpecificIDE.md @@ -0,0 +1,23 @@ +# Choosing language specific IDE + +Some language-specific modules depends on specific IntelliJ IDE: +* Python can work with IntelliJ Community, IntelliJ Ultimate, PyCharm Community, PyCharm Professional +* JavaScript can work with IntelliJ Ultimate, PyCharm Professional and WebStorm +* Java and Kotlin - IntelliJ Community and IntelliJ Ultimate + +You should select correct IDE in `gradle.properties` file: +``` +ideType= +ideVersion=<222.4167.29> +``` + +### IDE marking + +| Mark | Full name | Supported plugin | +|------|----------------------|----------------------------------------| +| IC | IntelliJ Community | JVM, Python, AndroidStudio | +| IU | IntelliJ Ultimate | JVM, Python, JavaScript, AndroidStudio | +| PC | PyCharm Community | Python | +| PY | PyCharm Professional | Python, JavaScript | + +[IntelliJ Platform Plugin SDK documentation](https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#tasks-runpluginverifier) \ No newline at end of file diff --git a/docs/CodeGenerationAndRendering.md b/docs/CodeGenerationAndRendering.md new file mode 100644 index 0000000000..eab4d56723 --- /dev/null +++ b/docs/CodeGenerationAndRendering.md @@ -0,0 +1,339 @@ +# Code generation and rendering + +Code generation and rendering are a part of the test generation process in UnitTestBot (find the overall picture in the +[UnitTestBot architecture +overview](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/OverallArchitecture.md)). +UnitTestBot gets the synthetic representation of generated test cases from the fuzzer or the symbolic engine. +This representation (or model) is implemented in the `UtExecution` class. + +The `codegen` module generates the real test code based on this `UtExecution` model and renders it in a +human-readable form in accordance with the requested configuration (considering programming language, testing +framework, mocking and parameterization options). + +The `codegen` module +- converts `UtExecution` test information into an Abstract Syntax Tree (AST) representation using `CodeGenerator`, +- renders this AST according to the requested programming language and other configurations using `renderer`. + +## Example + +Consider the following method under test: + +```java +package pack; + +public class Example { + + public int maxIfNotEquals(int a, int b) throws IllegalArgumentException { + if (a == b) throw new IllegalArgumentException("a == b"); + if (a > b) return a; else return b; + } +} +``` + +The standard UnitTestBot-generated tests for this method (without test summaries and clustering into regions) +look like this: + +```java +package pack; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public final class ExampleStandardTest { + + @Test + @DisplayName("maxIfNotEquals: a == b : False -> a > b") + public void testMaxIfNotEquals_AGreaterThanB() { + Example example = new Example(); + + int actual = example.maxIfNotEquals(1, 0); + + assertEquals(1, actual); + } + + @Test + @DisplayName("maxIfNotEquals: a == b -> ThrowIllegalArgumentException") + public void testMaxIfNotEquals_AEqualsB() { + Example example = new Example(); + + assertThrows(IllegalArgumentException.class, () -> example.maxIfNotEquals(-255, -255)); + } + + @Test + @DisplayName("maxIfNotEquals: a < 0, b > 0 -> return 1") + public void testMaxIfNotEqualsReturnsOne() { + Example example = new Example(); + + int actual = example.maxIfNotEquals(-1, 1); + + assertEquals(1, actual); + } +} +``` + +Here is an example of the parameterized tests for this method. We also implement the data provider method — the +argument source. + +```java +package pack; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +public final class ExampleParameterizedTest { + + @ParameterizedTest + @MethodSource("pack.ExampleTest#provideDataForMaxIfNotEquals") + public void parameterizedTestsForMaxIfNotEquals(Example example, int a, int b, Integer expectedResult, Class expectedError) { + try { + int actual = example.maxIfNotEquals(a, b); + + assertEquals(expectedResult, actual); + } catch (Throwable throwable) { + assertTrue(expectedError.isInstance(throwable)); + } + } + + public static ArrayList provideDataForMaxIfNotEquals() { + ArrayList argList = new ArrayList(); + + { + Example example = new Example(); + + Object[] testCaseObjects = new Object[5]; + testCaseObjects[0] = example; + testCaseObjects[1] = 1; + testCaseObjects[2] = 0; + testCaseObjects[3] = 1; + testCaseObjects[4] = null; + argList.add(arguments(testCaseObjects)); + } + { + Example example = new Example(); + + Object[] testCaseObjects = new Object[5]; + testCaseObjects[0] = example; + testCaseObjects[1] = -255; + testCaseObjects[2] = -128; + testCaseObjects[3] = -128; + testCaseObjects[4] = null; + argList.add(arguments(testCaseObjects)); + } + { + Example example = new Example(); + + Object[] testCaseObjects = new Object[5]; + testCaseObjects[0] = example; + testCaseObjects[1] = -255; + testCaseObjects[2] = -255; + testCaseObjects[3] = null; + testCaseObjects[4] = IllegalArgumentException.class; + argList.add(arguments(testCaseObjects)); + } + + return argList; + } +} +``` + +## Configurations + +UnitTestBot renders code in accordance with the chosen programming language, testing framework, +mocking and parameterization options. + +Supported languages for code generation are: +- Java +- Kotlin (experimental) — we have significant problems with the support for nullability and generics +- Python and JavaScript — in active development + +Supported testing frameworks are: +- JUnit 4 +- JUnit 5 +- TestNG (only for the projects with JDK 11 or later) + +Supported mocking options are: +- No mocking +- Mocking with Mockito framework +- Mocking static methods with Mockito + +Parameterized tests can be generated in Java only. Parameterization is not supported with the mocks enabled or +with JUnit 4 chosen as the testing framework. + +## Entry points + +The `codegen` module gets calls from various UnitTestBot components. The most common scenario is to call `codegen` +from integration tests as well as from the `utbot-intellij` project and its `CodeGenerationController` class. The +`utbot-online` and `utbot-cli` projects call `codegen` as well. + +The `codegen` entry points are: +- `CodeGenerator.generateAsString()` +- `CodeGenerator.generateAsStringWithTestReport()` + +The latter gets `UtExecution` information received from the symbolic engine or the fuzzer and converts it into the +`codegen`-related data units, each called `CgMethodTestSet`. As a result of further processing, the test code is +generated as a string with a test generation report (see [Reports](#Reports) for details). + +Previously, `CgMethodTestSet` has been considerably different from `UtMethodTestSet` as it has been using +`ExecutableId` instead of the legacy `UtMethod` (has been removed recently). +For now, `CgMethodTestSet` contains utility functions required for code generation, mostly for parameterized tests. + +## Abstract Syntax Tree (AST) + +The `codegen` module converts `UtExecution` information to the AST representation. +We create one AST per one source class (and one resulting test class). We use our own AST implementation. + +We generate a `UtUtils` class containing a set of utility functions, when they are necessary for a given test class. +If the `UtUtils` class has not been created previously, its AST representation is generated as well. To learn more +about the `UtUtils` class and how it is generated, refer to the +[design doc](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/UtUtilsClass.md). + +All the AST elements are `CgElement` inheritors. +`CgClassFile` is the top level element — it contains `CgClass` with the required imports. + +The class has the body (`CgClassBody`) as well as minor properties declared: documentation comments, annotations, +superclasses, interfaces, etc. + +The class body is a set of `CgRegion` elements, having the `header` and the corresponding `content`, which is mostly +the set of `CgMethod` elements. + +The further AST levels are created similarly. The AST leaves are `CgLiteral`, `CgVariable`, +`CgLogicalOr`, `CgEmptyLine`, etc. + +## Test method + +The below-mentioned functionality is implemented in `CgMethodConstructor`. + +To create a test method: +* store the initial values of the static fields and perform the seven steps for creating test method body mentioned later; +* if the static field values undergo changes, perform these seven steps in the `try` block and recover these values in the `finally` block accordingly. + +To create test method body: +1. substitute static fields with local variables +2. set up instrumentation (get mocking information from `UtExecution`) +3. create a variable for the current instance +4. create variables for method-under-test arguments +5. record an actual result by calling method under test +6. generate result assertions +7. for successful tests, generate field state assertions + +_Note:_ generating assertions has pitfalls. In primitive cases, like comparing two integers, we can use the standard +assertions of a selected test framework. To compare two objects of an arbitrary type, we need a +custom implementation of equality assertion, e.g. using `deepEquals()`. The `deepEquals()` method compares object +structures field by field. The method is recursive: if the current field is not of the primitive type, we call +`deepEquals()` for this field. The maximum recursion depth is limited. + +For the parameterized tests +- we do not support mocking, so we do not set up the initial environment; +- we do not generate field state assertions. + +`UtExecution` usually represents a single test scenario, and one `UtExecution` instance is used to create a single +test method. Parameterized tests are supposed to cover several test scenarios, so several `UtExecution` instances +are used for generating test methods. + +## Generic execution + +Parameterization often helps to reveal similarities between test scenarios. The combined outcome is sometimes more +expressive. To represent these similarities, we construct generic executions. + +Generic execution is a synthetic execution, formed by a group of real executions, that have +- the same type of result, +- the same modified static fields. + +Otherwise, we create several generic executions and several parameterized tests. The logic of splitting executions +into the test sets is implemented in the `CgMethodTestSet` class. + +From the group of `UtExecution` elements, we take the first successful execution with the non-nullable result. See +`CgMethodConstructor.chooseGenericExecution()` for more details. + +## Renderer + +We have a general approach for rendering the code of test classes. `UtUtils` class is rendered differently: we +hardcode the required method implementations for the specific code generation language. + +All the renderers implement `CgVisitor` interface. It has a separate `visit()` method for each supported +`CgElement` item. + +There are three renderers: +- `CgAbstractRenderer` for elements that are similar in Kotlin and Java +- `CgJavaRenderer` for Java-specific elements +- `CgKotlinRenderer` for Kotlin-specific elements + +Each renderer method visits the current `CgElement`. It means that all the required instructions are printed properly. +If an element contains the other element, the first one delegates rendering to its _child_. + +`CgVisitor` refers us to the _Visitor_ design pattern. Delegating means asking the _child_ element to +accept the renderer and manage it. Then we go through the list of `CgElement` types to find the first +matching `visit()` method. + +_Note:_ the order of `CgElement` items listed in `CgElement.accept()` is important. + +Rendering may be terminated if the generated code is too long (e.g. due to test generation bugs). + +## Reports + +While constructing the test class, we create test generation reports. It contains basic statistical information: the +number of generated tests, the number of successful tests, etc. It also may contain information on potential problems +like trying to use mocks when mocking framework is not installed. + +The report is an HTML string with clickable links. + +_Note:_ no test generation reports are created for parameterized tests. + +## Services + +Services help the `codegen` module to produce human-readable test code. + +### Name generator + +With this service, we create names for variables and methods. It assures avoiding duplicates in names, +resolving conflicts with keywords, etc. It also adds suffixes if we process a mock or a static item. + +Name generator is called directly from `CgStatementConstructor`. + +_Note:_ if you need a new variable, you should better use this service (e.g. the `newVar()` method) instead of calling +the `CgVariable` constructor manually. + +### Framework and language services + +Framework services help the `codegen` module to generate constructions (e.g. assertions) according to a given +testing or mocking framework. +Language services provide the `codegen` module with language-specific information on test class extensions, +prohibited keywords, etc. + +See the `Domain` file for more framework- and language-specific implementations. + +### CgFieldStateManager + +`CgFieldStateManager` stores the initial and the final environment states for the given method under test. +These states are used for generating assertions. Usually, the environment state consists of three parts: +* current instance state, +* argument state, +* static field state. + +All the state-related variables are marked as `INITIAL` or `FINAL`. + +### CgCallableAccessManager + +This service helps to validate access. For example, if the current argument +list is valid for the method under test, `CgCallableAccessManager` checks if one can call this method with these +arguments without using _Reflection_. + +`CgCallableAccessManager` analyzes callables as well as fields for accessibility. + +## CgContext + +`CgContext` contains context information for code generation. The `codegen` module uses one +context per one test class. `CgContext` also stores information about the scope for the inner context elements: e.g. when +they should be instantiated and cleared. For example, the context of the nested class is the part of the owner class context. + +`CgContext` is the so-called "God object" and should be split into independent storages and +helpers. This task seems to be difficult and is postponed for now. \ No newline at end of file diff --git a/docs/Fuzzing Platform.md b/docs/Fuzzing Platform.md new file mode 100644 index 0000000000..37b2314faa --- /dev/null +++ b/docs/Fuzzing Platform.md @@ -0,0 +1,246 @@ +# Fuzzing Platform (FP) Design + +**Problem:** fuzzing is a versatile technique for generating values to be used as method arguments. Normally, +to generate values, one needs information on a method signature, or rather on the parameter types (if a fuzzer is +able to "understand" them). +The _white-box_ approach also requires AST, and the _grey-box_ approach needs coverage +information. To generate values that may serve as method arguments, the fuzzer uses generators, mutators, and +predefined values. + +* _Generators_ yield concrete objects created by descriptions. The basic description for creating objects is _type_. + Constants, regular expressions, and other structured object specifications (e.g. in HTML) may also be used as + descriptions. + +* _Mutators_ modify the object in accordance with some logic that usually means random changes. To get better + results, mutators obtain feedback (information on coverage and the inner state of the + program) during method call. + +* _Predefined values_ work well for known problems, e.g. incorrect symbol sequences. To discover potential problems, one can analyze parameter names as well as the specific constructs or method calls inside the method body. + +The general API for using the fuzzer looks like this: + +``` +fuzz( + params = "number", "string", "object: number, string", + seedGenerator = (type: Type) -> seeds + details: (constants, providers, etc) +).forEveryGeneratedValues { values: List -> + feedback = exec(values); + return feedback +} +``` + +The fuzzer gets the list of types, +which can be provided in different formats: as a string, an object, or a Class<*> in Java. +The seed generator accepts these types and produces seeds. +The seeds are base objects for value generation and mutations. + +It is the fuzzer, which is responsible for choosing, combining and mutating values from the seed set. +The fuzzer API should not provide access to the inner fuzzing logic. +Only general configuration is available. + +## Parameters + +The general fuzzing process gets the list of parameter descriptions as input and returns the corresponding list of values. The simplest description is the specific object type, for example: + +```kotlin +[Int, Bool] +``` + +In this particular case, the fuzzing process can generate the set of all the pairs having integer as the first value +and `true` or `false` as the second one. +If values `-3, 0, 10` are generated to be the `Int` values, the set of all the possible combinations has six items: +`(-3, false), (0, false), (10, false), (-3, true), (0, true), (10, true)`. +Depending on the programming language, +one may use interface descriptions or annotations (type hints) instead of defining the specific type. +Fuzzing platform (FP) is not able to create the concrete objects as it does not deal with the specific languages. +It can still convert the descriptions to the known constructs it can work with. + +Say, in most of the programming languages, any integer may be represented as a bit array, and the fuzzer can construct and +modify bit arrays. So, in the general case, the boundary values for the integer are these bit arrays: + +* [0, 0, 0, ..., 0] — zero +* [1, 0, 0, ..., 0] — minimum value +* [0, 1, 1, ..., 1] — maximum value +* [0, 0, ..., 0, 1] — plus 1 +* [1, 1, 1, ..., 1] — minus 1 + +One can correctly use this representation for unsigned integers as well: + +* [0, 0, 0, ..., 0] — zero (minimum value) +* [1, 0, 0, ..., 0] — maximum value / 2 +* [0, 1, 1, ..., 1] — maximum value / 2 + 1 +* [0, 0, ..., 0, 1] — plus 1 +* [1, 1, 1, ..., 1] — maximum value + +Thus, FP interprets the _Byte_ and _Unsigned Byte_ descriptions in different ways: in the former case, +the maximum value is [0, 1, 1, 1, 1, 1, 1, 1], while in the latter case it is [1, 1, 1, 1, 1, 1, 1, 1]. +FP types are described in detail further. + +## Refined parameter description + +During the fuzzing process, some parameters get the refined description, for example: + +``` +public boolean isNaN(Number n) { + if (!(n instanceof Double)) { + return false; + } + return Double.isNaN((Double) n); +} +``` + +In the above example, let the parameter be `Integer`. Considering the feedback, the fuzzer suggests that nothing but `Double` might increase coverage, so the type may be downcasted to `Double`. This allows for filtering out a priori unfitting values. + +## Statically and dynamically generated values +Predefined, or _statically_ generated, values help to define the initial range of values, which could be used as method arguments. + +These values allow us to: +* check if it is possible to call the given method with at least some set of values as arguments; +* gather statistics on executing the program; +* refine the parameter description. + +_Dynamic_ values are generated in two ways: +* internally, via mutating the existing values, successfully performed as method arguments (i.e. seeds); +* externally, via obtaining feedback that can return not only the statistics on the execution (the paths explored, + the time spent, etc.) but also the set of new values to be blended with the values already in use. + +Dynamic values should have a higher priority for a sample; +that is why they should be chosen either first or at least more likely than the statically generated ones. +In general, the algorithm that guides the fuzzing process looks like this: + +``` +# dynamic values are stored with respect to their return priority +dynamic_values = empty_priority_queue() +# static values are generated beforehand +static_values = generate() +# "good" values +seeded_values = [] +# +filters = [] + +# the loop runs until coverage reaches 100% +while app.should_fuzz(seeded_values.feedbacks): + # first we choose all dynamic values + # if there are no dynamic values, choose the static ones + value = dynamic_values.take() ?: static_values.take_random() + # if there is no value or it was filtered out (static values are generated in advance — they can be random and unfitting), try to generate new values via mutating the seeds + if value is null or filters.is_filtered(value): + value = mutate(seeded_values.random_value()) + # if there is still no value at this point, it means that there are no appropriate values at all, and the process stops + if value is null: break + + # run with the given values and obtain feedback + feedback = yield_run(value) + # feedback says if it is reasonable to add the current value to the set of seeds + if feedback is good: + seeded_values[feedback] += value + # feedback may also provide fuzzer with the new values + if feedback has suggested_value: + dynamic_values += feedback.suggested_values() with high_priority + + # mutate the static value thus allowing fuzzer to alternate static and dynamic values + if value.is_static_generated: + dynamic_values += mutate(seeded_values.random_value()) with low_priority +``` + +## Helping fuzzer via code modification + +Sometimes it is reasonable to modify the source code so that it makes applying fuzzer to it easier. This is one of possible approaches: to split the complex _if_-statement into the sequence of simpler _if_-statements. See [Circumventing Fuzzing Roadblocks with Compiler Transformations](https://lafintel.wordpress.com/2016/08/15/circumventing-fuzzing-roadblocks-with-compiler-transformations/) for details. + +## Generators + +There are two types of generators: +* yielding values of primitive data types: integers, strings, booleans +* yielding values of recursive data types: objects, lists + +Sometimes it is necessary not only to create an object but to modify it as well. We can apply fuzzing to +the fuzzer-generated values that should be modified. For example, you have the `HashMap.java` class, and you need to +generate +three +modifications for it using `put(key, value)`. For this purpose, you may request for applying the fuzzer to six +parameters `(key, value, key, value, key, value)` and get the necessary modified values. + +Primitive type generators allow for yielding: +1. Signed integers of a given size (8, 16, 32, and 64 bits, usually) +2. Unsigned integers of a given size +3. Floating-point numbers with a given size of significand and exponent according to IEEE 754 +4. Booleans: _True_ and _False_ +5. Characters (in UTF-16 format) +6. Strings (consisting of UTF-16 characters) + +The fuzzer should be able to provide out-of-the-box support for these types — be able to create, modify, and process +them. +To work with multiple languages, it is enough to specify the possible type size and to describe and create +concrete objects based on the FP-generated values. + +The recursive types include two categories: +* Collections (arrays and lists) +* Objects + +Collections may be nested and have _n_ dimensions (one, two, three, or more). + +Collections may be: +* of a fixed size (e.g., arrays) +* of a variable size (e.g., lists and dictionaries) + +Objects may have: +1. Constructors with parameters +2. Modifiable inner fields +3. Modifiable global values (the static ones) +4. Calls for modifying methods + +FP should be able to create and describe such objects in the form of a tree. The semantics of actual modifications is under the responsibility of a programming language. + + +## Typing + +FP does not use the concept of _type_ for creating objects. Instead, FP introduces the _task_ concept — it +encapsulates the description of a type, which should be used to create an object. Generally, this task consists of two +blocks: the task for initializing values and the list of tasks for modifying the initialized value. + +``` +Task = [ + Initialization: [T1, T2, T3, ..., TN] + Modification(Initialization): [ + М1: [T1, T2, ..., TK], + М2: [T1, T2, ..., TJ], + МH: [T1, T2, ..., TI], + ] +] +``` + +Thus, we can group the tasks as follows: + +``` +1. Trivial task = [ + Initialization: [INT|UNSIGNED.INT|FLOAT.POINT.NUMBER|BOOLEAN|CHAR|STRING] + Modification(Initialization): [] +] + + +2. Task for creating an array = [ + Initialization: [UNSIGNED.INT] + Modification(UNSIGNED.INT) = [T] * UNSIGNED.INT +] + +or + +2. Task for creating an array = [ + Initialization: [UNSIGNED.INT] + Modification(UNSIGNED.INT) = [[T * UNSIGNED.INT]] +] + +where "*" means repeating the type the specified number of times + +3. Task for creating an object = [ + Initialization: [Т1, Т2, ... ТN], + Modification(UNSIGNED.INT) = [ + ... + ] +] + +``` + +Therefore, each programming language defines how to interpret a certain type and how to infer it. This allows fuzzer +to store and mutate complex objects without any additional support from the language. diff --git a/docs/GoSupport.md b/docs/GoSupport.md new file mode 100644 index 0000000000..10518a49a0 --- /dev/null +++ b/docs/GoSupport.md @@ -0,0 +1,145 @@ +# UnitTestBot Go + +UnitTestBot Go automatically generates ready-to-use unit tests for Go programs. + +With UnitTestBot Go, you can find bugs in your code and fixate the desired program behavior with less effort. + +The project is under development, +so feel free to [contribute](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-go/docs/DEVELOPER_GUIDE.md). + +## Features and details + +UnitTestBot Go now implements the _basic fuzzing technique_. +It generates input values considering the parameter types, +inserts these values into the user functions, and executes the resulting test cases. + +### Supported types for function parameters + +Now UnitTestBot Go can generate values for _primitive types_, _arrays_, _slices_, _maps_, _structs_, _channels_, _interfaces_, and _pointers_. + +For _floating point types_, UnitTestBot Go supports working with _infinity_ and _NaN_. + +### Supported types for the returned results + +For the returned function results, +UnitTestBot Go supports the `error` type as well as all the types supported for the function parameters except _interfaces_ and _channels_. + +It also captures `panic` cases correctly. + +Check the examples of [supported functions](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-go/go-samples/simple/samples.go). + +### Keeping tests near the source code + +[Testing code typically +lives in the same package as the code it tests](https://gobyexample.com/testing). +By default, UnitTestBot Go generates tests into the `[name of source file]_go_ut_test.go` file located in the same +directory and Go package as the corresponding source file. + +If you need to change the location for the generated tests, +use the `generateGo` CLI command and set the generated test output mode to +`StdOut` (`-p, --print-test` flag). +Then, with bash primitives, redirect the output to an arbitrary file. + +### Requirements to source code + +To simplify handling dependencies and generating tests, UnitTestBot Go requires the code under test _to +be a part of a Go project_ consisting of a module and packages. + +To create a simple project, refer to the [starter tutorial](https://go.dev/doc/tutorial/getting-started) if necessary. + +For larger projects, try the [Create a Go module](https://go.dev/doc/tutorial/create-module) +and [Call your code from another module](https://go.dev/doc/tutorial/call-module-code) tutorials. + +To create a new module rooted in the current directory, use the `go mod init` command. + +To add missing module requirements necessary to build the current module’s packages and dependencies, +use the `go mod tidy` command. For editing and formatting `go.mod` files, use the `go mod edit` command. + +In the future, we plan to make UnitTestBot Go working with arbitrary code as input and generate the simplest +Go projects automatically. + +## IntelliJ IDEA plugin + +### Requirements + +* IntelliJ IDEA Ultimate — for compatibility, see [UnitTestBot on JetBrains Marketplace](https://plugins.jetbrains.com/plugin/19445-unittestbot/versions). +* Go SDK 1.18 or later +* Compatible [Go plugin](https://plugins.jetbrains.com/plugin/9568-go) for IntelliJ IDEA +* Properly configured `go.mod` file for the code under test +* `github.com/stretchr/testify/assert` Go module installed (IntelliJ IDEA automatically offers to install it as soon as the tests are generated) + +### Installation + +Please refer to [UnitTestBot user guide](https://github.com/UnitTestBot/UTBotJava/wiki/Install-or-update-plugin). + +### Usage + +1. In your IntelliJ IDEA, go to **File** > **Settings** > **Tools**, choose **UnitTestBot** and enable **Experimental languages support**. + + **(!) NOTE:** be sure to enable this option for **_each_** project. + +2. Open a `.go` file and press **Alt+Shift+U**. +3. In the **Generate Tests with UnitTestBot** window, you can configure the settings. + +## Command-line interface (CLI) + +### Requirements + +* Java SDK 17 or later +* Go SDK 1.18 or later +* Properly configured `go.mod` file for the code under test +* GCC and `github.com/stretchr/testify/assert` installed + +### Installation + +To install the UnitTestBot Go CLI application, go to GitHub, scroll through the release notes and click **Assets**. +Download the zip-archive named like **utbot-cli-VERSION**. +Extract the JAR file from the archive. + +### Usage + +Run the extracted JAR file using a command line: `generateGo` and `runGo` actions are supported for now. +To find info about the options for these actions, +insert the necessary JAR file name instead of `utbot-cli-2022.8-beta.jar` in the example and run the following commands: + +```bash +java -jar utbot-cli-2022.8-beta.jar generateGo --help +``` +or +```bash +java -jar utbot-cli-2022.8-beta.jar runGo --help +``` + +`generateGo` options: + +* `-s, --source TEXT`, _required_: specifies a Go source file to generate tests for. +* `-f, --function TEXT`: specifies a function name to generate tests for. Can be used multiple times to select multiple functions. +* `-m, --method TEXT`: specifies a method name to generate tests for. Can be used multiple times to select multiple methods. +* `-go TEXT`, _required_: specifies a path to a Go executable. For example, `/usr/local/go/bin/go`. +* `-gopath TEXT`, _required_: specifies a path to your workspace location. It defaults to a directory named `go` in your home directory: `$HOME/go` for Unix, `$home/go` for Plan 9, and `%USERPROFILE%\go` (usually `C:\Users\YourName\go`) for Windows. +* `-parallel INT`: specifies the number of fuzzing processes running at once (eight by default). +* `-et, --each-execution-timeout INT`: specifies a timeout in milliseconds for each target function execution. + The default timeout is 1,000 ms. +* `-at, --all-execution-timeout INT`: specifies a timeout in milliseconds for all target function executions. + The default timeout is 60,000 ms. +* `-p, --print-test`: specifies whether a test should be printed out to `StdOut`. Disabled by default. +* `-w, --overwrite`: specifies whether to overwrite the output test file if it already exists. Disabled by default. +* `-fm, --fuzzing-mode`: stops test generation when a `panic` situation, an error, or timeout occurs (only one test will be generated for one of these cases). +* `-h, --help`: shows a help message and exits. + +`runGo` options: + +* `-p, --package TEXT`, _required_: specifies a Go package to run tests for. +* `-go, --go-path TEXT`, _required_: specifies a path to a Go executable. For example, `/usr/local/go/bin/go`. +* `-v, --verbose`: specifies whether an output should be verbose. Disabled by default. +* `-j, --json`: specifies whether an output should be in JSON format. Disabled by default. +* `-o, --output TEXT`: specifies an output file for a test run report. Prints to `StdOut` by default. +* `-cov-mode, --coverage-mode [html|func|json]`: specifies whether a test coverage report should be generated and defines the report format. + Coverage report generation is disabled by default. +* `-cov-out, --coverage-output TEXT`: specifies the output file for a test coverage report. Required if `[--coverage-mode]` is + set. + +## Contributing to UnitTestBot Go + +To take part in project development or learn more about UnitTestBot Go, check +out the [Developer guide](../utbot-go/docs/DEVELOPER_GUIDE.md) and our [plans](../utbot-go/docs/FUTURE_PLANS.md). \ No newline at end of file diff --git a/docs/JavaScriptSupport.md b/docs/JavaScriptSupport.md new file mode 100644 index 0000000000..cfafa94e42 --- /dev/null +++ b/docs/JavaScriptSupport.md @@ -0,0 +1,117 @@ +# UnitTestBot JavaScript + +[UnitTestBot](https://www.utbot.org/) is the tool for automated unit test generation available as an IntelliJ IDEA plugin, or a command-line interface. + +Now UnitTestBot provides fuzzing-based support for JavaScript. + +## IntelliJ IDEA plugin + +### Requirements + +1. IntelliJ IDEA Ultimate — for compatibility, see [UnitTestBot on JetBrains Marketplace](https://plugins.jetbrains.com/plugin/19445-unittestbot/versions). +2. UnitTestBot plugin: please refer to [UnitTestBot user guide](https://github.com/UnitTestBot/UTBotJava/wiki/Install-or-update-plugin). +3. [Node.js 10.0.0 or later](https://nodejs.org/en/download/) (we recommend that you add Node.js to environment variables) + +_Note:_ when _npm_ cannot install requirements, try troubleshooting. +1. If the system prohibits installation: run _cmd_ via `sudo` or with administrator access, run `npm install -g `. +2. If Node.js is missing, or _npm_ is not installed: install Node.js with default configuration from the official website. + +### How to use + +1. In your IntelliJ IDEA, go to **File** > **Settings** > **Tools**, choose **UnitTestBot** and enable **Experimental languages support**. + + **(!) NOTE:** be sure to enable this option for **_each_** project. + +2. Go to **File** > **Settings** > **Languages & Frameworks**, choose **Node.js** and check if the path to Node.js executable file is specified. +3. In a JavaScript file, press **Alt+Shift+U** to open the generation dialog. + +## Command-line interface (CLI) + +### Build + +JAR file can be built in [GitHub Actions](https://github.com/UnitTestBot/UTBotJava/actions/workflows/publish-plugin-and-cli-from-branch.yml) with the `publish-plugin-and-cli-from-branch` script. + +### Requirements + +* [Node.js 10.0.0 or later](https://nodejs.org/en/download/) +* [Java 11 or later](https://www.oracle.com/java/technologies/downloads/) +* _nyc_ 15.1.0 or later: `> npm install -g nyc` +* Mocha 10.0.0 or later: `> npm install -g mocha` + +_Note:_ for each new project, _npm_ needs internet connection to install the required packages. + +### Generate tests: `generate_js` + + java -jar utbot-cli.jar generate_js --source="dir/file_with_sources.js" --output="dir/generated_tests.js" + + This will generate tests for top-level functions from `file_with_sources.js`. + +#### Options + +- `-s, --source ` + + _(required)_ Source code file for test generation. +- `-c, --class ` + + Specifies the class to generate tests for. + If not specified, tests for top-level functions or a single class are generated. + +- `-o, --output ` + + File for generated tests. +- `-p, --print-test` + + Specifies whether a test should be printed out to `StdOut` (default = false). +- `-t, --timeout ` + + Timeout for a single test case to generate: in seconds (default = 15). +- `--coverage-mode ` + + Specifies the coverage mode for test generation (used for coverage-based optimization). For now, the fast mode cannot deal with exceeding timeouts, but works faster (default = FAST). Do not use the fast mode if you guess there might be infinite loops in your code. +- `--path-to-node ` + + Sets a path to Node.js executable (default = "node"). +- `--path-to-nyc ` + + Sets a path to _nyc_ executable (default = "nyc"). +- `--path-to-npm ` + + Sets a path to _npm_ executable (default = "npm"). + +### Run generated tests: `run_js` + + java -jar utbot-cli.jar run_js --fileOrDir="generated_tests.js" + + This will run generated tests from a file or directory. + +#### Options + +- `-f, --fileOrDir` + + _(required)_ File or directory with tests. +- `-o, --output` + + Specifies the output TXT file for a test framework result (if empty, prints the result to `StdOut`). + +- `-t, --test-framework ` + + Test framework to use for test running (default = "Mocha"). + +### Generate a coverage report: `coverage_js` + + java -jar utbot-cli.jar coverage_js --source=dir/generated_tests.js + + This will generate a coverage report for generated tests and print it to `StdOut`. + +#### Options + +- `-s, --source ` + + _(required)_ File with tests to generate a report for. + +- `-o, --output` + + Specifies the output JSON file for a coverage report (if empty, prints the report to `StdOut`). +- `--path-to-nyc ` + + Sets a path to _nyc_ executable (default = "nyc"). diff --git a/docs/NightStatisticsMonitoring.md b/docs/NightStatisticsMonitoring.md index 2d5ae26cd3..226356b97f 100644 --- a/docs/NightStatisticsMonitoring.md +++ b/docs/NightStatisticsMonitoring.md @@ -35,79 +35,53 @@ stats.json Output example (the result of three runs during one night): -```json +```json5 [ { "parameters": { - "target": "guava", - "class_timeout_sec": 20, - "run_timeout_min": 20 + "fuzzing_ratio": 0.1, // how long does fuzzing takes + "class_timeout_sec": 20, // test generation timeout for one class + "run_timeout_min": 20 // total timeout for this run }, - "metrics": { - "duration_ms": 604225, - "classes_for_generation": 20, - "testcases_generated": 1651, - "classes_without_problems": 12, - "classes_canceled_by_timeout": 2, - "total_methods_for_generation": 519, - "methods_with_at_least_one_testcase_generated": 365, - "methods_with_exceptions": 46, - "suspicious_methods": 85, - "test_classes_failed_to_compile": 0, - "covered_instructions": 5753, - "covered_instructions_by_fuzzing": 4375, - "covered_instructions_by_concolic": 4069, - "total_instructions": 10182, - "avg_coverage": 62.885408034613 - } - }, - { - "parameters": { - "target": "guava", - "class_timeout_sec": 20, - "run_timeout_min": 20 - }, - "metrics": { - "duration_ms": 633713, - "classes_for_generation": 20, - "testcases_generated": 1872, - "classes_without_problems": 12, - "classes_canceled_by_timeout": 2, - "total_methods_for_generation": 519, - "methods_with_at_least_one_testcase_generated": 413, - "methods_with_exceptions": 46, - "suspicious_methods": 38, - "test_classes_failed_to_compile": 0, - "covered_instructions": 6291, - "covered_instructions_by_fuzzing": 4470, - "covered_instructions_by_concolic": 5232, - "total_instructions": 11011, - "avg_coverage": 62.966064315865275 - } - }, - { - "parameters": { - "target": "guava", - "class_timeout_sec": 20, - "run_timeout_min": 20 - }, - "metrics": { - "duration_ms": 660421, - "classes_for_generation": 20, - "testcases_generated": 1770, - "classes_without_problems": 13, - "classes_canceled_by_timeout": 2, - "total_methods_for_generation": 519, - "methods_with_at_least_one_testcase_generated": 405, - "methods_with_exceptions": 44, - "suspicious_methods": 43, - "test_classes_failed_to_compile": 0, - "covered_instructions": 6266, - "covered_instructions_by_fuzzing": 4543, - "covered_instructions_by_concolic": 5041, - "total_instructions": 11011, - "avg_coverage": 61.59069193429194 - } + "targets": [ // projects that have been processed + { + "target": "guava", // name of project + "summarised_metrics": { // summarised metrics for processed project + "total_classes": 20, // classes count + "testcases_generated": 1042, // generated unit-tests count + "classes_failed_to_compile": 0, // classes that's tests are not compilable + "classes_canceled_by_timeout": 4, // classes that's generation was canceled because of timeout + "total_methods": 526, // methods count + "methods_with_at_least_one_testcase_generated": 345, // methods with at least one successfully generated test + "methods_with_at_least_one_exception": 32, // methods that's generation contains exceptions + "methods_without_any_tests_and_exceptions": 59, // suspicious methods without any tests and exceptions + "covered_bytecode_instructions": 4240, // amount of bytecode instructions that were covered by generated tests + "covered_bytecode_instructions_by_fuzzing": 2946, // amount of bytecode instructions that were covered by fuzzing's generated tests + "covered_bytecode_instructions_by_concolic": 3464, // amount of bytecode instructions that were covered by concolic's generated tests + "total_bytecode_instructions": 9531, // total amount of bytecode instructions in methods with at least one testcase generated + "averaged_bytecode_instruction_coverage_by_classes": 0.5315060991492891 // mean bytecode coverage by class + }, + "metrics_by_class": [ // metrics for all classes in this project + { + "class_name": "com.google.common.math.LongMath", // name of processed class + "metrics": { // metrics for specified class + "testcases_generated": 91, // amount of generated unit-tests + "failed_to_compile": false, // compilation generated tests are failure + "canceled_by_timeout": true, // generation is interrupted because of timeout + "total_methods_in_class": 31, // methods count in this class + "methods_with_at_least_one_testcase_generated": 26, // methods with at least one successfully generated test + "methods_with_at_least_one_exception": 0, // methods that's generation contains exceptions + "methods_without_any_tests_and_exceptions": 5, // suspicious methods without any tests and exceptions + "covered_bytecode_instructions_in_class": 585, // amount of bytecode instructions that were covered by generated tests + "covered_bytecode_instructions_in_class_by_fuzzing": 489, // amount of bytecode instructions that were covered by fuzzing's generated tests + "covered_bytecode_instructions_in_class_by_concolic": 376, // amount of bytecode instructions that were covered by concolic's generated tests + "total_bytecode_instructions_in_class": 1442 // total amount of bytecode instructions in methods with at least one testcase generated + } + }, + // ... + ] + } + ] } ] ``` @@ -120,7 +94,7 @@ The `insert_metadata.py` script is responsible for doing this. To run it you hav To get more information about input arguments call script with option `--help`. -Output format: you get the JSON file, containing statistics and parameters grouped by target project and metadata. +Output format: you get the JSON file, containing metadata, statistics and parameters grouped by target project and classes. Input example: ``` @@ -131,93 +105,78 @@ Input example: ``` Output example (statistics followed by metadata): -```json +```json5 { - "version": 1, - "targets": [ + "version": 2, // version of json format + "targets": [ // projects and methods that have been processed { - "id": "guava", - "version": "0", - "parameters": [ + "target": "guava", // name of project + "summarised": [ // list of summarised metrics with parameters on each run { - "class_timeout_sec": 20, - "run_timeout_min": 20 + "parameters": { + "fuzzing_ratio": 0.1, // how long does fuzzing takes + "class_timeout_sec": 20, // test generation timeout for one class + "run_timeout_min": 20 // total timeout for this run + }, + "metrics": { + "total_classes": 20, // classes count + "testcases_generated": 1042, // generated unit-tests count + "classes_failed_to_compile": 0, // classes that's tests are not compilable + "classes_canceled_by_timeout": 4, // classes that's generation was canceled because of timeout + "total_methods": 526, // methods count + "methods_with_at_least_one_testcase_generated": 345, // methods with at least one successfully generated test + "methods_with_at_least_one_exception": 32, // methods that's generation contains exceptions + "methods_without_any_tests_and_exceptions": 59, // suspicious methods without any tests and exceptions + "total_bytecode_instruction_coverage": 0.44486412758, // total bytecode coverage of generated tests + "total_bytecode_instruction_coverage_by_fuzzing": 0.30909663204, // total bytecode coverage of fuzzing's generated tests + "total_bytecode_instruction_coverage_by_concolic": 0.36344559857, // total bytecode coverage of concolic's generated tests + "averaged_bytecode_instruction_coverage_by_classes": 0.5315060991492891 // mean bytecode coverage by class + } }, - { - "class_timeout_sec": 20, - "run_timeout_min": 20 - }, - { - "class_timeout_sec": 20, - "run_timeout_min": 20 - } + // ... ], - "metrics": [ - { - "duration_ms": 604225, - "classes_for_generation": 20, - "testcases_generated": 1651, - "classes_without_problems": 12, - "classes_canceled_by_timeout": 2, - "total_methods_for_generation": 519, - "methods_with_at_least_one_testcase_generated": 365, - "methods_with_exceptions": 46, - "suspicious_methods": 85, - "test_classes_failed_to_compile": 0, - "covered_instructions": 5753, - "covered_instructions_by_fuzzing": 4375, - "covered_instructions_by_concolic": 4069, - "total_instructions": 10182, - "avg_coverage": 62.885408034613 - }, + "by_class": [ // list of metrics and parameters for all classes in project and on each run { - "duration_ms": 633713, - "classes_for_generation": 20, - "testcases_generated": 1872, - "classes_without_problems": 12, - "classes_canceled_by_timeout": 2, - "total_methods_for_generation": 519, - "methods_with_at_least_one_testcase_generated": 413, - "methods_with_exceptions": 46, - "suspicious_methods": 38, - "test_classes_failed_to_compile": 0, - "covered_instructions": 6291, - "covered_instructions_by_fuzzing": 4470, - "covered_instructions_by_concolic": 5232, - "total_instructions": 11011, - "avg_coverage": 62.966064315865275 + "class_name": "com.google.common.math.LongMath", // name of processed class + "data": [ // metrics and parameters on each run + { + "parameters": { // parameters on this run + "fuzzing_ratio": 0.1, // how long does fuzzing takes + "class_timeout_sec": 20, // test generation timeout for one class + "run_timeout_min": 20 // total timeout for this run + }, + "metrics": { // metrics for specified class on this run + "testcases_generated": 91, // amount of generated unit-tests + "failed_to_compile": false, // compilation generated tests are failure + "canceled_by_timeout": true, // generation is interrupted because of timeout + "total_methods_in_class": 31, // methods count in this class + "methods_with_at_least_one_testcase_generated": 26, // methods with at least one successfully generated test + "methods_with_at_least_one_exception": 0, // methods that's generation contains exceptions + "methods_without_any_tests_and_exceptions": 5, // suspicious methods without any tests and exceptions + "total_bytecode_instruction_coverage_in_class": 0.40568654646, // bytecode coverage of generated tests + "total_bytecode_instruction_coverage_in_class_by_fuzzing": 0.33911234396, // bytecode coverage of fuzzing's generated tests + "total_bytecode_instruction_coverage_in_class_by_concolic": 0.26074895977 // bytecode coverage of concolic's generated tests + } + }, + // ... + ] }, - { - "duration_ms": 660421, - "classes_for_generation": 20, - "testcases_generated": 1770, - "classes_without_problems": 13, - "classes_canceled_by_timeout": 2, - "total_methods_for_generation": 519, - "methods_with_at_least_one_testcase_generated": 405, - "methods_with_exceptions": 44, - "suspicious_methods": 43, - "test_classes_failed_to_compile": 0, - "covered_instructions": 6266, - "covered_instructions_by_fuzzing": 4543, - "covered_instructions_by_concolic": 5041, - "total_instructions": 11011, - "avg_coverage": 61.59069193429194 - } + // ... ] - } + }, + // ... ], - "metadata": { - "source": { + "metadata": { // device's properties + "source": { // information about runner "type": "github-action", "id": "2917672580" }, - "commit_hash": "66a1aeb6", - "branch": "main", - "build_number": "2022.8", - "timestamp": 1661330445, - "date": "2022-08-24T08:40:45", - "environment": { + "commit_hash": "66a1aeb6", // commit hash of used utbot build + "branch": "main", // branch of used utbot build + "build_number": "2022.8", // build number of used utbot build + "timestamp": 1661330445, // run timestamp + "date": "2022-08-24T08:40:45", // human-readable run timestamp + "environment": { // device's environment "host": "fv-az183-700", "OS": "Linux version #20~20.04.1-Ubuntu SMP Fri Aug 5 12:16:53 UTC 2022", "java_version": "openjdk version \"11.0.16\" 2022-07-19 LTS\nOpenJDK Runtime Environment Zulu11.58+15-CA (build 11.0.16+8-LTS)\nOpenJDK 64-Bit Server VM Zulu11.58+15-CA (build 11.0.16+8-LTS, mixed mode)\n", @@ -230,112 +189,53 @@ Output example (statistics followed by metadata): } ``` -### Aggregating - -The `build_aggregated_data.py` script gathers the results for several nights. The collected results for each of the nights are put together into one array. You can specify the period for aggregating. It is useful for visualising or finding statistical characteristics of UnitTestBot performance, e.g. the median or max/min values. - -To run aggregating you should provide the input. - -To get more information about input arguments call script with option `--help`. +### Datastorage structure -Output format: you get the JSON file, which contains arrays of grouped by target results for each of the nights during the specified period. +We store the collected statistics in our repository. You can find two special branches: `monitoring-data` and `monitoring-aggregated-data`. -Input example: +The `monitoring-data` branch is a storage for raw statistics data as well as metadata. -``` ---input_data_dir ./data --output_file aggregated_data.json ---timestamp_from 0 --timestamp_to 1661330445 -``` +The filename format: `--
-------.json` -Output example (You'll get an array of several outputs without metadata): -```json -[ - { - "id": "guava", - "version": "0", - "parameters": [ - { - "class_timeout_sec": 20, - "run_timeout_min": 20, - "timestamp": 1661330445 - }, - { - "class_timeout_sec": 20, - "run_timeout_min": 20, - "timestamp": 1661330445 - }, - { - "class_timeout_sec": 20, - "run_timeout_min": 20, - "timestamp": 1661330445 - } - ], - "metrics": [ - { - "duration_ms": 604225, - "classes_for_generation": 20, - "testcases_generated": 1651, - "classes_without_problems": 12, - "classes_canceled_by_timeout": 2, - "total_methods_for_generation": 519, - "methods_with_at_least_one_testcase_generated": 365, - "methods_with_exceptions": 46, - "suspicious_methods": 85, - "test_classes_failed_to_compile": 0, - "avg_coverage": 62.885408034613, - "total_coverage": 56.50166961304262, - "total_coverage_by_fuzzing": 42.967982714594385, - "total_coverage_by_concolic": 39.96267923787075 - }, - { - "duration_ms": 633713, - "classes_for_generation": 20, - "testcases_generated": 1872, - "classes_without_problems": 12, - "classes_canceled_by_timeout": 2, - "total_methods_for_generation": 519, - "methods_with_at_least_one_testcase_generated": 413, - "methods_with_exceptions": 46, - "suspicious_methods": 38, - "test_classes_failed_to_compile": 0, - "avg_coverage": 62.966064315865275, - "total_coverage": 57.133775315593496, - "total_coverage_by_fuzzing": 40.595767868495145, - "total_coverage_by_concolic": 47.51612024339297 - }, - { - "duration_ms": 660421, - "classes_for_generation": 20, - "testcases_generated": 1770, - "classes_without_problems": 13, - "classes_canceled_by_timeout": 2, - "total_methods_for_generation": 519, - "methods_with_at_least_one_testcase_generated": 405, - "methods_with_exceptions": 44, - "suspicious_methods": 43, - "test_classes_failed_to_compile": 0, - "avg_coverage": 61.59069193429194, - "total_coverage": 56.90672963400236, - "total_coverage_by_fuzzing": 41.25874125874126, - "total_coverage_by_concolic": 45.78149123603669 - } - ] - } -] -``` +### Grafana -### Datastorage structure +#### Usage -We store the collected statistics in our repository. You can find two special branches: `monitoring-data` and `monitoring-aggregated-data`. +We can use [Grafana](https://monitoring.utbot.org) for more dynamic and detailed statistics visualisation. Grafana pulls data from our repository automatically. -The `monitoring-data` branch is a storage for raw statistics data as well as metadata. +#### Metrics format -The filename format: `data----
--.json` +Our goal after collecting statistics is uploading results into grafana. For this we should prepare data and send it to our server. -The `monitoring-aggregated-data` branch is a storage for aggregated statistics. The aggregating period is set to one month by default. +The `prepare_metrics.py` script is responsible for doing this. To run it you have to specify the following arguments. -The filename format: `aggregated-data---
.json` +To get more information about input arguments call script with option `--help`. -### Grafana (in process) +Output format: you get the JSON file, containing array of metrics with some labels. -We can use [Grafana](https://monitoring.utbot.org) for more dynamic and detailed statistics visualisation. Grafana pulls data from our repository automatically by means of GitHub API. +Output example: +```json5 +[ + // summarised + { + "metric": "testcases_generated", + "labels": { + "project": "guava", + "fuzzing_ratio": 0.1 + }, + "value": 1024 + }, + // ... + // by classes + { + "metric": "testcases_generated", + "labels": { + "project": "guava", + "class": "com.google.common.math.LongMath", + "fuzzing_ratio": 0.1 + }, + "value": 91 + }, + // ... +] +``` diff --git a/docs/OverallArchitecture.md b/docs/OverallArchitecture.md new file mode 100644 index 0000000000..ba9a48562a --- /dev/null +++ b/docs/OverallArchitecture.md @@ -0,0 +1,380 @@ +# UnitTestBot overall architecture + +Get the bird's-eye view of UnitTestBot overall architecture in the following chart. Check the purpose of each component in the descriptions below. + +```mermaid +flowchart TB + subgraph Clients + direction LR + IntellijPlugin + MavenPlugin["Maven/Gradle plugins"] + GithubAction-->MavenPlugin + ExternalJavaClient[\External Java Client\] + CLI + ContestEstimator + + end + + subgraph Facades + direction LR + EngineMain[[EngineMain]] + UtBotJavaApi[[UtBotJavaApi]] + GenerateTestsAndSarifReport[[GenerateTestsAndSarifReport]] + end + IntellijPlugin--Rd-->EngineMain + MavenPlugin-->GenerateTestsAndSarifReport + ExternalJavaClient-->UtBotJavaApi + + subgraph API["Generation API"] + direction LR + TestCaseGenerator[[TestCaseGenerator]] + CodeGenerator[[CodeGenerator]] + end + Facades-->API + CLI-->API + ContestEstimator-->API + + + + subgraph Components + direction LR + SymbolicEngine-->jlearch + SymbolicEngine-->ConcreteExecutor + Fuzzer-->ConcreteExecutor + SarifReport + Minimizer + CodeRenderer + Summaries + jlearch + end + CodeGenerator-->CodeRenderer + TestCaseGenerator-->SymbolicEngine + TestCaseGenerator-->Fuzzer + TestCaseGenerator-->Minimizer + TestCaseGenerator-->Summaries + GenerateTestsAndSarifReport-->SarifReport + + UTSettings((UTSettings)) + UTSettings<--Rd/local-->Components + UTSettings<---->Clients + TestCaseGenerator--warmup-->ConcreteExecutor + ConcreteExecutor--Rd-->InstrumentedProcess + +``` + +## Typical interaction between components + +An interaction diagram for UnitTesBot components is presented below. See how it starts from IntelliJ IDEA plugin UI and follow the flow. +```mermaid +sequenceDiagram + autonumber + actor user as User + participant ij as IDE process + participant engine as Engine process + participant concrete as Instrumented process + + user ->> ij: Invoke "Generate Tests with UnitTestBot" + ij ->> ij: Calculate methods, framework to show + ij ->> user: Show UI + + break User clicked "Cancel" + user -->> user: Exit + end + user ->> ij: Click "Generate Tests" + ij ->> ij: Calculate what JAR needs to be installed + + opt Some JARs need to be installed + ij ->> ij: Install JARs + end + + ij ->> engine: Start process + activate engine + ij ->> engine: Setup up context + + loop For all files + ij ->> engine: Generate UtExecution models + loop For all UtExecution models: for the method found by engine + engine ->> concrete: Run concretely + end + engine --> engine: Minimize the number of tests for the method + engine --> engine: Generate summaries for the method + end + ij ->> engine: Render code + engine ->> ij: File with tests + deactivate engine + +``` + +## Clients + +### IntelliJ IDEA plugin + +The plugin provides +* a UI for the IntelliJ-based IDEs to use UnitTestBot directly from source code, +* the linkage between IntelliJ Platform API and UnitTestBot API, +* support for the most popular programming languages and frameworks for end users (the plugin and its optional dependencies are described in [plugin.xml](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-intellij/src/main/resources/META-INF/plugin.xml) and nearby, in the [`META-INF`](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-intellij/src/main/resources/META-INF) folder). + +The main plugin module is [utbot-intellij](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-intellij), providing support for Java and Kotlin. +Also, there is an auxiliary [utbot-ui-commons](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-ui-commons) module to support providers for other languages. + +As for the UI, there are two entry points: +* [GenerateTestAction](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt) for _preparing and calling_ test generation; +* [SettingsWindow](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/SettingsWindow.kt) for _per-project_ UnitTestBot configuring. + +The main plugin-specific features are: +* A common action for generating tests right from the editor or a project tree — with a generation scope from a single method up to the whole source root. See [GenerateTestAction](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt) — the same for all supported languages. +* Auto-installation of the user-chosen testing framework as a project library dependency (JUnit 4, JUnit 5, and TestNG are supported). See [UtIdeaProjectModelModifier](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtIdeaProjectModelModifier.kt) and the Maven-specific version: [UtMavenProjectModelModifier](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtMavenProjectModelModifier.kt). +* Suggesting the location for a test source root and auto-generating the `utbot_tests` folder there, providing users with a sandbox in their code space. +* Optimizing generated code with IDE-provided intentions (experimental). See [IntentionHelper](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/IntentionHelper.kt) for details. +* An option for distributing generation time between symbolic execution and fuzzing explicitly. +* Running generated tests while showing coverage with the IDE-provided measurement tools. See [RunConfigurationHelper](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/RunConfigurationHelper.kt) for implementation. +* Displaying the UnitTestBot-found code defects as IntelliJ-specific inspections and quickfixes in the "Problems" tool window. See the [inspection](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection) package. +* Two kinds of Javadoc comments in the generated code: rendered as plain text and structured via custom tags. See the [javadoc](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc) package. +* A self-documenting [`settings.properties`](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/SettingsProperties.md) file with the settings for low-level UnitTestBot tuning. + +### Gradle/Maven plugins + +Plugins for Gradle/Maven build systems provide the UnitTestBot `GenerateTestsAndSarifReportFacade` component with the user-chosen settings (test generation timeout, testing framework, etc.). This component runs test generation and creates SARIF reports. + +For more information on the plugins, please refer to the detailed design documents: +- [UnitTestBot Gradle plugin](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-gradle/docs/utbot-gradle.md) +- [UnitTestBot Maven plugin](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-maven/docs/utbot-maven.md) + +You can find the modules here: +* [utbot-gradle](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-gradle) +* [utbot-maven](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-maven) + +### GitHub Action + +UnitTestBot GitHub Action displays the detected code defects in the GitHub "Security Code Scanning Alerts" section. + +You can find UnitTestBot GitHub Action in the separate [repository](https://github.com/UnitTestBot/UTBotJava-action). + +UnitTestBot GitHub Action uses the [UnitTestBot Gradle plugin](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-gradle) +to run UnitTestBot on the user's repository and imports the SARIF output into the "Security Code Scanning Alerts" section. +Please note that at the moment this action cannot work with Maven projects because +the [UnitTestBot Maven plugin](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-maven) has not been published yet. + +For more information on UnitTestBot GitHub Action, please refer to the [related docs](https://github.com/UnitTestBot/UTBotJava-action#readme). +You can also find a detailed [usage example](https://github.com/UnitTestBot/UTBotJava-action-example). + +### Command-line interface (CLI) + +With CLI, one can run UnitTestBot from the command line. + +CLI implementation is placed in the [utbot-cli](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-cli) module. UnitTestBot CLI has two main commands: `generate` and `run` — use `--help` to find more info on their options. + +An executable CLI is distributed as a JAR file. + +We provide Linux Docker images containing CLI. They are stored on [GitHub Packages](https://github.com/UnitTestBot/UTBotJava/pkgs/container/utbotjava%2Futbot_java_cli). + +### Contest estimator + +Contest estimator runs UnitTestBot on the provided projects and returns the generation statistics such as instruction coverage. + +Contest estimator is placed in the [utbot-junit-contest](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-junit-contest) module and has two entry points: +- [ContestEstimator.kt](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimator.kt) is the main entry point. It runs UnitTestBot on the specified projects, calculates statistics for the target classes and projects, and outputs them to a console. +- [StatisticsMonitoring.kt](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-junit-contest/src/main/kotlin/org/utbot/monitoring/StatisticsMonitoring.kt) is an additional entry point, which does the same as the previous one but can be configured from a file and dumps the resulting statistics to a file. + It is used to [monitor and chart](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/NightStatisticsMonitoring.md) statistics nightly. + +## Components + +### Symbolic engine + +Symbolic engine is a component maintaining the whole analysis process: from the moment it takes information about a method under test (MUT) till the moment it returns a set of execution results along with the information required to reproduce the MUT's execution paths. The engine uses symbolic execution to explore the paths in a MUT's control flow graph (CFG). + +The technique is rather complex, so the engine consists of several subcomponents, each responsible for a certain part of the analysis: + +* [UtBotSymbolicEngine.kt](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt) contains a `UtBotSymbolicEngine` class — it manages interaction between different parts of the system and controls the analysis flow. This class is an entry point for symbolic execution in UnitTestBot. Using `UtBotSymbolicEngine` API, the users or UnitTestBot components can start, suspend or stop the analysis. `UtBotSymbolicEngine` provides a connection between the components by taking execution states from [PathSelector](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelector.kt) and pushing them either into [Traverser](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt) or in [ConcreteExecutor](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt), depending on their status and settings. + In a few words, the pipeline looks like this: the engine takes a state from [PathSelector](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelector.kt), pushes it into [Traverser](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt), and then gets an updated state from it. If this state is not terminal, `UtBotSymbolicEngine` pushes it to the queue in the path selector. If this state is terminal, it either calls `ConcreteExecutor` to get a concrete result state or yields a symbolic result into the resulting flow. +* [PathSelector](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelector.kt) is a class making a decision on which instruction of the program should be processed next. It is located in `PathSelector.kt`, but the whole [selectors](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-framework/src/main/kotlin/org/utbot/engine/selectors) package is related to it. `PathSelector` has a pretty simple interface: it can put a state in the queue or return a state from that queue. +* [Traverser](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt) processes a given state. It contains information about CFG, a hierarchy of classes in the program, a symbolic type system, and mocking information. `Traverser` is the most important class in the symbolic engine module. It knows how to process instructions in CFG, how to update the dependent symbolic memory, and which constraints should be added to go through a certain path. Having processed an instruction from the given state, `Traverser` creates a new one, with updated memory and path constraints. + +There are other important classes in the symbolic engine subsystem. Here are some of them: +* [Memory](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/engine/Memory.kt) is responsible for the symbolic memory representation of a state in the program. +* [TypeRegistry](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/engine/types/TypeRegistry.kt) contains information about a type system. +* `Mocker` decides whether we should mock an object or not. + +You can find all the engine-related classes in the [engine](https://github.com/UnitTestBot/UTBotJava/tree/main/utbot-framework/src/main/kotlin/org/utbot/engine) module. + +### Concrete executor + +`ConcreteExecutor` is the input point for the _instrumented process_ used by UnitTestBot symbolic engine and fuzzer. This class provides a smooth and concise interaction between the _instrumented process_ and a user, whereas the _instrumented process_ executes a given function with the supplied arguments. + +`ConcreteExecutor` is parameterized with `Instrumentation` and its return type via the generic arguments. `Instrumentation` is an interface, so inheritors have to implement the logic of a specific method invocation in an isolated environment as well as the `transform` function used for instrumenting classes. For our purposes, we use `UtExecutionInstrumentation`. + +The main `ConcreteExecutor` function is +```kotlin +suspend fun executeAsync( + kCallable: KCallable<*>, + arguments: Array, + parameters: Any? +): TResult +``` +It serializes the arguments and some parameters (e.g., static fields), sends it to the _instrumented process_ and retrieves the result. + +Internally, `ConcreteExecutor` uses `Rd` for interprocess communication and `Kryo` for objects serialization. You don't need to provide a marshaller, as `Kryo` serializes the objects (sometimes it fails). + +`ConcreteExecutor` is placed in the [utbot-instrumentation](../utbot-instrumentation) module. You can find the corresponding tests in the [utbot-instrumentation-tests](../utbot-instrumentation-tests) module. + +### Instrumented process + +`Instrumented process` concretely runs the user functions with the specified arguments and returns the result of execution. + +Additionally, `Instrumented process` evaluates instruction coverage and mocks function invocations and creating instances via the user's Java bytecode instrumentation. + +The main concept is _instrumentation_. `Instrumentation` is a class that implements the [corresponding](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt) interface. It transforms the user code and provides invoking user functions. + +`Instrumented process` supports the following commands described in [InstrumentedProcessModel.kt](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt): +- `AddPaths` tells where the `Instrumented process` should search for the user classes. +- `Warmup` forces loading and instrumenting user classes. +- `SetInstrumentation` tells which instrumentation the `Instrumented process` should use. +- `InvokeMethodCommand` requests the `Instumented process` to invoke a given user function and awaits the results. +- `StopProcess` just stops the `Instrumented process`. +- `CollectCoverage` requests collecting code coverage for the specified class. +- `ComputeStaticField` requests the specified static field value. + +These commands are delivered from the other processes by `Rd`. + +The main instrumentation of UnitTestBot is [UtExecutionInstrumentation](https://github.com/UnitTestBot/UTBotJava/blob/main/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt). + +### Code generator + +Code generation and rendering are a part of the test generation process in UnitTestBot. +UnitTestBot gets the synthetic representation of generated test cases from the fuzzer or the symbolic engine. +This representation (or model) is implemented in the `UtExecution` class. +The `codegen` module generates the real test code based on this `UtExecution` model +and renders it in a human-readable form. + +The `codegen` module +- converts `UtExecution` test information into an Abstract Syntax Tree (AST) representation using `CodeGenerator`, +- renders this AST according to the requested configuration (considering programming language, testing + framework, mocking and parameterization options) using `renderer`. + +The `codegen` entry points are: +- `CodeGenerator.generateAsString()` +- `CodeGenerator.generateAsStringWithTestReport()` + +The detailed implementation is described in the [Code generation and rendering](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/CodeGenerationAndRendering.md) design doc. + +### Fuzzer + +Fuzzing is a versatile technique for "guessing" values to be used as method arguments. To generate these kinds of values, the fuzzer uses generators, mutators, and predefined values. + +Fuzzing has been previously implemented in UnitTestBot as the solution for Java. For now, we have developed the generic platform that supports generating fuzzing tests for different languages. The Java fuzzing solution in UnitTestBot is marked as deprecated — it will soon be replaced with the fuzzing platform. + +You can find the relevant code here: +- `utbot-fuzzing` is the general fuzzing platform module. The related API is located in `org/utbot/fuzzing/Api.kt`. +- `utbot-fuzzer` is the module with the fuzzing solution for Java. Find the corresponding API in `org/utbot/fuzzing/JavaLanguage.kt`. + +Entry point for generating values (Java): `org/utbot/fuzzing/JavaLanguage.kt#runJavaFuzzing(...)` + +You can find the detailed description in the [Fuzzing Platform](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/Fuzzing%20Platform.md) design doc. + +### Minimizer + +Minimization is used to decrease the amount of `UtExecution` instances without decreasing coverage. + +The entry point is the [minimizeTestCase](https://github.com/UnitTestBot/UTBotJava/blob/d2d2e350bc75943b78f2078002a5cabc5dd62072/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt#L38) function. It receives a set of `UtExecution` instances and a grouping function (grouping by `UtExecution::utExecutionResult`). Then the minimization procedure divides the set of `UtExecution` instances into several groups. Each group is minimized independently. + +We have different groups — here are some of them: +- A _successful regression suite_ that consists of `UtSuccess` and `UtExplicitlyThrownException` executions. +- An _error suite_ consisting of `UtImplicitlyThrownException` executions. +- A _timeout suite_ that consists of `UtTimeoutException` executions. +- A _crash suite_ consisting of executions where some parts of the engine failed. + +Each `UtExecution` instance provided should have coverage information, otherwise we add this execution to the test suite instantly. Coverage data are usually obtained from the _instrumented process_ and consist of covered lines. + +To minimize the number of executions in a group, we use a simple greedy algorithm: +1. Pick an execution that covers the maximum number of the previously uncovered lines. +2. Add this execution to the final suite and mark new lines as covered. +3. Repeat the first step and continue till there are executions containing uncovered lines. + +The whole minimization procedure is located in the [org.utbot.framework.minimization](../utbot-framework/src/main/kotlin/org/utbot/framework/minimization) package inside the [utbot-framework](../utbot-framework) module. + +### Summarization module + +Summarization is the process of generating detailed test descriptions consisting of +- test method names +- testing framework annotations (including `@DisplayName`) +- Javadoc comments for tests +- cluster comments for groups of tests (_Regions_) + +Each of these description elements can be turned off via `{userHome}/.utbot/settings.properties` (which gets information from `UtSettings.kt`). +If the summarization process fails due to an error or insufficient information, then the test method receives a unique name and no additional meta-information. + +This meta-information is generated for each type of `UtExecution` model and thus may vary significantly. +Also, Javadoc comments can be rendered in two styles: as plain text or in a special format enriched with the [custom Javadoc tags](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/summaries/CustomJavadocTags.md). + +The whole summarization subsystem is located in the `utbot-summary` module. + +For detailed information, please refer to the Summarization architecture design document. + +### SARIF report generator + +SARIF (Static Analysis Results Interchange Format) is a JSON-based format for displaying static analysis results. + +All the necessary information about the format and its usage can be found +in the [official documentation](https://github.com/microsoft/sarif-tutorials/blob/main/README.md) +and in the related [GitHub Docs](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning). + +In UnitTestBot, the `SarifReport` class is responsible for generating SARIF reports. +We use them to display UnitTestBot-detected errors such as unchecked exceptions, overflows, assertion errors, etc. + +For example, for the class below +```Java +public class Main { + int example(int x) { + return 1 / x; + } +} +``` + +UnitTestBot creates a report containing the following information: +- `java.lang.ArithmeticException: / by zero` may occur in line 3 +- The exception occurs if `x == 0` +- To reproduce this error, the user can run the generated `MainTest.testExampleThrowsAEWithCornerCase` test +- The exception stack trace: + - `Main.example(Main.java:3)` + - `MainTest.testExampleThrowsAEWithCornerCase(MainTest.java:39)` + +## Cross-cutting subsystems + +### Rd + +UnitTestBot consists of three processes (according to the execution order): +* _IDE process_ — the process where the plugin part executes. +* _Engine process_ — the process where the test generation engine executes. +* _Instrumented process_ — the process where concrete execution takes place. + +These processes are built on top of the [Reactive distributed communication framework (Rd)](https://github.com/JetBrains/rd) developed by JetBrains. + +One of the main Rd concepts is a _Lifetime_ — it helps to release shared resources upon the object's termination. +You can find the Rd basic ideas and UnitTestBot implementation details in the [Multiprocess architecture](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/RD%20for%20UnitTestBot.md) design doc. + +### Settings + +In UnitTestBot, _settings_ are the set of _properties_. Each _property_ is a _key=value_ pair and affects some important aspect of UnitTestBot behavior. UnitTestBot as an IntelliJ IDEA plugin, a CI-tool, or a CLI-tool has low-level _core settings_. The UnitTestBot plugin also has per-project _plugin-specific_ settings. + +Core settings are persisted in the _settings file_: `{userHome}/.utbot/settings.properties`. This file is designed for reading only. The defaults for the core settings are provided in source code (`UtSettings.kt`). + +The plugin-specific settings are stored per project in the _plugin configuration file_: `{projectDir}/.idea/utbot-settings.xml`. Nobody is expected to edit this file manually. + +The end user has three places to change UnitTestBot behavior: +1. A `{userHome}/.utbot/settings.properties` file — for global settings. +2. Plugin settings UI (**File** > **Settings** > **Tools** > **UnitTestBot**) — for per-project settings. +3. Controls in the **Generate Tests with UnitTestBot window** dialog — for per-generation settings. + +### Logging + +The UnitTestBot Java logging system is implemented across the IDE process, the Engine process, and the Instrumented process. + +UnitTestBot Java logging relies on `log4j2` library. +The custom Rd logging system is recommended as the default one for the Instrumented process. + +In the [Logging](../docs/contributing/InterProcessLogging.md) document, +you can find how to configure the logging system when UnitTestBot Java is used +* as an IntelliJ IDEA plugin, +* as Contest estimator or the Gradle/Maven plugins, via CLI or during the CI test runs. + +Implementation details, log level and performance questions are also addressed [here](../docs/contributing/InterProcessLogging.md). \ No newline at end of file diff --git a/docs/PythonSupport.md b/docs/PythonSupport.md new file mode 100644 index 0000000000..eea6d80909 --- /dev/null +++ b/docs/PythonSupport.md @@ -0,0 +1,31 @@ +# UnitTestBot Python + +[UnitTestBot](https://www.utbot.org/) is the tool for automated unit test generation available as an IntelliJ IDEA plugin, or a command-line interface. + +Now UnitTestBot provides fuzzing-based support for Python. + +## Requirements + +1. IntelliJ IDEA — for compatibility, see [UnitTestBot on JetBrains Marketplace](https://plugins.jetbrains.com/plugin/19445-unittestbot/versions). +2. Python 3.8 or later +3. [Python plugin](https://plugins.jetbrains.com/plugin/631-python) for IntelliJ IDEA + +If you already have a Python project, you usually have no need to install or configure anything else, but if you +have trouble with launching UnitTestBot for Python code, please refer to [advanced requirements section](../utbot-python/docs/CLI.md#requirements). + +## How to install and use + +To try UnitTestBot Python in your IntelliJ IDEA: +1. To install the plugin, please refer to [UnitTestBot user guide](https://github.com/UnitTestBot/UTBotJava/wiki/Install-or-update-plugin). +2. Configure the Python interpreter for your project and make sure IntelliJ IDEA resolves all the imports. +3. In your IntelliJ IDEA, go to **File** > **Settings** > **Tools**, choose **UnitTestBot** and enable **Experimental languages support**. + + **(!) NOTE:** be sure to enable this option for **_each_** project. + +4. To generate tests, place the caret at the required function and press **Alt+Shift+U**. To find the appropriate shortcut for the OS you are using, check the context menu. + +To use UnitTestBot Python via command-line interface, please refer to the [CLI guide](../utbot-python/docs/CLI.md). + +## How to contribute and get support + +To get information on contributing and getting support, please see [UnitTestBot Java Readme](https://github.com/UnitTestBot/UTBotJava#readme). diff --git a/docs/RD for UnitTestBot.md b/docs/RD for UnitTestBot.md new file mode 100644 index 0000000000..49ffba4e4d --- /dev/null +++ b/docs/RD for UnitTestBot.md @@ -0,0 +1,235 @@ +# Multiprocess architecture + +## Overview + +UnitTestBot consists of three processes: +1. `IDE process` — the process where the plugin part executes. We also call it the _plugin process_ or the + _IntelliJ IDEA process_. +2. `Engine process` — the process where the test generation engine executes. +3. `Instrumented process` — the process where concrete execution takes place. + +These processes are built on top of the [Reactive distributed communication framework (Rd)](https://github.com/JetBrains/rd) developed by JetBrains. Rd plays a crucial role in UnitTestBot machinery, so we briefly +describe this library here. + +To gain an insight into Rd, one should grasp these Rd concepts: +1. Lifetime +2. Rd entities +3. `Rdgen` + +## Lifetime concept + +Imagine an object holding resources that should be released upon the object's death. In Java, +`Closeable` and `AutoCloseable` interfaces are introduced to help release resources that the object is holding. +Support for `try`-with-resources in Java and `Closeable.use` in Kotlin are also implemented to assure that the +resources are closed after the execution of the given block. + +Though, releasing resources upon the object's death is still problematic: +1. An object's lifetime can be more complicated than the scope of `Closeable.use`. +2. If `Closeable` is a function parameter, should we close it? +3. Multithreading and concurrency may lead to more complex situations. +4. Considering all these issues, how should we correctly close the objects that depend on some other object's + lifetime? How can we perform this task in a fast and easy way? + +So, Rd introduces the concept of `Lifetime`. + +### `Lifetime` + +_Note:_ the described relationships and behavior refer to the JVM-related part of Rd. + +`Lifetime` is an abstract class, with `LifetimeDefinition` as its inheritor. `LifetimeDefinition` +has only one difference from its parent: `LifetimeDefinition` can be terminated. +Each `Lifetime` variable is an instance of `LifetimeDefinition` (we later call it "`Lifetime` instance"). You +can register callbacks in this `Lifetime` instance — all of them will be executed upon the termination. + +Though all `Lifetime` objects are instances of `LifetimeDefinition`, there are conventions for using them: +1. Do not cast `Lifetime` to `LifetimeDefinion` unless you are the one who created `LifetimeDefinition`. +2. If you introduce `LifetimeDefinition` somewhere, you should attach it to another `Lifetime` or provide + the code that terminates it. + +A `Lifetime` instance has these useful methods: +- `onTermination` executes _lambda_/_closeable_ when the `Lifetime` instance is terminated. If an instance has been + already terminated, it executes _lambda_/_closeable_ instantly. Termination proceeds on a thread that has invoked + `LifetimeDefinition.terminate`. Callbacks are executed in the **reversed order**, which is _LIFO_: the last added + callback is executed first. +- `onTerminationIfAlive` is the same as `onTermination`, but the callback is executed only if the `Lifetime` + instance is `Alive`. +- `executeIfAlive` executes _lambda_ if the `Lifetime` instance is `Alive`. This method guarantees that the `Lifetime` + instance is alive (i.e. will not be terminated) during the whole time of _lambda_ execution. +- `createdNested` creates the _child_ `LifetimeDefinition` instance: it can be terminated if the _parent_ + instance is terminated as well; or it can be terminated separately, while the parent instance stays alive. +- `usingNested` is the same as the `createNested` method but behaves like the `Closeable.use` pattern. + +See also: +- `Lifetime.Eternal` is a global `Lifetime` instance that is never terminated. +- `Lifetime.Terminated` is a global `Lifetime` instance that has been already terminated. +- `status` — find more details in the +[LifetimeStatus.kt](https://github.com/JetBrains/rd/blob/9b806ccc770515f6288c778581c54607420c16a7/rd-kt/rd-core/src/main/kotlin/com/jetbrains/rd/util/lifetime/LifetimeStatus.kt) class from the Rd repository. There are three + convenient methods: `IsAlive`, `IsNotAlive`, `IsTerminated`. + +### `LifetimeDefinition` + +`LifetimeDefinition` instances have the `terminate` method that terminates a `Lifetime` instance and invokes all +the registered callbacks. If multiple concurrent terminations occur, the method may sometimes return before +executing all the callbacks because some other thread executes them. + +## Rd entities + +Rd is a lightweight reactive one-to-one RPC protocol, which is cross-language as well as cross-platform. It can +work on the same or different machines via the Internet. + +These are some Rd entities: +- `Protocol` encapsulates the logic of all Rd communications. All the entities should be bound to `Protocol` before + being used. `Protocol` contains `IScheduler`, which executes a _runnable_ instance on a different thread. +- `RdSignal` is an entity allowing one to **fire and forget**. You can add a callback for every received message + via the `advise(lifetime, callback)` method. There are two interfaces: `ISink` that only allows advising for + messages and `ISignal` that can also `fire` events. There is also a `Signal` class with the same behavior + but without remote communication. + +**Important:** if you `advise` and `fire` from the same process, your callback receives _not only_ +messages from the other process, but also the ones you `fire`. + +- `RdProperty` is a stateful property. You can get the current value and advise the callback — an advised + callback is executed on a current value and every change. +- `RdCall` is the remote procedure call. + +There are `RdSet`, `RdMap`, and other entities. + +An `async` property allows you to `fire` entities from any thread. Otherwise, you would need to do it from +the `Protocol.scheduler` thread: all Rd entities should be bound to the `Protocol` from the `scheduler` thread, or you +would get an exception. + +## `Rdgen` + +`Rdgen` generates custom classes and requests that can be bound to protocol and advised. There is a special model DSL +for it. + +### Model DSL + +Examples: +1. [Korifey](https://github.com/korifey/rd_example/blob/main/src/main/kotlin/org/korifey/rd_example/model/Root.kt) — + a simple one. +2. [Rider Unity plugin](https://github.com/JetBrains/resharper-unity/tree/net223/rider/protocol/src/main/kotlin/model) — a complicated one. + +First, you need to define a `Root` object: only one instance of each `Root` can be assigned to `Protocol`. + +There is a `Root` extension — `Ext(YourRoot)` — where you can define custom types and model entities. You can assign +multiple `Root` extensions to the `Protocol`. To generate the auxiliary structures, define them as direct fields. + +DSL: +- `structdef` is a structure with fields that cannot be bound to `Protocol` but can be serialized. This structure + can be `openstruct`, i.e. open for inheritance, and `basestruct`, i.e. abstract. Only `field` can be a member. +- `classdef` is a class that can be bound to a model. It can have `property`, `signal`, `call`, etc. + as members. It is possible to inherit: the class can be `openclass`, `baseclass`. +- `interfacedef` is provided to define interfaces. Use `method` to create a signature. + +You can use `extends` and `implements` to implement inheritance. + +_Note:_ `Rdgen` can generate models for C# and C++. Their structs and classes have different behavior. + +Rd entities — only in bindable models (`Ext`, `classdef`): +- `property` +- `signal` +- `source` +- `sink` +- `array` and `immutablelist` + +Useful properties in DSL entities: +- `async` — the same as `async` in Rd entities +- `docs` — provides KDoc/Javadoc documentation comments for the generated entity + +### Gradle + +[Example](https://github.com/korifey/rd_example/blob/main/build.gradle) + +`RdGenExtension` configures `Rdgen`. The properties are: +- `sources` — the folders with DSL `.kt` files. If there are no `sources`, scan classpath for the inheritors of `Root` + and `Ext`. +- `hashfile` — a folder to store the `.rdgen` hash file for incremental generation. +- `packages` — Java package names to search in toplevels, delimited by `,`. Example: `com.jetbrains.rd.model.nova,com, + org`. + +Configure model generation with the `RdGenExtension.generator` method: +- `root` — for which root this generator is declared. +- `namespace` — which namespace should be used in the generated source. In Kotlin, it configures the generated package + name. +- `directory` — where to put the generated files. +- `transform` — can be `symmetric`, `asis`, and `reversed`. It allows configuring of different model interfaces for + various client-server scenarios. _Note:_ in 99% of cases you should use `symmetric`. If you need another option, consult with someone. +- `language` — can be `kotlin`, `cpp` or `csharp`. + +## UnitTestBot project + +The `utbot-rd` Gradle project contains model sources in `rdgenModels`. You can find them at +[`utbot-rd/src/main/rdgen/org/utbot/rd/models`](../utbot-rd/src/main/rdgen/org/utbot/rd/models). + +### IDE process + +An _IDE process_ uses bundled JetBrains JDK. Code in `utbot-intellij` _**must**_ be compatible will all JDKs and plugin +SDKs, used by our officially supported Intellij IDEA versions. +See `sinceBuild` and `untilBuild` in [`utbot-intellij/build.gradle.kts`](../utbot-intellij/build.gradle.kts). + +The _IDE process_ starts the _Engine process_. The _IDE process_ keeps the `UtSettings` instance in memory and gets updates for it from Intellij IDEA. The other processes "ask" the _IDE process_ about settings via Rd RPC. + +### Engine process + +`TestCaseGenerator` and `UtBotSymbolicEngine` run here, in the _Engine process_. The process classpath contains all +the plugin JAR files (it uses the plugin classpath). + +The _Engine process_ _**must**_ run on the JDK that is used in the project under analysis. Otherwise, there will be +numerous problems with code analysis, `soot`, _Reflection_, and the divergence of the generated code Java API will occur. + +Currently, it is prohibited to run more than **one** generation process simultaneously (the limitation is related to +the characteristics of the native libraries). The process logging mechanism relies on +that fact, so UnitTestBot processes can exclusively write to a log file. + +The starting point in the _IDE process_ is the +[`EngineProcess`](../utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/process/EngineProcess.kt) class. +The _Engine process_ start file is +[`EngineProcessMain`](../utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt). +The _Engine process_ starts the _Instrumented process_. + +### Instrumented process + +The starting points in the _Engine process_ are the +[`InstrumentedProcess`](../utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentedProcess.kt) +and the [`ConcreteExecutor`](../utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt) +classes. The first one encapsulates the state, while the second one implements the request logic for concrete execution. + +The _Instrumented process_ runs on the same JDK as the _Engine process_ to prevent deviation from the _Engine process_. +Sometimes the _Instrumented process_ may unexpectedly die due to concrete execution. + +### Useful info + +1. If you need to use Rd, add the following dependencies: + ``` + implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: rdVersion + + implementation group: 'com.jetbrains.rd', name: 'rd-core', version: rdVersion + ``` +2. There are useful classes in `utbot-rd` to work with Rd and processes: + - `LifetimedProcess` binds a `Lifetime` instance to a process. If the process dies, the `Lifetime` instance + terminates, and vice versa. You can terminate the `Lifetime` instance manually — this will destroy the process. + - `ProcessWithRdServer` starts the Rd server and waits for the connection. + - `ClientProtocolBuilder` — you can use it in a client process to correctly connect to `ProcessWithRdServer`. +3. How `ProcessWithRdServer` communication works: + - Choose a free port. + - Create a client process and pass the port as an argument. + - Both processes create protocols, bind the model and setup callbacks. + - A server process cannot send messages until the _child_ creates a protocol (otherwise, messages are lost), so + the client process has to signal that it is ready. + - The client process creates a special file in the `temp` directory, which is observed by a _parent_ process. + - When the parent process spots the file, it deletes this file and sends a special message to the client process + confirming communication success. + - Only when the answer of the client process reaches the server, the processes are ready. +4. How to write custom RPC commands: + - Add a new `call` in a model, for example, in `EngineProcessModel`. + - Re-generate models: there are special Gradle tasks for this in the `utbot-rd/build.gradle` file. + - Add a callback for the new `call` in the corresponding start files, for example, in `EngineProcessMain.kt`. + - **Important**: do not add [`Rdgen`](https://mvnrepository.com/artifact/com.jetbrains.rd/rd-gen) as + an implementation dependency — it breaks some JAR files as it contains `kotlin-compiler-embeddable`. +5. Logging & debugging: + - [Interprocess logging](contributing/InterProcessLogging.md) + - [Interprocess debugging](./contributing/InterProcessDebugging.md) +6. Custom protocol marshaling types: do not spend time on it until `UtModels` get simpler, e.g. compatible with + `kotlinx.serialization`. + diff --git a/docs/ResultAndErrorHandlingApiOfTheInstrumentedProcess.md b/docs/ResultAndErrorHandlingApiOfTheInstrumentedProcess.md new file mode 100644 index 0000000000..e605f8c1e6 --- /dev/null +++ b/docs/ResultAndErrorHandlingApiOfTheInstrumentedProcess.md @@ -0,0 +1,92 @@ +# Instrumented process API: handling errors and results + +In UnitTestBot Java, there are three processes: +* IDE process +* Engine process +* Instrumented process + +The IDE process launches the plugin so a user can request test generation. +Upon the user request, the Engine process is initiated — it is responsible for the input values generation. + +Here, in the Engine process, there is a `ConcreteExecutor` class, +conveying the generated input values to the `InstrumentedProcess` class. +The `InstrumentedProcess` class creates the third physical process — +the Instrumented process that runs the user functions concretely with the provided input values +and returns the execution result. + +A _client_ is an object that uses the `ConcreteExecutor` directly — it works in the Engine process as well. + +`ConcreteExecutor` expects an `Instrumentation` object, which is responsible for, say, mocking static methods. In UnitTestBot Java, we use `UtExecutionInstrumentation` that implements the `Instrumentation` interface. + +Basically, if an exception occurs in the Instrumented process, +it is rethrown to the client object in the Engine process via Rd. + +## Concrete execution outcomes + +`ConcreteExecutor` is parameterized with `UtExecutionInstrumentation`. When the `ConcreteExecutor::executeAsync` method is called, it leads to one of the three possible outcomes: + +* `InstrumentedProcessDeathException` + +Some errors lead to the instant termination of the Instrumented process. + Such errors are wrapped in `InstrumentedProcessDeathException`. + Prior to processing the next request, the Instrumented process is restarted automatically, though it can take time. +`InstrumentedProcessDeathException` means that there is an Instrumented process internal issue. +Nonetheless, this exception is handled in the Engine process. + +* `InstrumentedProcessError` + +Errors that do not cause the Instrumented process termination are wrapped in `InstrumentedProcessError`. + The process is not restarted, so client's requests will be handled by the same process. + We believe that the Instrumented process state is consistent but in some tricky situations it _may be not_. + These situations should be reported as bugs. +`InstrumentedProcessError` also means +that there is an Instrumented process internal issue that should be handled by the client object +(in the Engine process). +The issue may occur because the client provides the wrong configuration or parameters, +but the Instrumented process cannot exactly determine what's wrong with the client's data: +one can find a description of the phase the exception has been thrown from. + +* `UtConcreteExecutionResult` + +If the Instrumented process performs well, +or something is broken but the Instrumented process knows exactly what is wrong with the input, `UtConcreteExecutionResult` is returned. +The Instrumented process guarantees that the state is _consistent_. +A `UtConcreteExecutionResult::result` field helps to find the exact reason for a failure: +* `UtSandboxFailure` — permission violation; +* `UtTimeoutException` — test execution time exceeds the provided time limit (`UtConcreteExecutionData::timeout`); +* `UtExecutionSuccess` — successful test execution; +* `UtExplicitlyThrownException` — explicitly thrown exception for a target method (via `throw` instruction); +* `UtImplicitlyThrownException` — implicitly thrown exception for a target method (`NPE`, `OOB`, etc., or an exception thrown inside the system library). + +## Error handling implementation + +The pipeline of `UtExecutionInstrumentation::invoke` includes 6 phases: +1. `ValueConstructionPhase` — constructs values from the models; +2. `PreparationPhase` — prepares statics, etc.; +3. `InvocationPhase` — invokes the target method; +4. `StatisticsCollectionPhase` — collects coverage and execution-related data; +5. `ModelConstructionPhase` — constructs the result models from the heap objects (`Any?`); +6. `PostprocessingPhase` — restores statics, clears mocks, etc. + +Each phase can throw two kinds of exceptions: +- `ExecutionPhaseStop` — indicates that the phase tries to stop the invocation pipeline completely because it already has a result. The returned result is the `ExecutionPhaseStop::result` field. +- `ExecutionPhaseError` — indicates that an unexpected error has occurred during the phase execution, and this error is rethrown to the Engine process. + +`PhasesController::computeConcreteExecutionResult` then matches on the exception type: +* it rethrows the exception if the type is `ExecutionPhaseError`, +* it returns the result if type is `ExecutionPhaseStop`. + +## Timeout + +Concrete execution is limited in time: the `UtExecutionInstrumentation::invoke` method is subject to timeout as well. + +For `UtExecutionInstrumentation` in the Instrumented process, we wrap the phases that can take a long time with the `executePhaseInTimeout` block. +This block tracks the elapsed time. +If a phase wrapped with this block exceeds the timeout, it returns `TimeoutException`. + +One cannot be sure that the cancellation request immediately breaks the invocation pipeline inside the Instrumented process. +Invocation is guaranteed to finish within timeout. +It may or _may not_ finish earlier. +The request that has been sent to the Instrumented process is _uncancellable_ by design. + +Even if the `TimeoutException` occurs, the Instrumented process is ready to process the new requests. \ No newline at end of file diff --git a/docs/Sandboxing.md b/docs/Sandboxing.md new file mode 100644 index 0000000000..15d2961146 --- /dev/null +++ b/docs/Sandboxing.md @@ -0,0 +1,110 @@ +# Sandboxing tests with Java Security Manager + +## What is sandboxing? + +Sandboxing is a security technique to find unsafe code fragments and prevent them from being executed. + +What do we mean by "unsafe code" in Java? The most common forbidden actions are: + +* working with files (read, write, create, delete), +* connecting to [sockets](https://github.com/UnitTestBot/UTBotJava/issues/792), +* invoking `System.exit()`, +* accessing system properties or JVM properties, +* using reflection. + +## Why do we need sandboxing for test generation? + +During test generation, UnitTestBot executes the source code with the concrete values. All the fuzzer runs require +concrete execution and some of the symbolic execution processes invoke it as well. If the source code contains +potentially unsafe operations, executing them with the concrete values may lead to fatal errors. It is safer to catch +these operations and break the concrete execution process with `AccessControlException` thrown. + +## What do the sandboxed tests look like? + +When the source code fragments are suspicious and the corresponding test generation processes are interrupted, the tests with the `@Disabled` annotation and a stack trace appear in the output: + + public void testPropertyWithBlankString() { + SecurityCheck securityCheck = new SecurityCheck(); + + /* This test fails because method [com.company.security.SecurityCheck.property] produces [java.security.AccessControlException: access denied ("java.util.PropertyPermission" " " "read")] + java.security.AccessControlContext.checkPermission(AccessControlContext.java:472) + java.security.AccessController.checkPermission(AccessController.java:886) + java.lang.SecurityManager.checkPermission(SecurityManager.java:549) + java.lang.SecurityManager.checkPropertyAccess(SecurityManager.java:1294) + java.lang.System.getProperty(System.java:719) + com.company.security.SecurityCheck.property(SecurityCheck.java:32) */ + } + +## How does UnitTestBot sandbox code execution? + +UnitTestBot for Java/Kotlin uses [Java Security Manager](https://docs.oracle.com/javase/tutorial/essential/environment/security.html) for sandboxing. In general, the Security Manager allows applications to implement a security policy. It determines whether an operation is potentially safe or not and interrupts the execution if needed. + +In UnitTestBot the **secure mode is enabled by default**: only a small subset of runtime permissions necessary for +test generation are given (e.g. fields reflection is permitted by default). To extend the list of permissions learn +[How to handle sandboxing](#How-to-handle-sandboxing). + +Java Security Manager monitors [all the code](https://github.com/UnitTestBot/UTBotJava/issues/791) for the risk of performing forbidden operations, including code in _class constructors, private methods, static blocks, [threads](https://github.com/UnitTestBot/UTBotJava/issues/895)_, and combinations of all of the above. + +## How to handle sandboxing + +You can **add permissions** by creating and editing the `~\.utbot\sandbox.policy` file. Find more about [Policy File and Syntax](https://docs.oracle.com/javase/7/docs/technotes/guides/security/PolicyFiles.html#Examples) and refer to the [Full list of permissions](https://docs.oracle.com/javase/1.5.0/docs/guide/security/spec/security-spec.doc3.html) to choose the proper approach. + +If the permission was added but somehow [not recognized](https://github.com/UnitTestBot/UTBotJava/issues/796), the UnitTestBot plugin will fail to start and generate no tests. + +If you are sure you want the code to be executed as is (**including the unsafe operations!**) you can **turn sandboxing off**: + +* You can add `AllPermission` to `~\.utbot\sandbox.policy`. Be careful! +* Alternatively, you can add `useSandbox=false` to `~\.utbot\settings.properties`. Create this file manually if you don't have one. Find [more information](https://github.com/UnitTestBot/UTBotJava/pull/857) on how to manage sandboxing to test the UnitTestBot plugin itself. + +It is reasonable to regard the `@Disabled` tests just as supplemental information about the source code, not as the tests for actual usage. + +## How to improve sandboxing + +For now there are several unsolved problems related to sandboxing in UnitTestBot: + +1. We need to replace Java Security Manager with our own tool. + + [Java Security Manager is deprecated since JDK 17](https://openjdk.org/jeps/411) and is subject to removal in some + future version. It is still present in JDK 19 but with limited functionality. E.g., in Java 18, a Java application or library is prevented from dynamically installing a Security Manager unless the end user has explicitly opted to allow it. Obviously, we cannot rely upon the deprecated tool and need to create our own one. + + +2. We need to provide a unified and readable description for disabled tests. + + UnitTestBot supports three testing frameworks and their annotations are slightly different: + +JUnit 4: `@Ignore("")` + +JUnit 5: `@Disabled("")` + +TestNG: `@Ignore` as an alternative to `@Test(enabled=false)` + +* How should we unify these annotations? +* How should we show info in Javadoc comments? +* Do we need to print a stack trace? + + +3. We need to add emulation for restricted operations (a kind of mocks) + + Emulating unsafe operations will allow UnitTestBot to generate useful tests even for the sandboxed code and run them instead of disabling. + +4. We need to provide a user with the sandboxing settings. + + The UnitTestBot plugin UI provides no information about configuring the behavior of Security Manager. Information on [How to + handle +sandboxing](#How-to-handle-sandboxing) is available only on GitHub. + +* Should we add Sandboxing (or Security Manager) settings to plugin UI? E.g.: **File operations: Forbidden / Allowed / Emulated in sandbox**. + +* Should we add a hyperlink to a piece of related documentation or to the `~\.utbot\sandbox.policy` file? + +## How to test sandboxing + +See the [short manual testing scenario](https://github.com/UnitTestBot/UTBotJava/pull/625) and the [full manual testing checklist](https://github.com/UnitTestBot/UTBotJava/issues/790). + +## Related links + +Initial feature request: [Add SecurityManager support to block suspicious code #622](https://github.com/UnitTestBot/UTBotJava/issues/622) + +Pull request: [Add SecurityManager support to block suspicious code #622 #625](https://github.com/UnitTestBot/UTBotJava/pull/625) + +Improvement request: [Improve sandbox-relative description in generated tests #782](https://github.com/UnitTestBot/UTBotJava/issues/782) \ No newline at end of file diff --git a/docs/SettingsProperties.md b/docs/SettingsProperties.md new file mode 100644 index 0000000000..3cc80ef60a --- /dev/null +++ b/docs/SettingsProperties.md @@ -0,0 +1,86 @@ +# UnitTestBot settings + +First, let's define "**settings**" as the set of "**properties**". +Each property is a _key=value_ pair, and it may be represented as source code or plain text stored in a file. +This property set affects the key aspects of UnitTestBot behavior. + +UnitTestBot is available as +- an IntelliJ IDEA plugin, +- a continuous integration (CI) tool, +- a command-line interface (CLI). + +These three application types have low-level _**core**_ settings. The plugin also has per-project _**plugin-specific**_ settings. + +## Core settings + +Core settings are persisted in the **_settings file_**: `{userHome}/.utbot/settings.properties`. This file is designed for reading only. + +The defaults for the core settings are provided in source code (namely in `UtSettings.kt`) so the file itself may be absent or exist with a few of the customized properties only. For example, a file with just one line like `utBotGenerationTimeoutInMillis=15000` is valid and useful. + +## Plugin-specific settings + +IDE persists the plugin-specific settings automatically (per project!) in the **plugin configuration file**: `{projectDir}/.idea/utbot-settings.xml`. Nobody is expected to edit this file manually. + +At the moment, the core and plugin-specific settings have very small intersection (i.e. the keys of different levels control the same behavior aspects). +If they still intersect, the core settings should provide the defaults for the plugin-specific settings. +As for now, this concept is partially implemented. + +## Property categories + +A developer usually represents the new feature settings as a subset of properties and has to choose the proper "level" for them. In practice, we have these property categories: + +- **Hardcoded directly as constants in source code** +_One can build the plugin with their own hardcoded preset._ +- **Experimental or temporary** +_These properties can disappear from the settings file or jump back to the hardcoded constants. We do not expect the end user to change these properties, but it is still possible to specify them in the settings file._ +- **Engine-level tuning with reasonable defaults** +_Designed for low-level tuning during contests, etc._ +- **Rarely used, good to be changed once per PC** +- **Project-level setup in IDE** +_The end user can change them via **File** > **Settings** > **Tools** > **UnitTestBot**_. +- **Small set of per-generation options** + +Thereby, some properties can be considered as public API while the rest are pretty "internal". + +The end user has three places to change UnitTestBot behavior: +1. Settings file, which is PC-wide — read by all the UnitTestBot instances across PC. +For example, CLI and two different projects in IDE will re-use it. +2. Plugin settings UI (**File** > **Settings** > **Tools** > **UnitTestBot**). +3. Controls in the **Generate Tests with UnitTestBot window** dialog. + +Properties from the plugin settings UI and the dialog are plugin-specific, and they are automatically persisted in `{projectDir}/.idea/utbot-settings.xml`. _Note:_ only non-default values are stored here. + +## Configuring UnitTestBot with auto-generated `settings.properties` + +Common users usually change UnitTestBot settings via UI: +* in the **Generate Tests with UnitTestBot** dialog, +* via **File** > **Settings** > **Tools** > **UnitTestBot**. + +Advanced users and contributors require advanced settings. + +### How to configure advanced settings: motivation to improve + +Advanced settings were not visible in UnitTestBot UI and were configurable only via `settings.properties`. +UnitTestBot did not provide this file by default, so you had to create it manually in your `{home}/.utbot` directory. +You could configure advanced settings here if you knew available options — they are listed in UnitTestBot source code, +namely, `UtSettings.kt`. As UnitTestBot is a developing product, it often gets new features and new settings +that UnitTestBot users sometimes are not aware of. + +### Implemented `settings.properties` improvements + +Currently, UnitTestBot generates a template `settings.properties` file with the up-to-date list of available setting +options, corresponding default values, and explicit descriptions for each option. + +This template file is auto-generated on the basis of `UtSettings.kt` doc comments. The template file consists of +the commented lines, so you can uncomment the line to enable the setting or easily get back to defaults. + +_Idea to be implemented: we can annotate properties in UtSettings.kt as @api to provide the template file with a narrow subset of properties._ + +Generating `settings.properties` is a part of a Gradle task in IntelliJ IDEA. The `settings.properties` file is +bundled with the published UnitTestBot plugin as a top-level entry inside the `utbot-intellij-{version}.jar` file. + +Upon IntelliJ IDEA start, the UnitTestBot plugin loads its settings and checks whether the template setting file exists +in the local file system as `{home}/.utbot/settings.properties`: +* If there is no such file, it is created (along with the hidden `{home}/.utbot` directory if needed). +* An existing file is updated with new settings and corresponding info if necessary. +* UnitTestBot does not re-write `settings.properties` if the file exists and has already been customized. \ No newline at end of file diff --git a/docs/SpeculativeFieldNonNullability.md b/docs/SpeculativeFieldNonNullability.md index 3ebcce6d08..0cd70fd67f 100644 --- a/docs/SpeculativeFieldNonNullability.md +++ b/docs/SpeculativeFieldNonNullability.md @@ -16,19 +16,31 @@ is desirable, as it increases the coverage, but it has a downside. It is possibl 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. +of the class. + +- First of all, this is particularly true for final fields, especially in system classes. it is also often true for non-public fields from standard library and third-party libraries (even setters often do not allow `null` values). Automatically generated tests assign `null` values to fields 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. +- After that, field may be declared with some annotation that shows that null value is actually impossible. +For example, in Spring applications `@InjectMocks` and `@Mock` annotations on the fields of class under test +mean that these fields always have value, so `NPE` branches for them would never occur in real code. + + ## 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 and non-public -fields of system classes, as they are usually correctly initialized and are not equal `null`. +do not have an explicit `@NotNull` annotation. + +- In particular, we can use this approach to final and non-public +fields of system classes, as they are usually correctly initialized and are not equal `null` +- For Spring application, we use this approach for the fields of class +under test not obtained from Spring bean definitions -At the same time, we can't always add the "not null" hard constraint for the field: it would break +At the same time, for non-Spring classes, +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. diff --git a/docs/StaticInitializersAnalysis.md b/docs/StaticInitializersAnalysis.md new file mode 100644 index 0000000000..7c0507201f --- /dev/null +++ b/docs/StaticInitializersAnalysis.md @@ -0,0 +1,73 @@ +# Symbolic analysis of static initializers + +## Problem + +Before the [Prohibit to set static fields from library classes](https://github.com/UnitTestBot/UTBotJava/pull/699) +change was implemented, every static field outside the `` block (the so-called _meaningful_ static fields) +was stored in `modelBefore` and `modelAfter`. These _meaningful_ static fields were set (and reset for test isolation) during code generation. This led to explicit static field initializations, which looked unexpected for a user. For example, an `EMPTY` static field from the `Optional` class might be set for the following method under test + +```java +class OptionalEmptyExample { + public java.util.Optional optionalExample(boolean isEmpty) { + return isEmpty ? java.util.Optional.empty() : java.util.Optional.of(42); + } +} +``` + +like: + +```java +setStaticField(optionalClazz, "EMPTY", empty); +``` + +**Goal**: we should not set such kind of static fields with initializers. + +## Current solution + +Having merged [Prohibit to set static fields from library classes](https://github.com/UnitTestBot/UTBotJava/pull/699) +, we now do not explicitly set the static fields of the classes from the so-called _trusted_ libraries (by default, +they are JDK packages). This behavior is guided by the `org.utbot.framework. +UtSettings#getIgnoreStaticsFromTrustedLibraries` setting. Current solution possibly **leads to coverage regression** +and needs to be investigated: [Investigate coverage regression because of not setting static fields](https://github.com/UnitTestBot/UTBotJava/issues/716). +So, take a look at other ways to fix the problem. + +## Alternative solutions + +### Use concrete values as soft constraints _(not yet implemented)_ + +The essence of the problem is assigning values to the static fields that should be set at runtime. To prevent it, +we can try to create models for the static fields according to their runtime values and filter out the static fields +that are equal to runtime values, using the following algorithm: + +1. Extract a concrete value for a static field. +2. Create `UtModel` for this value and store it. +3. Transform the produced model to soft constraints. +4. Add them to the current symbolic state. +5. Having resolved `stateBefore`, compare the resulting `UtModel` for the static field with the stored model and then drop the resulting model from `stateBefore` if they are equal. + +### Propagate information on the read static fields _(not yet implemented)_ + +We can define the _meaningful_ static fields in a different way: we can mark the static fields as _meaningful_ if only they affect the method-under-test result. To decide if they do: + +- find out whether a given statement reads a specific static value or not and store this info, +- while traversing the method graph, propagate this stored info to each of the following statements in a tree, +- upon reaching the `return` statement of the method under test, mark all these read static fields as _meaningful_. + +### Filter out static methods: check if they affect `UtExecution` _(not yet implemented)_* +Having collected all executions, we can analyze them and check whether the given static field affects the result of a current execution. Changing the static field value may have the same effect on every execution or no effect at all. It may also be required as an entry point during the executions (e.g., an _if_-statement as the first statement in the method under test): + +```java +class AlwaysThrowingException { + public void throwIfMagic() { + if (ClassWithStaticField.staticField == 42) { + throw new RuntimeException("Magic number"); + } + } +} + +class ClassWithStaticField { + public final static int staticField = 42; +} +``` + +*This solution should only be used with the [propagation](#propagate-information-on-the-read-static-fields) solution. \ No newline at end of file diff --git a/docs/Summarization module.md b/docs/Summarization module.md new file mode 100644 index 0000000000..b9b3d6cab6 --- /dev/null +++ b/docs/Summarization module.md @@ -0,0 +1,80 @@ +# Summarization module + +## Overview + +UnitTestBot minimizes the number of tests so that they are necessary and sufficient, but sometimes there are still a lot of them. Tests may look very similar to each other, and it may be hard for a user to distinguish between them. To ease test case comprehension, UnitTestBot generates summaries, or human-readable test descriptions. Summaries also facilitate navigation: they structure the whole collection of generated tests by clustering them into groups. + +Summarization module generates detailed meta-information: +- test method names +- testing framework annotations (including `@DisplayName`) +- Javadoc comments for tests +- cluster comments for groups of tests (_regions_) + +Javadoc comments can be rendered in two styles: as plain text or in a special format enriched with the [custom Javadoc tags](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/summaries/CustomJavadocTags.md). + +If the summarization process fails due to an error or insufficient information, then the test method receives a unique name and no meta-information. + +The whole summarization subsystem is located in the `utbot-summary` module. + +## Implementation + +At the last stage of test generation process, the `UtMethodTestSet.summarize` method is called. +As input, this method receives the set of `UtExecution` models with empty `testMethodName`, `displayName`, and `summary` fields. It fills these fields with the corresponding meta-information, groups the received `UtExecution` models into clusters and generates cluster names. + +Currently, there are three main `UtExecution` implementations: +* `UtSymbolicExecution`, +* `UtFailedExecution`, +* `UtFuzzedExecution`. + +To construct meta-information for the `UtFuzzedExecution` models, the summarization module uses method parameters with their values and types as well as the return value type. To generate summaries for each `UtSymbolicExecution`, it uses the symbolic code analysis results. + +Let's describe this process in detail for `UtSymbolicExecution` and `UtFuzzedExecution`. + +### Constructing meta-information for `UtSymbolicExecution` + +1. **Producing _Jimple statements_.** + For each method under test (or MUT), the symbolic execution engine generates `UtMethodTestSet` consisting of `UtExecution` models, i.e. a test suite consisting of unit tests. A unit test (or `UtExecution`) in this suite is a set of execution steps that traverses a particular path in the MUT. An execution `Step` contains info on a statement, the depth of execution step and an execution decision. +* A statement (`stmt`) is a Jimple statement, provided with the [Soot](https://github.com/soot-oss/soot) framework. A Jimple statement is a simplified representation of the Java program that is based on the three-address code. The symbolic engine accepts Java bytecode and transforms it to the Jimple statements for the analytical traversal of execution paths. +* The depth of execution step (`depth`) depicts an execution depth of the statement in a call graph where the MUT is a root. +* An execution decision (`decision`) is a number indicating the execution direction inside the control flow graph. If there are two edges coming out of the execution statement in the control flow graph, a decision number shows what edge is chosen to be executed next. + +2. **_Tagging_.** + For each pair of `UtMethodTestSet` and its source code file, the summarization module identifies unique execution steps, recursions, iteration cycles, skipped iterations, etc. These code constructs are marked with tags or meta-tags, which represent the execution paths in a structural view. The summarization module uses these tags directly to create meta-information, or summaries. + +At this moment, the summarization module is able to assign the following tags: +- Uniqueness of a statement: + - _Unique_: no other execution path in the cluster contains this step, so only one execution triggers this statement in its cluster. + - _Common_: all the paths execute these statements. + - _Partly Common_: only some executions in a cluster contain this step. +- The decision in the CFG (branching): _Right_, _Left_, _Return_ +- The number of statement executions in a given test +- Dealing with loops: _starting/ending an iteration_, _invoking the recursion_, etc. + +We use our own implementation of the [DBSCAN](https://en.wikipedia.org/wiki/DBSCAN) clustering algorithm with the non-euclidean distance measure based on the Minimum Edit Distance to identify _unique_, _common_ and _partly common_ execution steps. Firstly, we manually divided execution paths into groups: +- successfully executed paths (only this group is clustered into different regions with DBSCAN) +- paths with expected exceptions +- paths with unexpected exceptions +- other groups with errors and exceptions based on the given `UtResult` + +3. **Building _sentences_.** + _Sentences_ are the blocks for the resulting summaries. + To build the _sentence_, the summarization module +- parses the source file (containing the MUT) using [JavaParser](https://javaparser.org/) to get AST representations; +- maps the AST representations to Jimple statements (so each statement is mapped to AST node); +- builds the _sentence_ blocks (to provide custom Javadoc tags or plain-text mode); +- builds the _final sentence_ (valid for plain-text mode only); +- generates the `@DisplayName` annotation and test method names using the following rule: find the last _unique_ statement in each path (preferably, the condition statement) that has been executed once (being satisfied or unsatisfied); then the AST node of this statement is used for naming the execution; +- builds the cluster names based on the _common_ execution paths. + +### Constructing meta-information for `UtFuzzedExecution` + +For `UtFuzzedExecution`, meta-information is also available as test method names, `@DisplayName` annotations, Javadoc comments, and cluster comments. + +The difference is that clustering tests for `UtFuzzedExecution` is based on `UtResult`. No subgroups are generated for the successfully completed tests. + +The algorithm for generating meta-information is described in the `ModelBasedNameSuggester` class, which is the registration point for `SingleModelNameSuggester` interface. This interface is implemented in `PrimitiveModelNameSuggester` and `ArrayModelNameSuggester`. + +Depending on the received `UtExecutionResult` type, `ModelBasedNameSuggester` produces the basic part of the method name or the `@DisplayName` annotation. `UtFuzzedExecution` provides `FuzzedMethodDescription` and `FuzzedValue` that supplement the generated basic part for test name with information about the types, names and values of the MUT parameters. + +_Note:_ test method names and `@DisplayName` annotations are generated if only the number of MUT parameters is no more than three, otherwise they are not generated. + diff --git a/docs/TaintAnalysis.md b/docs/TaintAnalysis.md new file mode 100644 index 0000000000..67c4db3747 --- /dev/null +++ b/docs/TaintAnalysis.md @@ -0,0 +1,527 @@ +# Taint analysis + +## Introduction to the technique + +Taint analysis allows you to track the propagation of unverified external data through the program. +If this kind of data reaches critical code sections, it may lead to vulnerabilities, including SQL injections, +cross-site scripting (XSS) and others. +Attackers can use these vulnerabilities to disrupt correct system operation, get confidential data, or perform other unauthorized operations. +Taint analysis helps to find these mistakes at the compilation stage. + +The key idea of the approach is that any variable an external user can change stores a potential security +threat. If the variable is used in some expression, then the value of this expression also becomes suspicious. +The algorithm tracks situations when the variables marked as suspicious are used in dangerous +command execution, for example, in direct queries to a database or an operating system. + +Taint analysis requires a configuration, where you assign one of the following +roles to each method in a program. + +- Taint source — a source of unverified data. + For example, it can be a method for reading user input, or a method for getting a parameter of an incoming HTTP + request. + The Taint source execution result is marked. The method semantics determines the mark to be applied + to the variable. + The name of the mark can be completely arbitrary, since it is chosen by the one who writes the + configuration. + For example, according to configuration, the `getPassword()` method marks its return value with a "sensitive-data" + mark. +- Taint pass — a function that marks the return value taking into account the marks in its arguments. + Depending on the implementation, marks can be applied not only to method results but also to a `this` object, and to + input parameters. For example, you can configure the `concat(String a, String b)` concatenation method + to mark its result with all the marks from `a` and `b`. +- Taint cleaner — a function that removes a given set of marks from the passed arguments. + Most often, this is a kind of validation method that verifies that the user has entered data in the expected + format. + For example, the `validateEmail(String email)` method removes the "XSS" mark upon successful check completion, + because no unverified data in the `email` object that can now lead to cross-site + scripting vulnerability. +- Taint sink — a receiver, a critical section of an application. + It can be a method that accesses a database or a file system directly, or perform other potentially dangerous + operations. + For the Taint sink, you can set a list of marks. Variables with specified marks should not leak into this taint sink. + For example, if a value marked as "sensitive-data" is passed to a logging function, which prints its arguments + directly to the console, then this is a developer mistake, since data leaks. + +The taint analysis algorithm scans the data flow graph, trying to detect a route between a method from a set of Taint +sources and a method from Taint sinks. The UnitTesBot taint analysis feature is implemented inside the symbolic engine +to avoid a large number of false positives. + +**Example** + +Consider an example of a simple function with an SQL injection vulnerability inside: +if an attacker enters the string `"name'); DROP TABLE Employees; --"` into the variable name, then it will be possible +to delete the `Employees` table with the data in it. + +```java +class Example { + void example(Connection connection) { + Scanner sc = new Scanner(System.in); + String name = sc.nextLine(); + Statement stmt = connection.createStatement(); + String query = "INSERT INTO Employees(name) VALUES('" + .concat(name) + .concat("')"); + stmt.executeUpdate(query); + } +} +``` + +For taint analysis, you have to set the configuration. + +- Taint source is a `java.util.Scanner.nextLine` method that adds a "user-input" mark to the returned value. +- Taint pass is a `java.lang.String.concat` method that passes the "user-input" marks through itself received + either from the first argument or from the object on which this method is called (`this`). +- Taint sink is a `java.sql.Statement.executeUpdate` method that checks variables marked as "user-input". + +Any correct implementation of the taint analysis algorithm should detect a mistake in this code: the variable with +the "user-input" mark is passed to `executeUpdate` (the sink). + +Note that the algorithm is not responsible for detecting specific data that an attacker could +enter to harm the program. It only discovers the route connecting the source and the sink. + +## UnitTestBot implementation + +No unified configuration format is provided for taint analysis in the world, and all static analyzers describe their +own way of configuration. Thus, we provide a custom configuration scheme to describe the rules: sources, passes, +cleaners, and sinks. + +### Configuration: general structure + +The general structure of the configuration format (based on YAML) is presented below. + +```yaml +sources: + - + - + - <...> +passes: + - + - <...> +cleaners: + - + - <...> +sinks: + - + - <...> +``` + +That is, these are just lists of rules related to a specific type. + +Each rule has a set of characteristics. + +- The unique identifier of the method that this rule describes. + It consists of the method's full name, including the package name and the class name, + as well as the signature of the method — a set of argument types (the `signature` key in the YAML file). +- Some `conditions` that must be met during execution for the rule to work. +- A set of `marks` that the rule uses. +- A set of specific mark management actions that occur when the rule is triggered (`add-to`, `get-from`, + `remove-from`, or `check`, depending on the semantics of the rule). + +For example, the rule for the taint source can look like this. + +```yaml +com.abc.ClassName.methodName: + signature: [ , _, ] + conditions: + arg1: + not: [ -1, 1 ] + add-to: [ this, arg2, return ] + marks: user-input +``` + +This rule is defined for a method named `methodName` from the `ClassName` class located in the `com.abc` package. +The method takes exactly 3 arguments: the first one has the `int` type, the second can be anything, +and the last one has the `java.lang.Object` type. +The `signature` key may not be specified, then any `methodName` overload is appropriate. + +The rule is triggered when the first argument (`arg1`) is not equal to either -1 or 1 as specified by the `conditions` +key (the list is interpreted as logical OR). +This parameter is optional, if it is absent, no conditions are checked. + +The described source adds a "user-input" mark to the variables corresponding to `this`, `arg2` and `return`: +to the `ClassName` class object on which `methodName` is called, to the second argument of the function +and to the return value. Moreover, the `add-to` and `marks` keys can contain both a list, and a single value. + +The other rule types have the same syntax as the source, except for the `add-to` key. + +- Taint pass transfers marks from one set of objects to another, so two keys are defined for it: + `get-from` and `add-to`, respectively. The marks specified in `marks` are added on `add-to` if there is a mark in + `get-from`. +- Taint cleaner removes marks from objects, so its key is called `remove-from`. +- Taint sink checks for the marks in variables, which locates under the `check` key. + +### Configuration: details + +Fully qualified method names can be written in one line or using nested structure: the package name is specified first, +then the class name appears, and finally, there is the method name itself. + +```yaml +- com.abc.def.Example.example: ... +``` + +or + +```yaml +- com: + - abc.def: + - Example.example: ... +``` + +Note that regular expressions in names are not supported yet. + +The `add-to`, `get-from`, `remove-from`, and `check` fields specify the objects (or entities) to be marked. +You can specify only one value here or a whole list. + +Possible values are: + +- `this` +- `arg1` +- `arg2` +- ... +- `return` + +The user can define arbitrary mark names or specify an empty list (`[]`) for all possible marks. + +```yaml +passes: + - java.lang.String.concat: + get-from: this + add-to: return + marks: [ user-input, sensitive-data, my-super-mark ] +``` + +or + +```yaml +passes: + - java.lang.String.concat: + get-from: this + add-to: return + marks: [ ] # all possible marks +``` + +To check the conformity to `conditions`, you can set: + +- the specific values of method arguments +- their runtime types + +Values can be set for the following types: `boolean`, `int`, `float` or `string` (and `null` value for all nullable +types). + +The full type name must be specified in the angle brackets `<>`. + +The `conditions` field specifies runtime conditions for arguments (`arg1`, `arg2`, ...). +Conditions can also be specified for `this` and `return` if it makes sense. +For sinks, checking conditions for a return value makes no sense, so this functionality is not supported. + +```yaml +conditions: + this: # this should be java.lang.String + arg1: "test" # the first argument should be equal to "test" + arg2: 227 # int + arg3: 227.001 # float + arg4: null # null + return: true # return value should be equal to `true` +``` + +Values and types can be negated using the `not` key, as well as combined using lists (`or` semantics). + +Nesting is allowed. + +```yaml +conditions: + this: [ "in", "out" ] # this should be equal to either "in" or "out" + arg1: [ , ] # arg1 should be int or float + arg2: { not: 0 } # arg2 should not be equal to 0 + arg3: + not: [ 1, 2, 3, 5, 8 ] # should not be equal to any of the listed numbers + arg4: [ "", { not: } ] # should be an empty string or not a string at all +``` + +If several rules are suitable for one method call, they are all applied. + +**Overall example** + +```yaml +sources: + - com: + - abc: + - method1: + signature: [ _, _, ] + add-to: return + marks: xss + - method1: + signature: [ , _, ] + add-to: [ this, return ] + marks: sql-injection + - bca.method2: + conditions: + this: + not: "in" + add-to: return + marks: [ sensitive-data, xss ] + +passes: + - com.abc.method2: + get-from: [ this, arg1, arg3 ] + add-to: return + marks: sensitive-data + - org.example.method3: + conditions: + arg1: { not: "" } + get-from: arg1 + add-to: [ this, return ] + marks: sql-injection + +cleaners: + - com.example.pack.method7: + conditions: + return: true + remove-from: this + marks: [ sensitive-data, sql-injection ] + +sinks: + - org.example: + - log: + check: arg1 + marks: sensitive-data + - method17: + check: [ arg1, arg3 ] + marks: [ sql-injection, xss ] +``` + +**Usage examples** + +`java.lang.System.getenv` is a source of the “environment” mark. There are two overloads of this method: with one string +parameter and no parameters at all. We want to describe only the first overload: + + ```yaml + sources: + - java.lang.System.getenv: + signature: [ ] + add-to: return + marks: environment + ``` + +`java.lang.String.concat` is a pass-through only if `this` is marked and not equal to `""`, or if `arg1` is marked and +not equal to `""`: + + ```yaml + passes: + - java.lang.String: + - concat: + conditions: + this: { not: "" } + get-from: this + add-to: return + marks: sensitive-data + - concat: + conditions: + arg1: { not: "" } + get-from: arg1 + add-to: return + marks: sensitive-data + ``` + +If you want to define a `+` operator for strings as taint pass, you should write the following rules: + +```yaml +passes: + - java.lang.StringBuilder.append: + get-from: arg1 + add-to: this + marks: [ ] + - java.lang.StringBuilder.toString: + get-from: this + add-to: return + marks: [ ] +``` + +`java.lang.String.isEmpty` is a cleaner only if it returns `true`: + + ```yaml + cleaners: + - java.lang.String.isEmpty: + conditions: + return: true + remove-from: this + marks: [ sql-injection, xss ] + ``` + +Suppose that the `org.example.util.unsafe` method is a sink for the “environment” mark if the second argument is +an `Integer` and equal to zero: + + ```yaml + sinks: + - org.example.util.unsafe: + signature: [ _, ] + conditions: + arg2: 0 + check: arg2 + marks: environment + ``` + +The configuration above checks the type at compile time, but sometimes we want to check the type at runtime: + + ```yaml + sinks: + - org.example.util.unsafe: + conditions: + arg2: + not: [ { not: 0 }, { not: } ] + check: arg2 + marks: environment + ``` + +Perhaps explicit `and` for `conditions` will be added in the future. + +### Algorithm implementation details + +The main idea of the implemented approach is that each symbolic variable is associated with a taint vector — a 64-bit +value, where each `i` bit is responsible for the presence of a mark with the number `i` in this object. +After that, during the symbolic execution, these mappings are maintained and updated in accordance with +the classical taint analysis algorithm. + +The implementation mostly affects the `Traverser` and `Memory` classes, as well as the new `TaintMarkRegistry` +and `TaintMarkManager` classes. The diagram below shows a high-level architecture of the taint module (in the actual +code, the implementation is a bit different, but to understand the idea, the diagram is greatly simplified). + + + +The `TaintMarkRegistry` class stores a mapping between the mark name and its ordinal number from 0 to 63. +The number of marks is limited to 64. However, firstly, this is enough for almost any reasonable example, +and secondly, the decision was made due to performance issues — operations with the `Long` data type are +performed much faster than if a bit array was used. + +The `TaintModel` component (data classes at `org.utbot.taint.model`) is responsible for providing access +to the configuration. In particular, it defines a way to convert conditions (the value of the `conditions` key +in a YAML document) into logical expressions over symbolic variables. + +`Memory` stores the values of the taint bit-vectors for symbolic variables. Only simple methods were implemented there +(functions to update vectors and get them at the address of a symbolic object). +All the complex logic of adding and removing marks, based on taint analysis theory, +was written in a separate `TaintMarkManager` class. In other words, this class wraps low-level memory work into +domain-friendly operations. + +The information about the marked variables is updated during the `Traverser` work. Before each `invoke()` +instruction corresponding to the method call in the user code, a special `Traverser. +processTaintSink` handler is called, and after the `invoke` instruction, the `Traverser.processTaintSource`, +`Traverser.processTaintPass` and `Traverser.processTaintCleaner` handlers are called. This order is set because all the +rules, except tose for the sinks, need the result of the function. At the same time, the fact of transferring +the tainted data occurs when launching the sink function, therefore, you can report the vulnerability +found even before it is executed. + +The listed rule handlers get the configuration and perform the taint analysis semantics. The `processTaintSink` method +requests information from the `TaintMarkManager` about the marks already set and adds constraints to the SMT +solver: the satisfiability corresponds to the defect detection. The other handlers modify the symbolic +`Memory` through the `TaintMarkManager`, adding and removing marks from the selected symbolic variables. + +### Code generator modification + +UnitTestBot produces unit tests (and the SARIF reports). `CodeGenerator` is launched on each +found test case, and generates the test (as Java code). Moreover, the test, which leads to throwing an unhandled exception, +should not pass. Taint analysis errors are not real from the language perspective, since they +are not real exceptions. However, we still have to highlight such tests as failed. The code generator was modified +so that an artificial error was added at the end of each test to ensure a fail (the same strategy +was used in the integer overflow detection). + +```java +fail("'java.lang.String' marked 'user-input' was passed into 'Example.example' method"); +``` + +The solution allows us to automatically integrate with the SARIF reports and to visualize the results +in the IntelliJ IDEA _Problems_ tool window. The found test case is treated as a real exception, +and all the necessary logic has already been written for them. + +**Example** + +Consider the code below. + +```java +public class User { + + String getLogin() { /* some logic */ } + + String getPassword() { /* some logic */ } + + String userInfo(String login, String password) { + return login + "#" + password; + } + + void printUserInfo() { + var login = getLogin(); + var password = getPassword(); + var info = userInfo(login, password); + System.out.println(info); + } +} +``` + +The `getPassword` method returns sensitive data that should never leak out of the application, but the programmer prints +them to the `stdout`, which is a serious mistake. First, we write the corresponding configuration and save it to the +`./.idea/utbot-taint-config.yaml` file for the analyzer to read from. + +```yaml +sources: + - User.getPassword: + add-to: return + marks: sensitive-data + +passes: + - User.userInfo: + get-from: [ arg1, arg2 ] + add-to: return + marks: [ ] # all + +sinks: + - java.io.PrintStream.println: + check: arg1 + marks: sensitive-data +``` + +Then we enable taint analysis in settings and run the UnitTestBot plugin in IntelliJ IDEA. + + + +Generated code: + +```java +public final class UserTest { + // some omitted code + + ///region SYMBOLIC EXECUTION: TAINT ANALYSIS for method printUserInfo() + + @Test + @DisplayName("printUserInfo: System.out.println(info) : True -> DetectTaintAnalysisError") + public void testPrintUserInfo_PrintStreamPrintln_1() { + User user = new User(); + + user.printUserInfo(); + fail("'java.lang.String' marked 'sensitive-data' was passed into 'PrintStream.println' method"); + } + + ///endregion +} +``` + +We can see the detected problem in the _Problems_ tool window: + + + +**A brief explanation** + +Upon executing the `getPassword` method, the symbol corresponding to the password variable +is marked as "sensitive-data" (a zero bit is set to 1 in its taint vector). Upon calling `userInfo`, the `info` +variable is also marked, since `userInfo` is a taint pass that adds the marks from +both of its arguments to the return value. Before printing `info` to the console, the `processTaintSink` handler function adds a constraint +to the SMT solver, so that its satisfiability corresponds to throwing an artificial error. The logical +formula for +this path is satisfiable, so the analyzer reports an error detected, which we eventually observe. + +### Unit tests + +Taint analysis unit tests are located at the `./utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/` +directory. + +Test use examples are located at `utbot-sample/src/main/java/org/utbot/examples/taint`. Each example has its own +configuration file stored at `utbot-sample/src/main/resources/taint`. diff --git a/docs/UnitTestBotDecomposition.md b/docs/UnitTestBotDecomposition.md new file mode 100644 index 0000000000..ac118e9036 --- /dev/null +++ b/docs/UnitTestBotDecomposition.md @@ -0,0 +1,71 @@ +# UnitTestBot decomposition + +This document is a part of UnitTestBot roadmap for the nearest future. We plan to decompose the whole UnitTestBot mechanism into the following standalone systems. + +## Fuzzing platform + +Entry points: +* `org.utbot.fuzzing.Fuzzing` +* `fuzz` extension function + +Exit point: +overridden `Fuzzing#run` method + +Probable fields of use (without significant implementation changes): +1. Test generation +2. Taint analysis +3. Finding security vulnerabilities +4. Static analysis validation +5. Automatic input minimization +6. Specific input-output search + +## Symbolic engine platform + +Probable fields of use (without significant implementation changes): +1. Test generation +2. Taint analysis +3. Type inference + +A more abstract interface can be extracted. For instance, we can use the interface to solve type constraints for Python or other languages. +Currently, there are two levels of abstraction: +1. Java-oriented abstraction that is intended to mimic heap and stack memory. +2. More low-level and less Java-coupled API to store constraints for Z3 solver. + +There is a room for improvement, namely we can extract more high-level abstraction, which can be extended for different languages. + +Entry points: +* `org.utbot.engine.Memory` +* `org.utbot.engine.state.LocalVariableMemory` +* `org.utbot.engine.SymbolicValue` + +Exit point: +`org.utbot.engine.Resolver` → `UtModel` + +Another level of abstraction is `UtExpression`. Basically, it is a thin abstraction over Z3 solver. + +## Program synthesis system + +An implementation that allows `UtAssembleModel` to keep information about object creation in a human-readable format. Otherwise, the object state should be initiated with _Reflection_ or sufficient constructor call. The synthesizing process is built upon the UnitTestBot symbolic execution memory model and is supposed to preserve construction information during the analysis process. + +Entry and exit point: +`org.utbot.framework.synthesis.Synthesizer` + +## Program analysis system + +We use an outdated approach with the [Soot](https://github.com/soot-oss/soot) framework. It is not worth being extracted as a separate service. A good substitution is the [JacoDB](https://github.com/UnitTestBot/jacodb) library. Currently, this library provides an API to work with Java code, send queries, provide custom indexes, and so on. + +## Code generation system + +The current domain of code generation is specific for generating tests, though it could be reused for other purposes. Currently, the engine can be used to generate tests for different test frameworks. One can use the code generator to generate test templates inside the IntelliJ-based IDEs. + +Entry and exit point: +`org.utbot.framework.codegen.generator.CodeGenerator#generateAsStringWithTestReport` + +Note that for Spring projects `SpringCodeGenerator` is used. It supports both unit and integration tests generation. + +## SARIF report visualizer + +UnitTestBot represents the result of analysis using SARIF — the format that is widely used in the GitHub community. SARIF allows users to easily represent the results in the built-in GitHub viewer. Additionally, we provide our own SARIF report visualizer for IntelliJ IDEA. + +Entry and exit point: +`org.utbot.gradle.plugin.GenerateTestsAndSarifReportTask` \ No newline at end of file diff --git a/docs/UtUtilsClass.md b/docs/UtUtilsClass.md new file mode 100644 index 0000000000..af99a94bbe --- /dev/null +++ b/docs/UtUtilsClass.md @@ -0,0 +1,57 @@ +# UtUtils class + +## What are the utility methods + +_Utility methods_ implement common, often re-used operations which are helpful for accomplishing tasks in many +classes. In UnitTestBot, _utility methods_ include those related to creating instances, checking deep +equality, mocking, using lambdas and so on — miscellaneous methods necessary for generated tests. + +## Why to create UtUtils class + +Previously, UnitTestBot generated _utility methods_ for each test class when they were needed — and only those which +were necessary for the given class. They were declared right in the generated test class, occupying space. Generating multiple test classes often resulted in duplicating _utility methods_ and consuming even more space. + +For now UnitTestBot provides a special `UtUtils` class containing all _utility methods_ if at least one test class needs some of them. This class is generated once and the specific methods are imported from it if necessary. No need for _utility methods_ — no `UtUtils` class is generated. + +We create a separate `UtUtils` class for each supported language (if required). + +## What does it look like + +Here is an example of a documentation comment inherent to every `UtUtils` class: + +![Documentation](images/utbot_ututils_2.0.png) + +As one can see, the comment mentions two characteristics of the `UtUtils` class: + +1. _Version_ + +If the generated tests require additional _utility methods_, the +existing `UtUtils` class is upgraded and gets a new version number, which should be defined here: + +`org.utbot.framework.codegen.domain.builtin.UtilClassFileMethodProvider.UTIL_CLASS_VERSION` + +_2. Mockito support_ + +UnitTestBot uses [Mockito framework](https://site.mockito.org/) to implement mocking. When generated tests imply mocking, the +`deepEquals()` +_utility method_ should be configured — it should have a check: whether the compared object is a mock or not. That is why the `UtUtils` class for the tests with mocking differs from the one without mocking support. + +If you have previously generated tests with mocking, the next generated `UtUtils` class supports mocking as well — +even if +its version is upgraded or current tests do not need mocking, so that the existing tests can still +rely on the proper methods from `UtUtils` class. + +## Where to find it + +`UtUtils` class is usually located in the chosen **Test sources root** near the generated test classes. The corresponding package name mentions the language of the generated tests: e.g. `org.utbot.runtime.utils.java`. + +## How to test + +If you want to test `UtUtils` class generation using UnitTestBot project itself, refer to the [Manual testing of +UtUtils class generation #1233](https://github.com/UnitTestBot/UTBotJava/issues/1233). + +## How to improve + +UnitTestBot does not currently support generating tests for classes from multiple modules simultaneously. If this option was possible, we would probably have to generate a separate `UtUtils` class for each module. Perhaps we could find a special location for a `UtUtils` class reachable from every module. + +For now, you can generate separate `UtUtils` classes for different modules only if you manually choose the different **Test sources roots** when generating tests. \ No newline at end of file diff --git a/docs/UtbotFamilyChanges.md b/docs/UtbotFamilyChanges.md new file mode 100644 index 0000000000..e739aab937 --- /dev/null +++ b/docs/UtbotFamilyChanges.md @@ -0,0 +1,87 @@ +# Changes + +## Main settings + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `gradle.properties` | Add version parameters and IDE dependencies | +| `settings.gradle` | Rewrite to kts | + +## utbot-framework-api + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt` | Make `UtModel` open | +| `utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt` | Add field `missedInstructions` to class `Coverage` (default empty) | + +## utbot-framework + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt` | Add default else branch for Python and JS models in method `construct` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt` | Add default else branch for Python and JS models in method `assembleModel` | + +## utbot-framework > codegen + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt` | Make class `Import` abstract (for python imports), make class `TestFramework` open, field `assertEquals` and methdod `assertionId` open. Add nullable field `testSuperClass` to `TestFramework` (contains superclass for test class). | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt` | Move function `getLanguageKeywords` into `CgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt` | Make class `CodeGenerator`, field `context` and methods open. Swap fields in class `CodeGeneratorResult`. | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt` | Add new constructors for `CgMethodTestSet` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt` | Remove internal from data class `TestClassContext` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt` | Remove internal from class `UtilMethodProvider` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt` | Remove internal from classes `Context` and `CgContextOwner`. Add field `cgLanguageAssistant` into `CgContextOwner`. Move logic from `CgContext.__outerMostTestClassContext` and `CgContext.outerMostTestClass` to `CgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt` | Remove internal from `CgNameGenerator` and `CgNameGeneratorImpl`, change `codegenLanguage` argument to `cgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt` | Remove internal from interface `CgFieldStateManager` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt` | Change private to protected and add empty else branches in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt` | Change private to open or protected, add `cgLanguageAssistant` call instead standard implementations | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt` | Change private to open and add else branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt` | Remove internal and add else branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt` | Remove internal from `TestFrameworkManager` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt` | Remove internal from `EnvironmentFieldStateCache`, `FieldStateCache`, `CgFieldState`, `CgContextOwner.importIfNeeded` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt` | Add visit for `CgForEachLoop` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt` | Add else-branch in `TestFramework`-whens | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt` | Remove internal from `buildExceptionHandler` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt` | Add `CgForEachLoop` visit function, change private to protected some methods, move `makeRender` logic to `CgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt` | Remove `language` field | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt` | Remove `language` field, change `context.codegenLanugage` to `context.cgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt` | Remove internal and add `cgLanguageAssistant` field | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt` | Add visit for `CgForEachLoop` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt` | Add else-branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt` | Remove internal | +| `utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt` | Add else-branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt` | Add else-branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CgLanguageAssistant.kt` | New file with `CgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCgLanguageAssistant.kt` | Implementation `CgLanguageAssistant` for Java | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCgLanguageAssistant.kt` | Implementation `CgLanguageAssistant` for Kotlin | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/LanguageTestFrameworkManager.kt` | New file with `LanguageTestFrameworkManager` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JVMTestFrameworkManager.kt` | Implementation `LanguageTestFrameworkManager` for JVM (Java + Kotlin) | + +## utbot-fuzzers + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt` | Make class `FuzzedMethodDescription` open | + + +## utbot-intellij and utbot-ui-commons + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt` | Move `GenerateTestsModel.getAllTestSourceRoots()` to `BaseTestModel` method, add empty else branch to `insertImports` | +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt` | Implementation `LanguageAssistant` for Java | +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt` | Move common logic to `BaseTestModel` | +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt` | Add else-branches in `TestFramework`-whens | +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt` | Move all logic to `JvmLanguageAssistant` | +| `utbot-intellij/src/main/resources/META-INF/` | Add config xml files for Java, Kotlin, Android, Python, JS | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt` | New class for Actions logic | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt` | New parent class for TestModels | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestSourceDirectoryChooser.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt` | New file, moved from `utbot-intellij` | \ No newline at end of file diff --git a/docs/ci/ci-in-utbot-java.md b/docs/ci/ci-in-utbot-java.md new file mode 100644 index 0000000000..8fd539fcd7 --- /dev/null +++ b/docs/ci/ci-in-utbot-java.md @@ -0,0 +1,61 @@ + + +# CI features + +UTBot Java offers contributors bunch of workflows e.g., the workflow _building the project and running tests_, the workflow _archiving plugin and CLI_. + +The main CI features in UTBot Java: +* reproducible environment +* available monitoring processes + +## Reproducible environment + +Depending on the resources where you are intended to build and test software environment will be different. The key goal is to provide the same environment on different resources. To do that we use Docker images with appropriate software, environment variables and OS settings. + +Crucial CI workflows run in those docker containers thus you can reproduce the environment locally. The environment can be used for running tests or for debugging ([see detailed information](https://github.com/UnitTestBot/UTBotJava/wiki/docker-for-utbot-java)). + +If you have any questions of where images are placed, how many they are, what software versions are used, visit [repository](https://github.com/UnitTestBot/infra-images) please (now is private, will be changed in the future), leave an issue with your questions or ask in DM. + +## All stages Monitoring + +Since the workflow has started you can check access to the metrics on our monitoring service (ask teammates for url). The server offers developers the following dashboards: + +* **Node Exporter Full** - metrics of consuming the RAM, CPU, Network and other resources on the host +* **JVM dashboard** (don't forget to set job to `pushgateway`) - Java metrics +* **Test executor statistics*** - RAM consuming by Java processes +* **cAdvisor: container details*** - system resources consuming by certain container +* **cAdvisor: host summary*** - summarized system resources consuming by all containers + +**Note:** * developed by UTBot team + +When you open a dashboard you need to choose valid instance. GitHub runs **each job on separate runner** so instance ID (`HOSTNAME` env var) would be different. But all instances have **the same Run ID** (`GITHUB_RUN_ID` env var). Follow this steps: + +1. Go to Actions and open your Run; +2. Expand job list and choose any job you need; +3. At the right you'll see a list of steps. You need step `Run monitoring`; +4. Find the string like: +``` +Find your Prometheus metrics using label {instance="2911909439-7f83f93ff335"} +``` +5. Copy value between double quotes and go to monitoring dashboard. Set `github` service and expand instance list, CTRL+F and paste copied value. Choose your instance + +image + +**Note:** label consists of two part - `${GITHUB_RUN_ID}-${HOSTNAME}`. Use only one part to find all jobs of your Run. + +# Available workflows + +| Workflow name | What it's supposed to do | What it triggers on | +| --- | --- | --- | +| UTBot Java: build and run tests | Builds the project and runs tests for it | **push** or **pull request** to the **main** branch | +| [M] UTBot Java: build and run tests | Builds the project and runs tests for it | **manual** call or call from **another workflow** | +| [M] Run chosen tests | Runs a single test or tests in chosen package/class | **manual** call | +| Plugin and CLI: publish as archives | Archives plugin and CLI and stores them attached to the workflow run report | **push** to the **main** branch | +| [M] Plugin and CLI: publish as archives | Archives plugin and CLI and stores them attached to the workflow run report | **manual** call or call from **another workflow** | +| [M] Publish on GitHub Packages | Publishes artifacts such as _utbot-api_, _utbot-core_, _utbot-framework_, etc., on GitHub Packages | **manual** call | \ No newline at end of file diff --git a/docs/ci/docker-for-utbot-java.md b/docs/ci/docker-for-utbot-java.md new file mode 100644 index 0000000000..2a4d417826 --- /dev/null +++ b/docs/ci/docker-for-utbot-java.md @@ -0,0 +1,95 @@ + + +# Reproducible environment + +It's available to download docker image with the environment for UTBot. The environment is also used in the crucial CI scripts focused on building project and running tests. + +The docker image pre-installed environment includes: +1. *Java 17* + *JDK* package +3. *Gradle 7.6.1* +3. *Kotlin compiler 1.8.0* + +It's based on Ubuntu [SOME VERSION]. + +## How to install Docker + +Using reproducible environment requires Docker installed. + +The detailed information of how to install Docker can be found on the [official site](https://docs.docker.com/engine/install/). + +## How to run tests in docker container + +Do the following steps to run tests in docker container: + +1. Pull docker image +``` +docker pull unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 +``` +2. Run docker container +```bash +# -v :/usr/utbot-debug - mounts the host directory into the container directory +# -it - make the container look like a terminal connection session +# -w /usr/utbot-tests - sets up working directory inside the container +docker run -it -v :/usr/utbot-tests --name utbot-tests -w /usr/utbot-tests unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 +``` +3. Do whatever you want + +* Build UTBot and run tests: +``` +gradle clean build --no-daemon +``` +* Build UTBot without running tests: +``` +gradle clean build --no-daemon -x test +``` +* Run tests for *utbot-framework* project *CustomerExamplesTest* class: +``` +gradle :utbot-framework:test --no-daemon --tests "org.utbot.examples.collections.CustomerExamplesTest" +``` +4. Exit container +``` +exit +``` + +## How to debug UTBot in docker container + +Do the following steps to debug UTBot in docker container: + +1. Set up configuration for remote debug in IntelliJ IDEA + +**Run/Debug Configurations** → **Add New Configuration** → Choose **Remote JVM Debug** → Set up **Configuration name** → **Ok** + +2. Pull docker image +``` +docker pull unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 +``` +3. Run docker container +```bash +# -v :/usr/utbot-debug - mounts the host directory into the container directory +# -it - make the container look like a terminal connection session +# -w /usr/utbot-tests - sets up working directory inside the container +# -p 5005:5005 - mounts the host port into the container port (debugging port) +docker run -it -p 5005:5005 -v :/usr/utbot-debug --name utbot-debug -w /usr/utbot-tests unittestbot/java-env:java17-zulu-jdk-gradle7.6.1-kotlinc1.8.0 +``` +4. Set up gradle options for remote debug: +``` +export GRADLE_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 +``` +5. Start building and running tests +``` +gradle clean build --no-daemon +``` +6. Attach in IntelliJ IDEA to the gradle process in the container + +Set up **breakpoints** wherever you want → **Run** new **Configuration** in **Debug** mode + +7. Exit container +``` +exit +``` diff --git a/docs/ci/ssh-session-with-github-agent.md b/docs/ci/ssh-session-with-github-agent.md new file mode 100644 index 0000000000..5f8b8b48d6 --- /dev/null +++ b/docs/ci/ssh-session-with-github-agent.md @@ -0,0 +1,21 @@ + + +# SSH session with GitHub agent + +It's available to use **action** letting set up SSH session with GitHub agent in your **workflows**. The detailed documentation with the examples of use can be found in the [official repository](https://github.com/mxschmitt/action-tmate). + +The action setting SSH session can be easily plugged in your workflow with the example below: +``` +- name: Setup tmate session + uses: mxschmitt/action-tmate@v3 +``` + +When the action is plugged in the workflow log (the part corresponding to tmate action log) can be found the URL. By the URL you can access the terminal of your host. + +There are also some ways to setup action behavior. E.g., the default behavior of the action is to remain SSH session open until the workflow times out. It's available to setup timeout parameter yourself. \ No newline at end of file diff --git a/Conventions.md b/docs/contributing/Conventions.md similarity index 100% rename from Conventions.md rename to docs/contributing/Conventions.md diff --git a/docs/contributing/InterProcessDebugging.md b/docs/contributing/InterProcessDebugging.md new file mode 100644 index 0000000000..d488d7f56f --- /dev/null +++ b/docs/contributing/InterProcessDebugging.md @@ -0,0 +1,122 @@ +# Interprocess debugging of UnitTestBot Java + +### Background + +We have split the UnitTestBot machinery into three processes. See the [document on UnitTestBot multiprocess +architecture](../RD%20for%20UnitTestBot.md). +This approach has improved UnitTestBot capabilities, e.g., provided support for various JVMs and scenarios but also +complicated the debugging flow. + +These are UnitTestBot processes (according to the execution order): + +* _IDE process_ +* _Engine process_ +* _Instrumented process_ + +Usually, the main problems happen in the _Engine process_, but it is not the process we run first. +See how to debug UnitTestBot processes effectively. + +### Enable debugging + +Debugging the _IDE process_ is pretty straightforward: start the debugger session (**Shift+F9**) for the `runIde` +Gradle task in `utbot-intellij` project from your IntelliJ IDEA. + +To debug the _Engine process_ and the _Instrumented process_, you need to enable the debugging options: +1. Open [`UtSettings.kt`](../../utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt). +2. There are two similar options: `runEngineProcessWithDebug` and `runInstrumentedProcessWithDebug` — enable the + relevant one(s). There are two ways to do this: + * You can create the `~/.utbot/settings.properties` file and write the following: + + ``` + runEngineProcessWithDebug=true + runInstrumentedProcessWithDebug=true + ``` + Then restart the IntelliJ IDEA instance you want to debug. + + * **Discouraged**: you can change the options in the source file, but this will involve moderate project + recompilation. +3. You can set additional options for the Java Debug Wire Protocol (JDWP) agent if debugging is enabled: + * `engineProcessDebugPort` and `instrumentedProcessDebugPort` are the ports for debugging. + + Default values: + - 5005 for the _Engine process_ + - 5006 for the _Instrumented process_ + + * `suspendEngineProcessExecutionInDebugMode` and `suspendInstrumentedProcessExecutionInDebugMode` define whether + the JDWP agent should suspend the process until the debugger is connected. + + More formally, if debugging is enabled, the following switch is added to the _Engine process_ JVM at the start by + default: + ``` + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=5005" + ``` + + These options set `suspend` and `address` values. For example, with the following options in `~/.utbot/settings.properties`: + ``` + runEngineProcessWithDebug=true + engineProcessDebugPort=12345 + suspendEngineProcessExecutionInDebugMode=false + ``` + the resulting switch will be: + ``` + "-agentlib:jdwp=transport=dt_socket,server=n,suspend=n,quiet=y,address=12345" + ``` + See `org.utbot.intellij.plugin.process.EngineProcess.Companion.debugArgument` for switch implementation. +4. For information about logs, refer to the [Interprocess logging](InterProcessLogging.md) guide. + +### Run configurations for debugging the Engine process + +There are three basic run configurations: +1. `Run IDE` configuration allows running the plugin in IntelliJ IDEA. +2. `Utility Configurations/Listen for Instrumented Process` configuration allows listening to port 5006 to check if + the _Instrumented process_ is available for debugging. +3. `Utility Configurations/Listen for Engine Process` configuration allows listening to port 5005 to check if the _Engine process_ is available for debugging. + +On top of them, there are three compound run configurations for debugging: +1. `Debug Engine Process` and `Debug Instrumented Process` — a combination for debugging the _IDE process_ and + the selected process. +3. `Debug All` — a combination for debugging all three processes. + +To make debug configurations work properly, you need to set the required properties in `~/.utbot/settings.properties`. If you change the _port number_ and/or the _suspend mode_, do change these default values in the corresponding Utility Configuration. + +### How to debug + +Let's walk through an example illustrating how to debug the "_IDE process_ → _Engine process_" communication. + +1. In your current IntelliJ IDEA with source code, use breakpoints to define where the program needs to be stopped. For example, set the breakpoints at `EngineProcess.generate` and somewhere in `watchdog.wrapActiveCall(generate)`. +2. Select the `Debug Engine Process` configuration, add the required parameters to `~/.utbot/settings.properties` and + start the debugger session. +3. Generate tests with UnitTestBot in the debug IDE instance. Make sure symbolic execution is turned on, otherwise some processes do not even start. +4. The debug IDE instance will stop generation (if you have not changed the debug parameters). If you take no action, test generation will be canceled by timeout. +5. When the _Engine process_ has started (build processes have finished, and the progress bar says: _"Generate + tests: read classes"_), there will be another debug window — "Listen for Engine Process", — which automatically + connects and starts debugging. +6. Wait for the program to be suspended upon reaching the first breakpoint in the _Engine process_. + +### Interprocess call mapping + +Now you are standing on a breakpoint in the _IDE process_, for example, the process stopped on: + + EngineProcess.generate() + +If you go along the execution, it reaches the next line (you are still in the _IDE process_): + + engineModel.generate.startBlocking(params) + +It seems that test generation itself should occur in the _Engine process_ and there should be an entry point in the _Engine process_. +How can we find it? + +Standing on the breakpoint at `engineModel.generate.startBlocking(params)`, right-click on +`EngineProcessModel.generate` and **Go to** > **Declaration or Usages**. This navigates to the `RdCall` definition (which is +responsible for cross-process communication) in the `EngineProcesModel.Generated.kt` file. + +Now **Find Usages** for `EngineProcessModel.generate` and see the point where `RdCall` is passed to the next method: + + watchdog.wrapActiveCall(generate) + +This is the point where `RdCall` is called in the _Engine process_. + +You could have skipped the previous step and used **Find Usages** right away, but it is useful to know +where `RdCall` is defined. + +If you are interested in the trailing lambda of `watchdog.wrapActiveCall(generate)`, set the breakpoint here. \ No newline at end of file diff --git a/docs/contributing/InterProcessLogging.md b/docs/contributing/InterProcessLogging.md new file mode 100644 index 0000000000..a023dba59e --- /dev/null +++ b/docs/contributing/InterProcessLogging.md @@ -0,0 +1,248 @@ +# Interprocess logging + +This document describes +how logging is implemented across the UnitTestBot Java [processes](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/RD%20for%20UnitTestBot.md): +the IDE process, the Engine process, and the Instrumented process. + +## Architecture + +The UnitTestBot Java logging system relies on `log4j2` library. + +For UnitTestBot Java used as an IntelliJ IDEA plugin, the configuration file for logging is [`utbot-intellij/log4j2.xml`](../../utbot-intellij/src/main/resources/log4j2.xml). + +When used as Contest estimator or the Gradle/Maven plugins, via CLI or during the CI test runs, +UnitTestBot Java engine searches classpath for the first `log4j2.xml` in the `resources` directory. + +### IDE process + +The IDE process writes logging information to standard `idea.log` files and puts them into the default log directory. + +To configure logs for the IDE process, use the log configuration file in a straightforward way. + +To change the log configuration for the prebuilt plugin, +go to **Help** > **Diagnostic Tools** > **Debug Log Settings...** and configure `log4j2.xml`. + +To store log data for the Engine process started from the IDE process, the UnitTestBot Java plugin creates a directory: +`org.utbot.intellij.plugin.process.EngineProcessKt.engineProcessLogDirectory`. + +### Engine process + +The Engine process can be started either from IntelliJ IDEA or separately — as a standalone engine. + +#### Engine process started from IntelliJ IDEA + +As the plugin does not support multiple generation processes, +the logs from the Engine process are written to the same file. + +The default log file directory is `%user_temp%/UtBot/rdEngineProcessLogs`. + +The [`utbot-intellij/log4j2.xml`](../../utbot-intellij/src/main/resources/log4j2.xml) file is copied to the +UnitTestBot Java temporary directory: +`org.utbot.intellij.plugin.process.EngineProcessKt.engineProcessLogConfigurationsDirectory`. +Then this file is provided to the Engine process via the following CLI switch: + ``` + -Dlog4j2.configurationFile=%configuration_file% + ``` + +Here, `%configuration_file%` can take one of two values: +1. A modified copy of [`utbot-intellij/log4j2.xml`](../../utbot-intellij/src/main/resources/log4j2.xml) file, which is stored in UnitTestBot Java temporary directory. + + More precisely, there are 2 appenders in the configuration file: + ```xml + + + + + ``` + By default, `IdeaAppender` is used everywhere in a file for the IDE plugin. + + For the Engine process, a temporary `log4j2.xml` is created, + where the `ref="IdeaAppender"` substring is replaced with `ref="EngineProcessAppender"`: + this replacement changes all the appenders and the log pattern + but keeps categories and log levels for the loggers the same. + + As soon as the file reaches 20 MB size, `RollingFileAppender` writes the logs to the `utbot-engine-current.log` file. + The created log files are named `utbot-engine-%i.log`. + A log file with the largest index is the latest one: `utbot-engine-1.log` has been created earlier than `utbot-engine-10.log`. + + Each time the Engine process starts, the following lines are printed into the IntelliJ IDEA log: + ``` + | UtBot - EngineProcess | Engine process started with PID = 4172 + | UtBot - EngineProcess | Engine process log directory - C:\Users\user_name\AppData\Local\Temp\UTBot\rdEngineProcessLogs + | UtBot - EngineProcess | Engine process log file - C:\Users\user_name\AppData\Local\Temp\UTBot\rdEngineProcessLogs\utbot-engine-current.log + ``` + +2. A path from `UtSettings.engineProcessLogConfigFile`. + + The option provides the external `log4j2` configuration file with the path instead of [`utbot-intellij/log4j2.xml`](../../utbot-intellij/src/main/resources/log4j2.xml). + In the `~/.utbot/settings.properties` file, one can set this path to a custom configuration file applicable to the Engine process, for example: + ``` + engineProcessLogConfigFile=C:\wrk\UTBotJava\engineProcessLog4j2.xml + ``` + This allows you to configure logs for the Engine process even for the prebuilt plugin (you need to restart an IDE). + +#### Engine process started separately + +When used as Contest estimator or the Gradle/Maven plugins, via CLI or during the CI test runs, +UnitTestBot Java engine searches classpath for the first `log4j2.xml` in the `resources` directory +to get configuration information. + +### Instrumented process + +The Instrumented process sends the logs to its parent — to the Engine process. +Logs are sent via the corresponding Rd model: `org.utbot.rd.models.LoggerModel`. + +See also `org.utbot.instrumentation.rd.InstrumentedProcess.Companion.invoke` and +`org.utbot.instrumentation.process.InstrumentedProcessMainKt.main`. + +## Rd logging system + +Rd has the custom logging system based on `com.jetbrains.rd.util.Logger` interface. +It is convenient to set the Rd logging system as default for the Instrumented process: +during concrete execution, +the `log4j2` classes in UnitTestBot Java could be confused with the `log4j2` classes from the project under test. +Duplicated `log4j2` libraries can break instrumentation and coverage statistics. + +You should always override the default Rd logging strategy, which writes log data to `stdout/stderr`. +Use `com.jetbrains.rd.util.Logger.Companion.set` method to provide custom +`com.jetbrains.rd.util.ILoggerFactory`. +The created loggers will be automatically re-instantiated to obtain a new logger from the provided factory. +You can obtain a logger via the `com.jetbrains.rd.util.getLogger` function. +Check `EngineProcessMain` for Rd logging example. + +For available Rd factories, see the `org.utbot.rd.loggers` package: it contains the implemented factories. +The format of the log messages is the same as described in `utbot-intellij/src/main/resources/log4j2.xml`. + +## Implementation details + +### Additivity + +An entry may appear in a log many times due to _additivity_. The resulting log may look like this: +``` +13:55:41.204 | INFO | AnalyticsConfigureUtil | PathSelectorType: INHERITORS_SELECTOR +13:55:41.204 | INFO | AnalyticsConfigureUtil | PathSelectorType: INHERITORS_SELECTOR +``` + +The logger's full name constitutes a tree structure so that the logged events from a child are visible to a parent. + +For example, the following `log4j2.xml` configuration in IntelliJ IDEA will produce such a problem: +```xml +... + + + + + + + + +... +``` + +This happens because the `org.utbot` logger is a parent to `org.utbot.intellij`, and all the events from +`org.utbot.intellij` are also transferred to `org.utbot`. + +To modify this behavior, add the `additivity="false"` tag to all loggers manually: +```xml +... + + + + + + + + +... +``` + +Consider this problem when you manually configure log level and appender for a logger. + +For more information, +refer to the [`log4j2` additivity](https://logging.apache.org/log4j/2.x/manual/configuration.html#Additivity) document. + +### Logging: auxiliary methods + +Find more logging-related methods at `UtRdLogUtil.kt` and `Logging.kt`. + +To trace the execution duration, +use the `measureTime` method (see `Logging.kt`) with the corresponding log level scope. + +In the Engine process, the entries from the Instrumented process are logged by `org.utbot.instrumentation.rd.InstrumentedProcessKt.rdLogger`. + +## Log levels and performance + +For development, the `Debug` level is preferred in most cases. + +The `Info` log level is sufficient for release. + +In Rd, if you choose the `Trace` level for all loggers or set it as default for the root logger, +this enables logging for all technical _send/receive_ events from protocol. +It may cause ~50 MB of additional entries per generation to appear and _heavily_ pollutes the log. This might be useful +for troubleshooting interprocess communication but in all other cases prefer the `Debug` level or +specify the `Trace` level per logger explicitly. + +For the `Debug` level, if a log message requires heavy string interpolation, wrap it in lambda, for example: +```kotlin +val someVeryBigDataStructure = VeryBigDataStructure() + +logger.debug("data structure representation - $someVeryBigDataStructure") // <---- interpolation +``` +Here, even for a message with the `Debug` level, interpolation will always occur because +the message is passed as a parameter, which is evaluated at call site. +If the `Info` level (or higher) is set for a logger, +the message is built, but not logged, +resulting in unnecessary work, possibly causing performance issues. + +Consider using lambdas: +```kotlin +// message will be created only if debug log level is available +logger.debug { "data structure representation - $someVeryBigDataStructure"} +``` + +Here, although the logs are sent from one process to another, no performance penalties have been noticed. + +To reach higher performance, try to use `bufferedIO` and `immediateFlush` properties in `log4j2.xml`. +For example, you can make the following changes to the `log4j2.xml` file in `utbot-intellij`: +```xml + +``` + +This will reduce a number of I/O operations and help to use `log4j2` buffer more efficiently. +This may also have a flip side: +when the process terminates, `log4j2` terminates the logging service before the buffer is flushed, and +you will lose the last portion of logs. +This behavior is undesirable for testing and debugging, +but probably acceptable for release. + +## Docker and Gradle + +To see the logs in Gradle from console, Docker and CI, add the following `build.gradle.kts` file: +```kotlin +allprojects { + tasks { + withType { + testLogging.showStandardStreams = true + testLogging.showStackTraces = true + } + } +} +``` + +## Useful links + +UnitTestBot Java documentation: +1. [Multiprocess architecture](../RD%20for%20UnitTestBot.md) +2. [Interprocess debugging](InterProcessDebugging.md) +3. [How to use loggers](../../HowToUseLoggers.md) + +`log4j2` documentation: +1. [Architecture](https://logging.apache.org/log4j/2.x/manual/architecture.html) — an overall `log4j2` description. +2. [Layouts](https://logging.apache.org/log4j/2.x/manual/layouts.html) — how to format log messages. + (UnitTestBot Java uses `Pattern layout` everywhere.) +3. [Appenders](https://logging.apache.org/log4j/2.x/manual/appenders.html) — + a description of various ways to store log entries (and how to configure the storages). + UnitTestBot Java uses the `Console`, `File` and `RollingFile` appenders. +4. [Configuration](https://logging.apache.org/log4j/2.x/manual/configuration.html) — + how to use a configuration file, how to check the file, and other useful information. + It is **highly advised** to read the `Additivity` part. \ No newline at end of file diff --git a/docs/contributing/LabelUsageGuideline.md b/docs/contributing/LabelUsageGuideline.md new file mode 100644 index 0000000000..05007f6be8 --- /dev/null +++ b/docs/contributing/LabelUsageGuideline.md @@ -0,0 +1,93 @@ +# Label usage guideline + +We recommend to use labels only in these cases + + +![bug](https://user-images.githubusercontent.com/106974353/174105036-53ac8736-2e63-4a02-ac90-1aca34a8fb53.png) + +Something isn't working. +Indicates an unexpected problem or unintended behavior. + +# + +![170533338-082f808e-b74b-437d-802e-568099036b1e-depositphotos-bgremover](https://user-images.githubusercontent.com/106974353/174105268-52897d9b-3939-4063-bfec-2572dcef67f4.png) + +This label applies to the issues and pull requests related to `org.utbot.engine` package. +Use it if your issue or fix deals with model construction (including Soot and Jimple), +memory modeling, symbolic values, wrappers, mocking, value resolving, or interaction +with the SMT solver. + +Path selector issues generally should also have the "engine" label, unless the problem +is specific to a ML-based path selection algorithm. + +# + +![171006007-3ad32d41-1968-4a43-ac4b-f68f016f978b-depositphotos-bgremover](https://user-images.githubusercontent.com/106974353/174105349-f33620af-8694-486b-95af-eaabfd1e4fa7.png) + +This label applies to the issues and pull requests related to `org.utbot.intellij` module. +Use it if your changes in code are related to plugin UI appearance (mostly `ui` package) +or close functionality: frameworks installation, sarif reports generation, etc. + +# + +![171006369-d7810250-258d-4b8d-8321-2742bd0a81db-depositphotos-bgremover](https://user-images.githubusercontent.com/106974353/174105444-beab859e-ea77-47dd-a5c2-27c0be350e82.png) + +This label applies to the issues and pull requests related to `org.utbot.framework.codegen`package. +Use it if your issue or fix deals with generating (rendering) code of unit tests based on obtained +from symbolic engine executions. It may relate to generation on both supported languages (Java and Kotlin). +Code generator related class names are often marked with `Cg` prefix or with `CodeGenerator` suffix. + +# + +![170533255-7fe1342b-4121-44f8-8678-78e52581235e-depositphotos-bgremover](https://user-images.githubusercontent.com/106974353/174105494-23cd502f-6181-445e-85fa-1f80ddc90e5f.png) + +Indicates a need for improvements or additions to documentation. + + +# + +![170533304-e0f95623-1fa5-427b-8545-ceb4113de597-depositphotos-bgremover (1)](https://user-images.githubusercontent.com/106974353/174105712-5ffc4157-142f-4971-8e4b-aced5ed2bc19.png) + +This issue or pull request already exists. +Indicates similar issues, pull requests, or discussions. + +# + +![170537552-fba154f5-14b8-4054-aa3b-d0c7a040677f-depositphotos-bgremover](https://user-images.githubusercontent.com/106974353/174105774-4688face-7e82-4bb6-8b2a-2372e3fb6400.png) + +New feature or request. + +# + +![170537570-ae56bc9f-19b7-4864-8a92-05e5b7f5f342-depositphotos-bgremover](https://user-images.githubusercontent.com/106974353/174105839-f0fbd000-b4c7-40fe-a261-bde8048de13b.png) + +Good for newcomers. +Indicates a good issue for first-time contributors. + +# + +![170537578-37181739-204f-4527-a337-17333d45542d-depositphotos-bgremover](https://user-images.githubusercontent.com/106974353/174105932-596eb120-4f28-4e5c-842a-3fb4c5c375b7.png) + +Extra attention is needed. +Indicates that a maintainer wants help on an issue or pull request. + +# + +![170537586-ef98f24c-d12d-47b3-95eb-e396c2a14337-depositphotos-bgremover](https://user-images.githubusercontent.com/106974353/174106017-4be04dff-0451-46f1-b552-e5f5f3730438.png) + +This issue / PR doesn't seem right. +Indicates that an issue, pull request, or discussion is no longer relevant. + +# + +![170537612-daeed618-7cc2-44e6-9d67-d74939761dae-depositphotos-bgremover1](https://user-images.githubusercontent.com/106974353/174106553-506fe0bc-7ddb-47a7-9609-b7cd1b775f22.png) + +Further information is requested. +Indicates that an issue, pull request, or discussion needs more information. + +# + +![170537619-538ec3a4-1f50-4f19-8bf3-71ce7e2d1afe-depositphotos-bgremover (3)](https://user-images.githubusercontent.com/106974353/174106628-08b7cd36-8dc7-4eb3-82c7-e917d7d11e8f.png) + +This will not be worked on. +Indicates that work won't continue on an issue, pull request, or discussion. \ No newline at end of file diff --git a/docs/images/utbot_custom_javadoc_tags.png b/docs/images/utbot_custom_javadoc_tags.png new file mode 100644 index 0000000000..0025b4bb01 Binary files /dev/null and b/docs/images/utbot_custom_javadoc_tags.png differ diff --git a/docs/images/utbot_settings.png b/docs/images/utbot_settings.png new file mode 100644 index 0000000000..efd775711b Binary files /dev/null and b/docs/images/utbot_settings.png differ diff --git a/docs/images/utbot_ututils_2.0.png b/docs/images/utbot_ututils_2.0.png new file mode 100644 index 0000000000..c1146646c3 Binary files /dev/null and b/docs/images/utbot_ututils_2.0.png differ diff --git a/docs/spring.md b/docs/spring.md new file mode 100644 index 0000000000..85a87b4991 --- /dev/null +++ b/docs/spring.md @@ -0,0 +1,370 @@ +# Automated test generation for Spring-based code + +Java developers actively use the Spring framework to implement the inversion of control and dependency injection. +Testing Spring-based applications differs significantly from testing standard Java programs. Thus, we customized +UnitTestBot to analyze Spring projects. + + + * [General notes](#general-notes) + * [Limitations](#limitations) + * [Testability](#testability) + * [Standard unit tests](#standard-unit-tests) + * [Example](#example) + * [Use cases](#use-cases) + * [Spring-specific unit tests](#spring-specific-unit-tests) + * [Example](#example-1) + * [Use cases](#use-cases-1) + * [Side effects](#side-effects) + * [Mechanism](#mechanism) + * [Integration tests](#integration-tests) + * [Service layer](#service-layer) + * [Side effects](#side-effects-1) + * [Use cases](#use-cases-2) + * [Controller layer](#controller-layer) + * [Example](#example-2) + * [Microservice layer](#microservice-layer) + + +## General notes + +UnitTestBot proposes three approaches to automated test generation: +* [standard unit tests](#standard-unit-tests) that mock environmental interactions; +* [Spring-specific unit tests](#spring-specific-unit-tests) that use information about the Spring application context to reduce the number of + mocks; +* and [integration tests](#integration-tests) that validate interactions between application components. + +Hereinafter, by _components_ we mean Spring components. + +For classes under test, one should select an appropriate type of test generation based on their knowledge +about the Spring specifics of the current class. Recommendations on how to choose the test type are provided below. +For developers who are new to Spring, there is a "default" generation type. + +### Limitations + +UnitTestBot Java with Spring support uses symbolic execution to generate unit tests, so typical problems +related to this technique may appear: it may be not so efficient for multithreaded programs, functions with calls to +external libraries, processing large collections, etc. + +### Testability + +Note that UnitTestBot may generate unit tests more efficiently if your code is written to be unit-testable: the +functions are not too complex, each function implements one logical unit, static and global data are used +only if required, etc. Difficulties with automated test generation may have "diagnostic" value: it +may mean that you should refactor your code. + +## Standard unit tests + +The easiest way to test Spring applications is to generate unit tests for components: to +mock the external calls found in the method under test and to test just this method's +functionality. UnitTestBot Java uses the Mockito framework that allows to mark +the to-be-mocked objects with the `@Mock` annotation and to use the `@InjectMock` +annotation for the tested instance injecting all the mocked fields. See [Mockito](https://site.mockito.org/) +documentation for details. + +### Example + +Consider generating unit tests for the `OrderService` class that autowires `OrderRepository`: + +```java + +@Service +public class OrderService { + +@Autowired +private OrderRepository orderRepository ; + +public List getOrders () { +return orderRepository.findAll (); +} +} + +public interface OrderRepository extends JpaRepository +``` +Then we mock the repository and inject the resulting mock into a service: + +```java +public final class OrderServiceTest { + @InjectMocks + private OrderService orderService + + @Mock + private OrderRepository orderRepositoryMock + + @Test + public void testGetOrders () { + when(orderRepositoryMock .findAll()).thenReturn((List)null) + + List actual = orderService .getOrders() + assertNull(actual) + } +``` + +This test type does not process the Spring context of the original application. The components are tested in +isolation. + +It is convenient when the component has its own meaningful logic and may be useless when its main responsibility is to call other components. + +Note that if you autowire several beans of one type or a collection into the class under test, the code of test +class will be a bit different: for example, when a collection is autowired, it is marked with `@Spy` annotation due +to Mockito specifics (not with `@Mock`). + +### Use cases + +When to generate standard unit tests: +* _Service_ or _DAO_ layer of Spring application is tested. +* Class having no Spring specific is tested. +* You would like to test your code in isolation. +* You would like to generate tests as fast as possible. +* You would like to avoid starting application context and be sure the test generation process has no Spring-related side effects. +* You would like to generate tests in one click and avoid creating specific profiles or configuration classes for + testing purposes. + +We suggest using this test generation type for the users that are not so experienced in Spring or would like to get +test coverage for their projects without additional efforts. + +## Spring-specific unit tests + +This is a modification of standard unit tests generated for Spring projects that may allow us to get more +meaningful tests. + +### Example + +Consider the following class under test + +```java +@Service +public class GenderService { + +@Autowired +public Human human + +public String getGender () { +return human.getGender(); +} +} +``` +where `Human` is an interface that has just one implementation actually used in current project configuration. + +```java +public interface Human { +String getGender(); +} + +public class Man implements Human { +public String getGender() { +return “man” +} +} +``` + +The standard unit test generation approach is to mock the _autowired_ objects. It means that the generated test will be +correct but useless. However, there is just one implementation of the `Human` interface, so we may use it directly +and generate a test like this: + +```java +@Test +public void testGetGender_HumanGetGender() { +GenderService genderService = new GenderService(); +genderService.human = new Man(); +String actual = genderService.getGender(); +assertEquals(“man”, actual); +} +``` + +Actually, dependencies in Spring applications are often injected via interfaces, and they often have just one actual +implementation, so it can be used in the generated tests instead of an interface. If a class is injected itself, it +will also be used in tests instead of a mock. + +You need to select a configuration to guide the process of creating unit tests. We support all commonly used +approaches to configure the application: +* using an XML file, +* Java annotation, +* or automated configuration in Spring Boot. + +Although it is possible to use the development configuration for testing purposes, we strictly recommend creating a separate one. + +### Use cases + +When to generate Spring-specific unit tests: +* to reduce the amount of mocks in generated tests +* and to use real object types instead of their interfaces, obtaining tests that simulate the method under test execution. + +### Side effects + +We do not recommend generating Spring-specific unit tests, when you would like to maximize line coverage. +The goal of this approach is to cover the lines that are relevant for the current configuration and are to be used +during the application run. The other lines are ignored. + +When a concrete object is created instead of mocks, it is analyzed with symbolic execution. It means that the +generation process may take longer and may exceed the requested timeout. + +### Mechanism + +A Spring application is created to simulate a user one. It uses configuration importing users one with an additional +bean of a special _bean factory post processor_. + +This _post processor_ is called when bean definitions have already been created, but actual bean initialization has +not been started. It gets all accessible information about bean types from the definitions and destroys these +definitions after that. + +Further Spring context initialization is gracefully crashed as bean definitions do not exist anymore. Thus, this +test generation type is still safe and will not have any Spring-related side effects. + +Bean type information is used in symbolic execution to decide if we should mock the current object or instantiate it. + +## Integration tests + +The main difference of integration testing is that it tests the current component while taking interactions with +other classes into account. + +### _Service_ layer + +Consider an `OrderService` class we have already seen. Actually, this class has just one +responsibility: to return the result of a call to the repository. So, if we mock the repository, our unit test is +actually useless. However, we can test this service in interaction with the repository: save some information to the +database and verify if we have successfully read it in our method. Thus, the test method looks as follows. + +```java + +@Autowired +private OrderService orderService + +@Autowired +private OrderRepository orderRepository + +@Test +public void testGetOrderById() throws Exception { +Order order = new Order(); +Order order1 = orderRepository.save(order); +long id = (Long) getFieldValue(order1, "com.rest.order.models.Order ", "id“); + +Order actual = orderService.getOrderById(id); +assertEquals (order1, actual); +} +``` +The key idea of integration testing is to initialize the context of a Spring application and to autowire a bean of +the class under test, and the beans it depends on. The main difficulty is to mutate the initial _autowired_ state of the +object under test to another state to obtain meaningful tests (e.g. save some data to related repositories). +Here we use fuzzing methods instead of symbolic execution. + +You should take into account that our integration tests do not use mocks at all. It also means that if the method +under test contains calls to other microservices, you need to start the microservice unless you want to test your +component under an assumption that the microservice is not responding. +Writing tests manually, users can investigate the expected behavior of the external service for the current scenario, +but automated test generation tools have no way to do it. + +Note that XML configuration files are currently not supported in integration testing. However, you may create a Java +configuration class importing your XML file as a resource. The list of supported test +frameworks is reduced to JUnit 4 and JUnit 5; TestNG is not supported for integration tests. + +To run integration tests properly, several annotations are generated for the class with tests (some of them may be +missed: for example, we can avoid setting active profiles via the annotation if a default profile is used). + +* `@SpringBootTest` for Spring Boot applications +* `@RunWith(SpringRunner.class)`/`@ExtendWith(SpringExtension.class)` depending on the test framework +* `@BootstrapWith(SpringBootTestContextBootstrapper.class)` for Spring Boot applications +* `@ActiveProfiles(profiles = {profile_names})` to activate requested profiles +* `@ContextConfiguration(classes = {configuration_classes})` to initialize a proper configuration +* `@AutoConfugureTestDatabase` + +Two additional annotations are: + +* `@Transactional`: using this annotation is not a good idea for some developers because it can +hide problems in the tested code. For example, it leads to getting data from the transaction cache instead of real +communication with database. +However, we need to use this annotation during the test generation process due to the +efficiency reasons and the current fuzzing approach. Generating tests in transaction but not running them in +transaction may sometimes lead to failing tests. +In future, we are going to modify the test generation process and to use `EntityManager` and manual flushing to the +database, so running tests in transaction will not have a mentioned disadvantage any more. + +* `@DirtiesContext(classMode=BEFORE_EACH_TEST_METHOD)`: although running test method in transaction rollbacks most +actions in the context, there are two reasons to use `DirtiesContext`. First, we are going to remove +`@Transactional`. After that, the database `id` sequences are not rolled back with the transaction, while we would +like to have a clean context state for each new test to avoid unobvious dependencies between them. + +Currently, we do not have proper support for Spring security issues in UnitTestBot. We are going to improve it in +future releases, but to get at least some results on the classes requiring authorization, we use `@WithMockUser` for +applications with security issues. + +#### Side effects + +Actually, yes! Integration test generation requires Spring context initialization that may contain unexpected +actions: HTTP requests, calls to other microservices, changing the computer parameters. So you need to +validate the configuration carefully before trying to generate integration tests. We strictly recommend avoiding +using _production_ and _development_ configuration classes for testing purposes, and creating separate ones. + +#### Use cases + +When to generate integration tests: +* You have a properly prepared configuration class for testing +* You would like to test your component in interaction with others +* You would like to generate tests without mocks +* You would like to test a controller +* You consent that generation may be much longer than for unit tests + +### _Controller_ layer + +When you write tests for controllers manually, it is recommended to do it a bit differently. Of course, you may just +mock the other classes and generate unit tests looking similarly to the tests we created for services, but they may +not be representative. To solve this problem, we suggest a specific integration test generation approach for controllers. + +#### Example + +Consider testing the following controller method: + +```java + +@RestController +@RequestMapping(value = "/api") +public class OrderController { + + @Autowired + private OrderService orderService; + + @GetMapping(path = "/orders") + public ResponseEntity> getAllOrders() { + return ResponseEntity.ok().body(orderService.getOrders()); + } +} +``` +UnitTestBot generates the following integration test for it: + +```java +@Test +public void testGetAllOrders() throws Exception { +Object[] objectArray = {}; +MockHttpServletRequestBuilder mockHttpServletRequestBuilder = get("/api/orders", objectArray); + +ResultActions actual = mockMvc.perform(mockHttpServletRequestBuilder); + +actual.andDo(print()); +actual.andExpect((status()).is(200)); +actual.andExpect((content()).string("[]")); +} +``` + +Note that generating specific tests for controllers is now in active development, so some parameter annotations and +types have not been supported yet. For example, we have not supported the `@RequestParam` annotation yet. For now, +specific integration tests for controllers are just an experimental feature. + +### _Microservice_ layer + +Actually, during integration test generation we create one specific test that can be considered as a test for the +whole microservice. It is the `contextLoads` test, and it checks if a Spring application context has started normally. +If this test fails, it means that your application is not properly configured, so the failure of other tests is not caused by the regression in the tested code. + +Normally, this test is very simple: + +```java +/** +* This sanity check test fails if the application context cannot start. + */ + @Test + public void contextLoads() { + } +``` + +If there are context loading problems, the test contains a commented exception type, a message, and a +track trace, so it is easier to investigate why context initialization has failed. + diff --git a/docs/summaries/CustomJavadocTags.md b/docs/summaries/CustomJavadocTags.md new file mode 100644 index 0000000000..5b1a7eb819 --- /dev/null +++ b/docs/summaries/CustomJavadocTags.md @@ -0,0 +1,85 @@ +## Custom Javadoc Tags + +Currently, summaries are hard to read because of formatting issues and a lot of details they contain. + +**Goal**: to make test summaries structured and clear. + +**Idea**: to structure summary by introducing custom JavaDoc tags. + +Tags should start with a prefix "utbot" to avoid possible issues with user (or some plugin) custom tags (if they have +the same names). + +**Suggested custom tags (NOT ALL OF THEM ARE USED)** + +| Name | Description | Usage example | +|----------------------------|------------------------------------|-----------------------------------------------------------------| +| `@utbot.classUnderTest` | Inline link to the enclosing class | `@utbot.methodUnderTest {@link main.IntMath}` | +| `@utbot.methodUnderTest` | Inline link to the method we test. | `@utbot.methodUnderTest {@link main.IntMath#pow(int, int)}` | +| `@utbot.expectedResult` | Value we expect to get. | `@utbot.expectedResult 4` | +| `@utbot.actualResult` | Value we got. | `@utbot.actualResult 64` | +| `@utbot.executes` | Executed condition. | `@utbot.executes {@code (k < Integer.SIZE): True}` | +| `@utbot.invokes` | Invoked method. | `@utbot.invokes {@link main.IntMath#mul(int, int)}` | +| `@utbot.triggersRecursion` | Triggered recursion. | `@utbot.recursion triggers recursion of leftBinSearch` | +| `@utbot.activatesSwitch` | Activated switch case. | `@utbot.activatesSwitch {code case 10}` | +| `@utbot.returnsFrom` | Statement we return from. | `@utbot.returnsFrom {@code return (k == 0) ? 1 : 0;}` | +| `@utbot.throwsException` | Thrown exception. | `@utbot.throwsException {@link java.lang.NullPointerException}` | +| `@utbot.caughtException` | Caught exception. | `@utbot.caughtException {@code RuntimeException e}` | + +## Settings + +There is a setting `Javadoc comment style` in the main plugin's `Settings`. It has two options: `Plain` text +and `Structured via custom Javadoc tags` (selected by default). + +![Settings](../images/utbot_settings.png) + +## View + +There are two modes the comment could be shown in IntelliJ IDEA: plain text and rendered view. + +To activate rendered mode, click on the toggle near comment. + +![Example](../images/utbot_custom_javadoc_tags.png) + +## Implementation details + +Implemented `JavadocTagInfo` to introduce our custom JavaDoc tags. + +Implemented `CustomJavadocTagProvider` and registered it in `plugin.xml` to support plugin's custom tags. + +Overrided behavior of `JavaDocumentationProvider#generateRenderedDoc` and registered it in `plugin.xml` to render our +custom JavaDoc tags correctly. + +Added a flag `USE_CUSTOM_TAGS` to settings. + +After plugin's removal, IDE doesn't recognize our custom tags. It doesn't lead to errors, but highlights tags with +yellow color. + +## Test scenarios + +Currently, the feature works only for Symbolic execution engine, so make sure the slider is on the Symbolic execution +side. + +### Default behaviour (the feature is enabled). + +1. Run plugin on any Java project and run tests generation. +2. Check if the comments are generated and pretty formatted. +3. Check that all links are clickable (parts that start with `@link` tag). + +### Manual settings (you can choose any comment style – old and new). + +1. Go to the `Settings` menu, check that the drop-down list `Javadoc comment style` exists and has two options (Plain + text + and Structured via custom Javadoc tags). +2. Select any option, click OK, run tests generation and check that the option is applied and the comments are generated + according to the chosen style. + +### Content + +First, generate comment with one style, then generate with another one and compare its content. If it differs, +please, provide code snippet and both generated comments. It could differ because currently the +style with custom Javadoc tags is a bit simplified. + +### View + +Check that the comments are rendered well. To do it, click on the toggle near the comment (see post about Rendered +view feature in IntelliJ IDEA). \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 5cf79d2a49..f54610ca84 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,47 +1,87 @@ kotlin.code.style=official -# IU, IC, PC, PY, WS... +# === IDE settings === +# Project Type +# - Community: for Java + Spring + Python (IC supported features) +# - Ultimate: for Java + Spring + Python (IU supported features) + JavaScript + Go +projectType=Ultimate + +communityEdition = Community +ultimateEdition=Ultimate + +# IU, IC, PC, PY # IC for AndroidStudio ideType=IC +ideaVersion=232.8660.185 +pycharmVersion=2023.2 +golandVersion=2023.2 +# ALL, NOJS +buildType=NOJS + +# IDE types that supports appropriate language +javaIde=IC,IU +pythonIde=IC,IU,PC,PY +jsIde=IU,PY +jsBuild=ALL +goIde=IU,GO -# In order to run Android Studion instead of Intellij Community, -# specify the path to your Android Studio installation -//androidStudioPath=D:/AS2021 +# IDE types that require Pycharm plugin +pycharmIdeType=PC,PY -pythonCommunityPluginVersion=222.4167.37 -#Version numbers: https://plugins.jetbrains.com/plugin/631-python/versions -pythonUltimatePluginVersion=222.4167.37 +# In order to run Android Studio instead of IntelliJ Community, specify the path to your Android Studio installation +#androidStudioPath=your_path_to_android_studio -junit5Version=5.8.0-RC1 +# Version numbers: https://plugins.jetbrains.com/plugin/7322-python-community-edition/versions +pythonCommunityPluginVersion=232.8660.185 +# Version numbers: https://plugins.jetbrains.com/plugin/631-python/versions +pythonUltimatePluginVersion=232.8660.185 +# Version numbers: https://plugins.jetbrains.com/plugin/9568-go/versions +goPluginVersion=232.8660.142 +# === IDE settings === + +junit5Version=5.8.2 junit4Version=4.13.2 junit4PlatformVersion=1.9.0 -mockitoVersion=3.5.13 -z3Version=4.8.9.1 -z3JavaApiVersion=4.8.9 -sootCommitHash=3adf23c3 -kotlinVersion=1.7.20 +# NOTE: Mockito versions 5+ are not compatible with Java 8: https://www.davidvlijmincx.com/posts/upgrade-to-mockito-5 +mockitoVersion=4.11.0 +mockitoInlineVersion=4.11.0 +ksmtVersion=0.5.13 +sootVersion=4.4.0-FORK-2 +kotlinVersion=1.8.0 log4j2Version=2.13.3 -coroutinesVersion=1.6.3 -collectionsVersion=0.3.4 -intellijPluginVersion=1.7.0 +coroutinesVersion=1.6.4 +collectionsVersion=0.3.5 +# after updating plugin version you should manually bump corresponding versions in plugin +# as they cannot be set from properties +# utbot-intellij/build.gradle.kts +# utbot-rd/build.gradle +# utbot-rider/build.gradle.kts +intellijPluginVersion=1.13.1 +# TODO every time you bump rd version: +# 1. regenerate all models +# 2. check if rider plugin works +# 3. search for previous RD version (as string) in entire project and update it manually in places where it has to be hardcoded +rdVersion=2023.2.0 +# to enable - add -PincludeRiderInBuild=true in build CLI +includeRiderInBuild=false jacocoVersion=0.8.8 commonsLangVersion=3.11 commonsIoVersion=2.8.0 kotlinLoggingVersion=1.8.3 ktorVersion=1.4.1 cliktVersion=3.2.0 -guavaVersion=30.0-jre +guavaVersion=32.1.2-jre apacheCommonsExecVersion=1.2 apacheCommonsTextVersion=1.9 rgxgenVersion=1.3 antlrVersion=4.9.2 -kryoVersion=5.3.0 +kryoVersion=5.4.0 kryoSerializersVersion=0.45 asmVersion=9.2 testNgVersion=7.6.0 -mockitoInlineVersion=4.0.0 -jacksonVersion = 2.12.3 -javasmtSolverZ3Version=4.8.9-sosy1 +kamlVersion=0.51.0 +jacksonVersion=2.12.3 +kotlinxSerializationVersion=1.5.0 slf4jVersion=1.7.36 eclipseAetherVersion=1.1.0 mavenWagonVersion=3.5.1 @@ -57,10 +97,41 @@ pytorchNativeVersion=1.9.1 shadowJarVersion=7.1.2 openblasVersion=0.3.10-1.5.4 arpackNgVersion=3.7.0-1.5.4 +commonsLoggingVersion=1.2 +commonsIOVersion=2.11.0 +javaxVersion=2.2 +jakartaVersion=3.1.0 +jacoDbVersion=1.4.3 +moshiVersion=1.15.1 +pythonTypesAPIHash=e5a5d9c + +# use latest Java 8 compaitable Spring and Spring Boot versions +springVersion=5.3.28 +springBootVersion=2.7.13 +springSecurityVersion=5.8.5 + +approximationsVersion=bfce4eedde +usvmVersion=72924ad -org.gradle.daemon=false -org.gradle.parallel=false -org.gradle.jvmargs="-XX:MaxHeapSize=6144m" -kotlin.compiler.execution.strategy=in-process +# configuration for build server +# +# the following options are passed to gradle command explicitly (see appropriate workflow): +# --build-cache (the same as org.gradle.caching=true) +# --no-daemon (the same as org.gradle.daemon=false) +# +# read about options precedence at: https://docs.gradle.org/current/userguide/build_environment.html +org.gradle.jvmargs="-Xmx6g" -org.gradle.caching=false \ No newline at end of file +# configuration for local compilation - much faster +# overriden by some parameters in CI, read below about each option +# +# overrided by --no-daemon +org.gradle.daemon=true +# overrided by -Dkotlin.daemon.jvm.options=-Xmx4g +kotlin.daemon.jvm.options=-Xmx4g +# overrided by --no-parallel +org.gradle.parallel=true +# not overrided, we use cache in CI as well +org.gradle.caching=true +# there is no need to override the option below because parallel execution is disabled by --no-parallel +org.gradle.workers.max=8 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f3d88b1c2f..943f0cbfa7 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 60c76b3408..6f615571cc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists \ No newline at end of file diff --git a/gradlew b/gradlew index af6708ff22..65dcd68d65 100644 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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 +# +# https://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. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -89,84 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 6d57edc706..93e3f59f13 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,19 +25,23 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,38 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/monitoring/MonitoringSettings.md b/monitoring/MonitoringSettings.md new file mode 100644 index 0000000000..0a9c05e37e --- /dev/null +++ b/monitoring/MonitoringSettings.md @@ -0,0 +1,24 @@ +# Monitoring Settings + +## Configuration files + +There are monitoring configuration files for each project in `project//monitoring.properties`. + +### Monitoring configure +The file `monitoring.properties` is passed as a java property `-Dutbot.monitoring.settings.path`. It configures `org.utbot.monitoring.MonitoringSettings` class. + +#### Properties description: +- `project` is a name of project that will be run in monitoring. +- `classTimeoutSeconds` is a unit-test generation timeout for one class. +- `runTimeoutMinutes` is a timeout for one whole run of the project. +- `fuzzingRatios` is a list of numbers that configure the ratio of fuzzing time to total generation time. + +## Which project can be run? + +### Prerequisites + +Firstly, you should read [this](../utbot-junit-contest/README.md) paper about available projects and how to extend them. + +### How to add projects to monitoring + +To add a project to monitoring you should create a folder with a project name and create a file `monitoring.properties` with needed configurations. diff --git a/monitoring/build_aggregated_data.py b/monitoring/build_aggregated_data.py deleted file mode 100644 index da2a873a99..0000000000 --- a/monitoring/build_aggregated_data.py +++ /dev/null @@ -1,129 +0,0 @@ -import argparse -import json -from os import listdir -from os.path import isfile, join -from time import time -from typing import Iterator - -from monitoring_settings import JSON_VERSION -from utils import * - - -def get_file_seq(input_data_dir: str) -> Iterator[str]: - """ - Get all files from specified directory - :param input_data_dir: path to directory with files - :return: sequence of filepaths - """ - for filename in listdir(input_data_dir): - path = join(input_data_dir, filename) - if isfile(path): - yield path - - -def check_stats(stats: dict, args: argparse.Namespace) -> bool: - """ - Checks timestamp and version of given statistics - :param stats: dictionary with statistics and metadata - :param args: parsed program arguments - :return: is timestamp and version match - """ - try: - timestamp = stats["metadata"]["timestamp"] - timestamp_match = args.timestamp_from <= timestamp <= args.timestamp_to - json_version_match = stats["version"] == JSON_VERSION - return timestamp_match and json_version_match - except: - return False - - -def get_stats_seq(args: argparse.Namespace) -> Iterator[dict]: - """ - Get statistics with metadata matched specified period - :param args: parsed program arguments - :return: sequence of statistics with metadata filtered by version and timestamp - """ - for file in get_file_seq(args.input_data_dir): - with open(file, "r") as f: - stats = json.load(f) - if check_stats(stats, args): - yield stats - - -def transform_target_stats(stats: dict) -> dict: - """ - Transform metrics by computing total coverage - :param stats: metrics - :return: transformed metrics - """ - common_prefix = "covered_instructions" - denum = stats["total_instructions"] - - nums_keys = [(key, key.removeprefix(common_prefix)) for key in stats.keys() if key.startswith(common_prefix)] - - for (key, by) in nums_keys: - num = stats[key] - stats["total_coverage" + by] = 100 * num / denum if denum != 0 else 0 - del stats[key] - - del stats["total_instructions"] - - return stats - - -def aggregate_stats(stats_seq: Iterator[dict]) -> List[dict]: - """ - Aggregate list of metrics and parameters into list of transformed metrics and parameters grouped by targets - :param stats_seq: sequence of metrics and parameters - :return: list of metrics and parameters grouped by targets - """ - result = get_default_metrics_dict() - - for stats in stats_seq: - targets = stats["targets"] - timestamp = stats["metadata"]["timestamp"] - for target in targets: - full_name = f'{target["id"]}-{target["version"]}' - new_data = result[full_name] - for target_stats in target["metrics"]: - new_data["metrics"].append(transform_target_stats(target_stats)) - for target_params in target["parameters"]: - target_params["timestamp"] = timestamp - new_data["parameters"].append(target_params) - - return postprocess_targets(result) - - -def get_args(): - parser = argparse.ArgumentParser() - parser.add_argument( - '--input_data_dir', required=True, - help='input directory with data', type=str - ) - parser.add_argument( - '--output_file', required=True, - help='output file', type=str - ) - parser.add_argument( - '--timestamp_from', help='timestamp started collection from', - type=int, default=0 - ) - parser.add_argument( - '--timestamp_to', help='timestamp finished collection to', - type=int, default=int(time()) - ) - - args = parser.parse_args() - return args - - -def main(): - args = get_args() - stats_seq = get_stats_seq(args) - result = aggregate_stats(stats_seq) - with open(args.output_file, "w") as f: - json.dump(result, f, indent=4) - - -if __name__ == '__main__': - main() diff --git a/monitoring/insert_metadata.py b/monitoring/insert_metadata.py index e202762cb2..10be3c4b45 100644 --- a/monitoring/insert_metadata.py +++ b/monitoring/insert_metadata.py @@ -1,27 +1,16 @@ import argparse import json +import re import subprocess +from collections import OrderedDict from datetime import datetime from os import environ -from os.path import exists from platform import uname from time import time -from typing import Optional +from typing import Optional, List from monitoring_settings import JSON_VERSION -from utils import * - - -def load(json_file: str) -> Optional[any]: - """ - Try load object from json file - :param json_file: path to json file - :return: object from given json file or None - """ - if exists(json_file): - with open(json_file, "r") as f: - return json.load(f) - return None +from utils import load def try_get_output(args: str) -> Optional[str]: @@ -91,17 +80,142 @@ def build_metadata(args: argparse.Namespace) -> dict: return metadata +def build_target(target_name: str) -> dict: + return { + "target": target_name, + "summarised": [], + "by_class": OrderedDict() + } + + +def transform_metrics(metrics: dict) -> dict: + """ + Transform given metrics with calculation coverage + :param metrics: given metrics + :return: transformed metrics + """ + result = OrderedDict() + + instr_count_prefix = "covered_bytecode_instructions" + total_instr_count_prefix = "total_bytecode_instructions" + + coverage_prefix = "total_bytecode_instruction_coverage" + + total_count = 0 + for metric in metrics: + if metric.startswith(total_instr_count_prefix): + total_count = metrics[metric] + break + + for metric in metrics: + if metric.startswith(total_instr_count_prefix): + continue + if metric.startswith(instr_count_prefix): + coverage = metrics[metric] / total_count if total_count > 0 else 0.0 + result[coverage_prefix + metric.removeprefix(instr_count_prefix)] = coverage + else: + result[metric] = metrics[metric] + + return result + + +def build_data(parameters: dict, metrics: dict) -> dict: + return { + "parameters": { + **parameters + }, + "metrics": { + **transform_metrics(metrics) + } + } + + +def build_by_class(class_name: str) -> dict: + return { + "class_name": class_name, + "data": [] + } + + +def update_from_class(by_class: dict, class_item: dict, parameters: dict): + """ + Update class object using given class_item + :param by_class: dictionary with classname keys + :param class_item: class metrics of current run + :param parameters: parameters of current run + """ + class_name = class_item["class_name"] + if class_name not in by_class: + by_class[class_name] = build_by_class(class_name) + + metrics = class_item["metrics"] + by_class[class_name]["data"].append( + build_data(parameters, metrics) + ) + + +def update_from_target(targets: dict, target_item: dict, parameters: dict): + """ + Update targets using given target_item + :param targets: dictionary with target keys + :param target_item: metrics of current run + :param parameters: parameters of current run + """ + target_name = target_item["target"] + if target_name not in targets: + targets[target_name] = build_target(target_name) + + summarised_metrics = target_item["summarised_metrics"] + targets[target_name]["summarised"].append( + build_data(parameters, summarised_metrics) + ) + + for class_item in target_item["metrics_by_class"]: + update_from_class(targets[target_name]["by_class"], class_item, parameters) + + +def update_from_stats(targets: dict, stats: dict): + """ + Updates targets using given statistics + :param targets: dictionary with target keys + :param stats: target object + """ + parameters = stats["parameters"] + for target_item in stats["targets"]: + update_from_target(targets, target_item, parameters) + + +def postprocess_by_class(by_class: dict) -> List[dict]: + """ + Transform dictionary with classname keys into array with class objects + :param by_class: dictionary with classname keys + :return: array of class objects + """ + return list(by_class.values()) + + +def postprocess_targets(targets: dict) -> List[dict]: + """ + Transform dictionary with target keys into array with target objects + :param targets: dictionary with target keys + :return: array of targets + """ + result = [] + for target in targets.values(): + target["by_class"] = postprocess_by_class(target["by_class"]) + result.append(target) + return result + + def build_targets(stats_array: List[dict]) -> List[dict]: """ Collect and group statistics by target :param stats_array: list of dictionaries with parameters and metrics :return: list of metrics and parameters grouped by target """ - result = get_default_metrics_dict() + result = OrderedDict() for stats in stats_array: - target = stats['parameters']['target'] - del stats['parameters']['target'] - update_target(result[target], stats) + update_from_stats(result, stats) return postprocess_targets(result) diff --git a/monitoring/monitoring.properties b/monitoring/monitoring.properties deleted file mode 100644 index e3062857d0..0000000000 --- a/monitoring/monitoring.properties +++ /dev/null @@ -1,4 +0,0 @@ -project=guava -classTimeoutMillis=20 -runTries=1 -runTimeoutMinutes=20 \ No newline at end of file diff --git a/monitoring/monitoring_settings.py b/monitoring/monitoring_settings.py index 301e7b7ae3..8b0fb83a0f 100644 --- a/monitoring/monitoring_settings.py +++ b/monitoring/monitoring_settings.py @@ -1,9 +1,4 @@ """ Json format version. """ -JSON_VERSION = 1 - -""" -Default version for projects without it. -""" -DEFAULT_PROJECT_VERSION = "0" +JSON_VERSION = 2 diff --git a/monitoring/prepare_metrics.py b/monitoring/prepare_metrics.py new file mode 100644 index 0000000000..59da56fca0 --- /dev/null +++ b/monitoring/prepare_metrics.py @@ -0,0 +1,150 @@ +import argparse +import json +from typing import List + +from utils import load + + +def remove_in_class(name: str) -> str: + in_class = "_in_class" + idx = name.find(in_class) + if idx == -1: + return name + return name[:idx] + name[idx:].removeprefix(in_class) + + +def update_from_counter_name(key_word: str, name: str, labels: dict) -> str: + if name == f"total_{key_word}": + labels["type"] = "total" + return key_word + if name.startswith(key_word): + labels["type"] = name.removeprefix(f"{key_word}_") + return key_word + return name + + +def update_from_coverage(name: str, labels: dict) -> str: + coverage_key = "bytecode_instruction_coverage" + idx = name.find(coverage_key) + if idx == -1: + return name + labels["type"] = name[:idx - 1] + source = name[idx:].removeprefix(f"{coverage_key}") + if len(source) > 0: + source = source.removeprefix("_by_") + if source == "classes": + labels["type"] = "averaged_by_classes" + else: + labels["source"] = source + if "source" not in labels: + labels["source"] = "all" + return coverage_key + + +def build_metric_struct(name: str, value: any, labels: dict) -> dict: + name = remove_in_class(name) + name = update_from_counter_name("classes", name, labels) + name = update_from_counter_name("methods", name, labels) + name = update_from_coverage(name, labels) + + if type(value) == bool: + value = int(value) + name = f"test_generation_{name}" + elif type(value) == int: + name = f"{name}_total" + + name = f"utbot_{name}" + + return { + "metric": name, + "labels": labels, + "value": value + } + + +def build_metrics_from_data(data: dict, labels: dict) -> List[dict]: + result = [] + fuzzing_ratio = data["parameters"]["fuzzing_ratio"] + new_labels = { + **labels, + "fuzzing_ratio": fuzzing_ratio + } + metrics = data["metrics"] + for metric in metrics: + result.append(build_metric_struct(metric, metrics[metric], new_labels.copy())) + return result + + +def build_metrics_from_data_array(metrics: List[dict], labels: dict) -> List[dict]: + result = [] + for metric in metrics: + result.extend(build_metrics_from_data(metric, labels)) + return result + + +def build_metrics_from_target(target: dict, run_id: str) -> List[dict]: + result = [] + project = target["target"] + + result.extend(build_metrics_from_data_array( + target["summarised"], + { + "run_id": run_id, + "project": project, + "class": "All" + } + )) + + for class_item in target["by_class"]: + class_name = class_item["class_name"] + result.extend(build_metrics_from_data_array( + class_item["data"], + { + "run_id": run_id, + "project": project, + "class": class_name + } + )) + + return result + + +def build_metrics_from_targets(targets: List[dict], run_id: str) -> List[dict]: + metrics = [] + for target in targets: + metrics.extend(build_metrics_from_target(target, run_id)) + return metrics + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--stats_file', required=True, + help='files with statistics after insertion metadata', type=str + ) + parser.add_argument( + '--output_file', required=True, + help='output file', type=str + ) + + args = parser.parse_args() + return args + + +def extract_run_id(text: str): + idx = text.find('-') + return "run" + text[idx:] + + +def main(): + args = get_args() + stats = load(args.stats_file) + run_id = extract_run_id(stats["metadata"]["source"]["id"]) + metrics = build_metrics_from_targets(stats["targets"], run_id) + metrics.sort(key=lambda x: x["metric"]) + with open(args.output_file, "w") as f: + json.dump(metrics, f, indent=4) + + +if __name__ == "__main__": + main() diff --git a/monitoring/projects/fescar/monitoring.properties b/monitoring/projects/fescar/monitoring.properties new file mode 100644 index 0000000000..cabd34e51e --- /dev/null +++ b/monitoring/projects/fescar/monitoring.properties @@ -0,0 +1,4 @@ +project=fescar +classTimeoutSeconds=20 +runTimeoutMinutes=20 +fuzzingRatios=0.0;0.05;1.0 \ No newline at end of file diff --git a/monitoring/projects/guava/monitoring.properties b/monitoring/projects/guava/monitoring.properties new file mode 100644 index 0000000000..0555a8c44a --- /dev/null +++ b/monitoring/projects/guava/monitoring.properties @@ -0,0 +1,4 @@ +project=guava +classTimeoutSeconds=20 +runTimeoutMinutes=20 +fuzzingRatios=0.0;0.05;1.0 \ No newline at end of file diff --git a/monitoring/projects/pdfbox/monitoring.properties b/monitoring/projects/pdfbox/monitoring.properties new file mode 100644 index 0000000000..3878e706fe --- /dev/null +++ b/monitoring/projects/pdfbox/monitoring.properties @@ -0,0 +1,4 @@ +project=pdfbox +classTimeoutSeconds=20 +runTimeoutMinutes=20 +fuzzingRatios=0.0;0.05;1.0 \ No newline at end of file diff --git a/monitoring/projects/seata/monitoring.properties b/monitoring/projects/seata/monitoring.properties new file mode 100644 index 0000000000..9cb7421c9a --- /dev/null +++ b/monitoring/projects/seata/monitoring.properties @@ -0,0 +1,4 @@ +project=seata +classTimeoutSeconds=20 +runTimeoutMinutes=20 +fuzzingRatios=0.0;0.05;1.0 \ No newline at end of file diff --git a/monitoring/projects/spoon/monitoring.properties b/monitoring/projects/spoon/monitoring.properties new file mode 100644 index 0000000000..41b56c94f0 --- /dev/null +++ b/monitoring/projects/spoon/monitoring.properties @@ -0,0 +1,4 @@ +project=spoon +classTimeoutSeconds=20 +runTimeoutMinutes=20 +fuzzingRatios=0.0;0.05;1.0 \ No newline at end of file diff --git a/monitoring/utils.py b/monitoring/utils.py index d5fd1a5a2f..c897f46df6 100644 --- a/monitoring/utils.py +++ b/monitoring/utils.py @@ -1,56 +1,15 @@ -import re -from collections import defaultdict -from typing import List +import json +from os.path import exists +from typing import Optional -from monitoring_settings import DEFAULT_PROJECT_VERSION - -def parse_name_and_version(full_name: str) -> tuple[str, str]: - """ - Parse from string name and version of project - :param full_name: string with format - - :return: pair of name and version strings - """ - regex = re.compile(r'([^.]+)-([\d.]+)') - result = regex.fullmatch(full_name) - if result is None: - return full_name, DEFAULT_PROJECT_VERSION - name = result.group(1) - version = result.group(2) - return name, version - - -def postprocess_targets(targets: dict) -> List[dict]: - """ - Transform dictionary with fullname target keys into array with target objects - :param targets: dictionary with fullname target keys - :return: array of targets - """ - result = [] - for target in targets: - (name, version) = parse_name_and_version(target) - result.append({ - 'id': name, - 'version': version, - **targets[target] - }) - return result - - -def get_default_metrics_dict() -> dict: - return defaultdict(lambda: { - 'parameters': [], - 'metrics': [] - }) - - -def update_target(target: dict, stats: dict) -> dict: +def load(json_file: str) -> Optional[any]: """ - Update dictionary with target by new stats - :param target: dictionary with target metrics and parameters - :param stats: new metrics and parameters - :return: updated target dictionary + Try load object from json file + :param json_file: path to json file + :return: object from given json file or None """ - target['parameters'].append(stats['parameters']) - target['metrics'].append(stats['metrics']) - return target + if exists(json_file): + with open(json_file, "r") as f: + return json.load(f) + return None diff --git a/scripts/project/json_to_prometheus.py b/scripts/project/json_to_prometheus.py new file mode 100644 index 0000000000..0a00270f9b --- /dev/null +++ b/scripts/project/json_to_prometheus.py @@ -0,0 +1,36 @@ +import sys +import json + +with open(sys.argv[1]) as metrics_raw: + metrics_json = json.load(metrics_raw) + +# metrics is a json list e.g.: +# [ +# { +# "metric": "total_classes", +# "labels": { +# "project": "guava", +# "fuzzing_ratio": 0.1 +# }, +# "value": 20 +# }, +# { +# "metric": "testcases_generated", +# "labels": { +# "project": "guava", +# "fuzzing_ratio": 0.1 +# }, +# "value": 1042 +# } +# ] +# +# the loop below iterates over each list item and constructs metrics set +metrics_set_str = "" +for metric in metrics_json: + labels_set_str = "" + comma = "" + for label, value in metric['labels'].items(): + labels_set_str = f'{labels_set_str}{comma}{label}=\"{value}\"' + comma = "," + metrics_set_str += f'{metric["metric"]}{{{labels_set_str}}} {metric["value"]}\n' +print(metrics_set_str) diff --git a/scripts/project/logging.sh b/scripts/project/logging.sh new file mode 100644 index 0000000000..52931a78cc --- /dev/null +++ b/scripts/project/logging.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +FILEBEAT_DIR=${1} +LOGSTASH_HOST=${2} + +cat > ${FILEBEAT_DIR}/filebeat.yml < from(zipTree(artifact.getFile())) { diff --git a/utbot-analytics/build.gradle b/utbot-analytics/build.gradle index 78257d605d..5838ac7591 100644 --- a/utbot-analytics/build.gradle +++ b/utbot-analytics/build.gradle @@ -14,7 +14,9 @@ dependencies { testImplementation project(':utbot-sample') testImplementation group: 'junit', name: 'junit', version: junit4Version - implementation "com.github.UnitTestBot:soot:${sootCommitHash}" + implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { + exclude group:'com.google.guava', module:'guava' + } implementation group: 'com.github.haifengl', name: 'smile-kotlin', version: '2.6.0' implementation group: 'com.github.haifengl', name: 'smile-plot', version: '2.6.0' implementation group: 'com.github.haifengl', name: 'smile-core', version: '2.6.0' @@ -31,20 +33,10 @@ dependencies { implementation group: 'org.apache.commons', name: 'commons-text', version: '1.9' implementation group: 'com.github.javaparser', name: 'javaparser-core', version: '3.22.1' + testImplementation project(':utbot-testing') testImplementation project(':utbot-framework').sourceSets.test.output } -test { - minHeapSize = "128m" - maxHeapSize = "3072m" - - jvmArgs '-XX:MaxHeapSize=3072m' - - useJUnitPlatform() { - excludeTags 'slow', 'IntegrationTest' - } -} - processResources { configurations.mlmodels.resolvedConfiguration.resolvedArtifacts.each { artifact -> from(zipTree(artifact.getFile())) { @@ -53,17 +45,27 @@ processResources { } } -jar { - dependsOn classes - manifest { - attributes 'Main-Class': 'org.utbot.QualityAnalysisKt' - } - - dependsOn configurations.runtimeClasspath - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } - - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - zip64 = true -} \ No newline at end of file +// TODO if you need utbot-analytics fat jar, use shadow jar to create a SEPARATE task for a fat jar. +// Do not use main jar for a fat jar, because it breaks Gradle conflict resolution, here's how: +// 1. utbot-analytics depends on library A version 1.0 (and adds it to own main jar) +// 2. utbot-junit-contest depends on utbot-analytics and library A version 1.1 +// 3. Both library A version 1.0 and version 1.1 end up on the classpath and it's a matter of chance which one is earlier +// If utbot-analytics were to only declare its dependency on library A version 1.0 and not force it by adding it to a +// main jar, then Gradle would be able to recognize the conflict of library A version 1.0 and version 1.1 and resolve +// it according to a conflict resolution strategy, which by default picks the latest version, which works in most cases. +// But if you put library A version 1.0 into some fat jar, Gradle will no longer be able to exclude it from the fat jar +// in favor of a newer version when it needs to resolve dependency conflicts. +//jar { +// dependsOn classes +// manifest { +// attributes 'Main-Class': 'org.utbot.QualityAnalysisKt' +// } +// +// dependsOn configurations.runtimeClasspath +// from { +// configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } +// } +// +// duplicatesStrategy = DuplicatesStrategy.EXCLUDE +// zip64 = true +//} \ 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 index 6786c9847a..ddf1322659 100644 --- a/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureExtractorImpl.kt +++ b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureExtractorImpl.kt @@ -1,7 +1,7 @@ package org.utbot.features import org.utbot.analytics.FeatureExtractor -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import org.utbot.engine.selectors.strategies.StatementsStatistics import org.utbot.engine.selectors.strategies.SubpathStatistics diff --git a/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetition.kt b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetition.kt index 49f9427678..4c80f18dda 100644 --- a/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetition.kt +++ b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetition.kt @@ -2,7 +2,7 @@ package org.utbot.features import org.utbot.analytics.EngineAnalyticsContext import org.utbot.analytics.FeatureProcessor -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import org.utbot.framework.UtSettings import soot.jimple.Stmt @@ -107,7 +107,7 @@ class FeatureProcessorWithStatesRepetition( } } -internal class RewardEstimator { +class RewardEstimator { fun calculateRewards(testCases: List): Map { val rewards = mutableMapOf() diff --git a/utbot-analytics/src/main/kotlin/org/utbot/features/UtExpressionStructureCounter.kt b/utbot-analytics/src/main/kotlin/org/utbot/features/UtExpressionStructureCounter.kt index f6f209875c..b1c5661919 100644 --- a/utbot-analytics/src/main/kotlin/org/utbot/features/UtExpressionStructureCounter.kt +++ b/utbot-analytics/src/main/kotlin/org/utbot/features/UtExpressionStructureCounter.kt @@ -34,21 +34,6 @@ val featureIndex = listOf( UtBoolOpExpression::class.simpleName, UtIsExpression::class.simpleName, UtIteExpression::class.simpleName, - UtStringConst::class.simpleName, - UtConcatExpression::class.simpleName, - UtConvertToString::class.simpleName, - UtStringLength::class.simpleName, - UtStringPositiveLength::class.simpleName, - UtStringCharAt::class.simpleName, - UtStringEq::class.simpleName, - UtSubstringExpression::class.simpleName, - UtReplaceExpression::class.simpleName, - UtStartsWithExpression::class.simpleName, - UtEndsWithExpression::class.simpleName, - UtIndexOfExpression::class.simpleName, - UtContainsExpression::class.simpleName, - UtToStringExpression::class.simpleName, - UtSeqLiteral::class.simpleName, TREES, MAX_NODES, MIN_NODES, @@ -160,6 +145,7 @@ class UtExpressionStructureCounter(private val input: Iterable) : override fun visit(expr: UtAddNoOverflowExpression) = multipleExpressions(expr.left, expr.right) override fun visit(expr: UtSubNoOverflowExpression) = multipleExpressions(expr.left, expr.right) + override fun visit(expr: UtMulNoOverflowExpression)= multipleExpressions(expr.left, expr.right) override fun visit(expr: UtNegExpression): NestStat { val stat = buildState(expr.variable.expr) @@ -168,6 +154,13 @@ class UtExpressionStructureCounter(private val input: Iterable) : return stat } + override fun visit(expr: UtBvNotExpression): NestStat { + val stat = buildState(expr.variable.expr) + stat.level++ + stat.nodes++ + return stat + } + override fun visit(expr: UtCastExpression): NestStat { val stat = buildState(expr.variable.expr) stat.level++ @@ -216,59 +209,6 @@ class UtExpressionStructureCounter(private val input: Iterable) : ) } - //const string value - override fun visit(expr: UtStringConst) = NestStat() - - override fun visit(expr: UtConcatExpression) = multipleExpression(expr.parts) - - override fun visit(expr: UtConvertToString): NestStat { - val stat = buildState(expr.expression) - stat.level++ - stat.nodes++ - return stat - } - - override fun visit(expr: UtStringToInt): NestStat { - val stat = buildState(expr.expression) - stat.level++ - stat.nodes++ - return stat - } - - override fun visit(expr: UtStringLength): NestStat { - val stat = buildState(expr.string) - stat.level++ - stat.nodes++ - return stat - } - - override fun visit(expr: UtStringPositiveLength): NestStat { - val stat = buildState(expr.string) - stat.level++ - stat.nodes++ - return stat - } - - override fun visit(expr: UtStringCharAt) = multipleExpressions(expr.string, expr.index) - - override fun visit(expr: UtStringEq) = multipleExpressions(expr.left, expr.right) - - override fun visit(expr: UtSubstringExpression) = multipleExpressions(expr.string, expr.beginIndex, expr.length) - - override fun visit(expr: UtReplaceExpression) = multipleExpressions(expr.string, expr.regex, expr.replacement) - - override fun visit(expr: UtStartsWithExpression) = multipleExpressions(expr.string, expr.prefix) - - override fun visit(expr: UtEndsWithExpression) = multipleExpressions(expr.string, expr.suffix) - - override fun visit(expr: UtIndexOfExpression) = multipleExpressions(expr.string, expr.substring) - - override fun visit(expr: UtContainsExpression) = multipleExpressions(expr.string, expr.substring) - - override fun visit(expr: UtToStringExpression) = multipleExpressions(expr.notNullExpr, expr.isNull) - - override fun visit(expr: UtSeqLiteral) = NestStat() - private fun multipleExpressions(vararg expressions: UtExpression) = multipleExpression(expressions.toList()) private fun multipleExpression(expressions: List): NestStat { @@ -311,14 +251,6 @@ class UtExpressionStructureCounter(private val input: Iterable) : override fun visit(expr: UtArrayApplyForAll): NestStat { return NestStat() } - - override fun visit(expr: UtStringToArray): NestStat { - return NestStat() - } - - override fun visit(expr: UtArrayToString): NestStat { - return NestStat() - } } data class NestStat(var nodes: Int = 1, var level: Int = 1) diff --git a/utbot-analytics/src/main/kotlin/org/utbot/visual/AbstractHtmlReport.kt b/utbot-analytics/src/main/kotlin/org/utbot/visual/AbstractHtmlReport.kt index 9f1673a57e..ce7ff03e6f 100644 --- a/utbot-analytics/src/main/kotlin/org/utbot/visual/AbstractHtmlReport.kt +++ b/utbot-analytics/src/main/kotlin/org/utbot/visual/AbstractHtmlReport.kt @@ -1,5 +1,6 @@ package org.utbot.visual +import org.utbot.common.dateTimeFormatter import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -7,8 +8,6 @@ import java.time.format.DateTimeFormatter abstract class AbstractHtmlReport(bodyWidth: Int = 600) { val builder = HtmlBuilder(bodyMaxWidth = bodyWidth) - private val dateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy_HH-mm-ss") - private fun nameWithDate() = "logs/Report_" + dateTimeFormatter.format(LocalDateTime.now()) + ".html" diff --git a/utbot-analytics/src/test/kotlin/org/utbot/features/FeatureProcessorWithRepetitionTest.kt b/utbot-analytics/src/test/kotlin/org/utbot/features/FeatureProcessorWithRepetitionTest.kt index 8ab741488c..b2020d374b 100644 --- a/utbot-analytics/src/test/kotlin/org/utbot/features/FeatureProcessorWithRepetitionTest.kt +++ b/utbot-analytics/src/test/kotlin/org/utbot/features/FeatureProcessorWithRepetitionTest.kt @@ -5,9 +5,9 @@ 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.tests.infrastructure.UtValueTestCaseChecker import org.utbot.testcheckers.eq import org.utbot.testcheckers.withFeaturePath +import org.utbot.testing.UtValueTestCaseChecker import java.io.File import java.io.FileInputStream diff --git a/utbot-android-studio/build.gradle.kts b/utbot-android-studio/build.gradle.kts new file mode 100644 index 0000000000..96197ffc94 --- /dev/null +++ b/utbot-android-studio/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("org.jetbrains.intellij") version "1.13.1" +} + +intellij { + /* + The list of Android Studio releases can be found here https://plugins.jetbrains.com/docs/intellij/android-studio-releases-list.html + For each release a compatible Intellij Idea version can be found in the right column. Specify it in "version.set("...") + + NOTE!!! + We use Android Studio Chipmunk (2021.2.1), although Android Studio Dolphin (2021.3.1) has been released. + The reason is that a version of Kotlin plugin compatible with Android Studio is required. + The list of Kotlin plugin releases can be found here https://plugins.jetbrains.com/plugin/6954-kotlin/versions/stable + The last compatible with AS plugin version on 19 Oct 2022 is Kotlin 212-1.7.10-release-333-AS5457.46, + it is not compatible with Dolphin release (https://plugins.jetbrains.com/plugin/6954-kotlin/versions/stable/193255). + */ + + val androidPlugins = listOf("org.jetbrains.android") + + val jvmPlugins = listOf( + "java", + "org.jetbrains.kotlin:212-1.7.10-release-333-AS5457.46" + ) + + plugins.set(jvmPlugins + androidPlugins) + + version.set("212.5712.43") + type.set("IC") +} + +project.tasks.asMap["runIde"]?.enabled = false + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} \ No newline at end of file diff --git a/utbot-android-studio/src/main/kotlin/org/androidstudio/plugin/util/UtAndroidGradleJavaProjectModelModifier.kt b/utbot-android-studio/src/main/kotlin/org/androidstudio/plugin/util/UtAndroidGradleJavaProjectModelModifier.kt new file mode 100644 index 0000000000..4b5665d7e2 --- /dev/null +++ b/utbot-android-studio/src/main/kotlin/org/androidstudio/plugin/util/UtAndroidGradleJavaProjectModelModifier.kt @@ -0,0 +1,117 @@ +package org.androidstudio.plugin.util + +import com.android.tools.idea.gradle.AndroidGradleJavaProjectModelModifier +import com.android.tools.idea.gradle.dsl.api.GradleBuildModel +import com.android.tools.idea.gradle.dsl.api.dependencies.ArtifactDependencySpec +import com.android.tools.idea.gradle.dsl.api.dependencies.CommonConfigurationNames +import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker +import com.android.tools.idea.gradle.project.sync.GradleSyncListener +import com.android.tools.idea.gradle.project.sync.idea.GradleSyncExecutor +import com.android.tools.idea.gradle.util.GradleUtil +import com.android.tools.idea.project.AndroidProjectInfo +import com.android.tools.idea.projectsystem.TestArtifactSearchScopes +import com.google.wireless.android.sdk.stats.GradleSyncStats +import com.intellij.ide.plugins.PluginManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.command.undo.BasicUndoableAction +import com.intellij.openapi.command.undo.UndoManager +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx +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.libraries.Library +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.pom.java.LanguageLevel +import com.intellij.util.containers.ContainerUtil +import org.jetbrains.concurrency.AsyncPromise +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.rejectedPromise + +class UtAndroidGradleJavaProjectModelModifier : AndroidGradleJavaProjectModelModifier() { + override fun addExternalLibraryDependency( + modules: Collection, + descriptor: ExternalLibraryDescriptor, + scope: DependencyScope + ): Promise? { + val module = ContainerUtil.getFirstItem(modules) ?: return null + val dependencySpec = ArtifactDependencySpec.create(descriptor.libraryArtifactId, descriptor.libraryGroupId, descriptor.preferredVersion) + return addExternalLibraryDependency(module, dependencySpec, scope) + } + + private fun addExternalLibraryDependency( + module: Module, + dependencySpec: ArtifactDependencySpec, + scope: DependencyScope, + ): Promise? { + val project = module.project + val openedFile = FileEditorManagerEx.getInstanceEx(project).currentFile + val buildModelsToUpdate: MutableList = ArrayList() + + val buildModel = GradleBuildModel.get(module) ?: return null + val configurationName = getConfigurationName(module, scope, openedFile) + val dependencies = buildModel.dependencies() + dependencies.addArtifact(configurationName, dependencySpec) + buildModelsToUpdate.add(buildModel) + + WriteCommandAction.writeCommandAction(project).withName("Add Gradle Library Dependency").run { + buildModelsToUpdate.forEach { buildModel -> buildModel.applyChanges() } + registerUndoAction(project) + } + + return doAndroidGradleSync(project, GradleSyncStats.Trigger.TRIGGER_MODIFIER_ADD_LIBRARY_DEPENDENCY) + } + + private fun getConfigurationName(module: Module, scope: DependencyScope, openedFile: VirtualFile?): String = + GradleUtil.mapConfigurationName( + getLegacyConfigurationName(module, scope, openedFile), + GradleUtil.getAndroidGradleModelVersionInUse(module), + false + ) + + private fun getLegacyConfigurationName( + module: Module, + scope: DependencyScope, + openedFile: VirtualFile? + ): String { + if (!scope.isForProductionCompile) { + val testScopes = TestArtifactSearchScopes.getInstance(module) + if (testScopes != null && openedFile != null) { + return if (testScopes.isAndroidTestSource(openedFile)) CommonConfigurationNames.ANDROID_TEST_COMPILE else CommonConfigurationNames.TEST_COMPILE + } + } + return CommonConfigurationNames.COMPILE + } + + private fun registerUndoAction(project: Project) { + UndoManager.getInstance(project).undoableActionPerformed(object : BasicUndoableAction() { + + override fun undo() { + doAndroidGradleSync(project, GradleSyncStats.Trigger.TRIGGER_MODIFIER_ACTION_UNDONE) + + } + + override fun redo() { + doAndroidGradleSync(project, GradleSyncStats.Trigger.TRIGGER_MODIFIER_ACTION_REDONE) + } + }) + } + + private fun doAndroidGradleSync(project: Project, trigger: GradleSyncStats.Trigger): AsyncPromise { + val promise = AsyncPromise() + val request = GradleSyncInvoker.Request(trigger) + val listener = object : GradleSyncListener { + override fun syncSucceeded(project: Project) { + promise.setResult(null) + } + + override fun syncFailed(project: Project, errorMessage: String) { + promise.setError(errorMessage) + } + } + GradleSyncExecutor(project).sync(request, listener) + + return promise + } +} \ No newline at end of file diff --git a/utbot-android-studio/src/main/kotlin/org/androidstudio/plugin/util/UtAndroidGradleJavaProjectModelModifierWrapper.kt b/utbot-android-studio/src/main/kotlin/org/androidstudio/plugin/util/UtAndroidGradleJavaProjectModelModifierWrapper.kt new file mode 100644 index 0000000000..4eba5a7db9 --- /dev/null +++ b/utbot-android-studio/src/main/kotlin/org/androidstudio/plugin/util/UtAndroidGradleJavaProjectModelModifierWrapper.kt @@ -0,0 +1,63 @@ +package org.androidstudio.plugin.util + +import com.android.tools.idea.model.AndroidModel +import com.intellij.facet.ProjectFacetManager +import com.intellij.ide.plugins.PluginManager +import com.intellij.openapi.extensions.PluginId +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.impl.IdeaProjectModelModifier +import com.intellij.openapi.roots.libraries.Library +import com.intellij.pom.java.LanguageLevel +import org.jetbrains.android.facet.AndroidFacet +import org.jetbrains.concurrency.Promise + +/* +NOTE: this is a wrapper for [UtAndroidGradleJavaProjectModelModifier]. +The purpose of this wrapper is to avoid inheritance of [AndroidGradleJavaProjectModelModifier] +because it leads to crashes when Android plugin is disabled. + */ +class UtAndroidGradleJavaProjectModelModifierWrapper(val project: Project): IdeaProjectModelModifier(project) { + + override fun addExternalLibraryDependency( + modules: Collection, + descriptor: ExternalLibraryDescriptor, + scope: DependencyScope + ): Promise? { + if (!isAndroidGradleProject(project)) { + return null + } + + // NOTE: we use such DependencyScope to obtain `implementation`, not `testImplementation` + // to deal with androidTest modules (there is no way to add `androidTestImplementation` additionally. + return UtAndroidGradleJavaProjectModelModifier().addExternalLibraryDependency(modules, descriptor, DependencyScope.COMPILE) + } + + override fun addModuleDependency( + from: Module, + to: Module, + scope: DependencyScope, + exported: Boolean + ): Promise? = null + + override fun addLibraryDependency( + from: Module, + library: Library, + scope: DependencyScope, + exported: Boolean + ): Promise? = null + + override fun changeLanguageLevel(module: Module, level: LanguageLevel): Promise? = null + + private fun isAndroidGradleProject(project: Project): Boolean { + val pluginId = PluginId.findId("org.jetbrains.android") + if (pluginId == null || PluginManager.getInstance().findEnabledPlugin(pluginId) == null) { + return false + } + + return ProjectFacetManager.getInstance(project).getFacets(AndroidFacet.ID).stream() + .anyMatch { AndroidModel.isRequired(it) } + } +} \ No newline at end of file diff --git a/utbot-api/build.gradle.kts b/utbot-api/build.gradle.kts index 5f02cd7356..e69de29bb2 100644 --- a/utbot-api/build.gradle.kts +++ b/utbot-api/build.gradle.kts @@ -1,12 +0,0 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - -plugins { - id("com.github.johnrengelman.shadow") version "7.1.2" -} - -tasks { - withType { - archiveClassifier.set(" ") - minimize() - } -} \ No newline at end of file diff --git a/utbot-api/src/main/java/org/utbot/api/exception/UtMockAssumptionViolatedException.java b/utbot-api/src/main/java/org/utbot/api/exception/UtMockAssumptionViolatedException.java new file mode 100644 index 0000000000..25a4b58691 --- /dev/null +++ b/utbot-api/src/main/java/org/utbot/api/exception/UtMockAssumptionViolatedException.java @@ -0,0 +1,8 @@ +package org.utbot.api.exception; + +public class UtMockAssumptionViolatedException extends RuntimeException { + @Override + public String getMessage() { + return "UtMock assumption violated"; + } +} 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 e80f72e560..42e25a21f1 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 @@ -1,5 +1,7 @@ package org.utbot.api.mock; +import org.utbot.api.exception.UtMockAssumptionViolatedException; + public class UtMock { public static T makeSymbolic() { return makeSymbolic(false); @@ -14,14 +16,14 @@ public static T makeSymbolic(boolean isNullable) { public static void assume(boolean predicate) { // to use compilers checks, i.e. for possible NPE if (!predicate) { - throw new RuntimeException(); + throw new UtMockAssumptionViolatedException(); } } @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 + // to avoid UtMockAssumptionViolatedException during concrete execution } @SuppressWarnings("unused") diff --git a/utbot-cli-go/build.gradle b/utbot-cli-go/build.gradle new file mode 100644 index 0000000000..1f0cf22758 --- /dev/null +++ b/utbot-cli-go/build.gradle @@ -0,0 +1,77 @@ +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] + } +} + +tasks.withType(JavaCompile) { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_17 +} + +configurations { + fetchInstrumentationJar +} + +dependencies { + implementation project(':utbot-framework') + implementation project(':utbot-cli') + implementation project(':utbot-go') + + // Without this dependency testng tests do not run. + implementation group: 'com.beust', name: 'jcommander', version: '1.48' + implementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4PlatformVersion + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: cliktVersion + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit5Version + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2Version + implementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion + //noinspection GroovyAssignabilityCheck + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration: 'instrumentationArchive') + + implementation 'com.beust:klaxon:5.5' // to read and write JSON +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } +} + +task createProperties(dependsOn: processResources) { + doLast { + new File("$buildDir/resources/main/version.properties").withWriter { w -> + Properties properties = new Properties() + //noinspection GroovyAssignabilityCheck + properties['version'] = project.version.toString() + properties.store w, null + } + } +} + +classes { + dependsOn createProperties +} + +jar { + manifest { + attributes 'Main-Class': 'org.utbot.cli.go.ApplicationKt' + attributes 'Bundle-SymbolicName': 'org.utbot.cli.go' + attributes 'Bundle-Version': "${project.version}" + attributes 'Implementation-Title': 'UtBot Go CLI' + attributes 'JAR-Type': 'Fat JAR' + } + + archiveVersion.set(project.version as String) + + dependsOn configurations.runtimeClasspath + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/Application.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/Application.kt new file mode 100644 index 0000000000..2492442c23 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/Application.kt @@ -0,0 +1,36 @@ +package org.utbot.cli.go + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.versionOption +import com.github.ajalt.clikt.parameters.types.enum +import org.slf4j.event.Level +import org.utbot.cli.getVersion +import org.utbot.cli.setVerbosity +import kotlin.system.exitProcess +import org.utbot.cli.go.commands.GenerateGoTestsCommand +import org.utbot.cli.go.commands.RunGoTestsCommand + +class UtBotCli : CliktCommand(name = "UnitTestBot Go Command Line Interface") { + private val verbosity by option("--verbosity", help = "Changes verbosity level, case insensitive") + .enum(ignoreCase = true) + .default(Level.INFO) + + override fun run() = setVerbosity(verbosity) + + init { + versionOption(getVersion()) + } +} + +fun main(args: Array) = try { + UtBotCli().subcommands( + GenerateGoTestsCommand(), + RunGoTestsCommand(), + ).main(args) +} catch (ex: Throwable) { + ex.printStackTrace() + exitProcess(1) +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt new file mode 100644 index 0000000000..55c97cb554 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt @@ -0,0 +1,13 @@ +package org.utbot.cli.go.commands + +import com.beust.klaxon.Json + +internal data class Position(@Json(index = 1) val line: Int, @Json(index = 2) val column: Int) + +internal data class CodeRegion(@Json(index = 1) val start: Position, @Json(index = 2) val end: Position) + +internal data class CoveredSourceFile( + @Json(index = 1) val sourceFileName: String, + @Json(index = 2) val covered: List, + @Json(index = 3) val uncovered: List +) \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt new file mode 100644 index 0000000000..b836808e7b --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt @@ -0,0 +1,154 @@ +package org.utbot.cli.go.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.int +import com.github.ajalt.clikt.parameters.types.long +import mu.KotlinLogging +import org.utbot.cli.go.logic.CliGoUtTestsGenerationController +import org.utbot.cli.go.util.durationInMillis +import org.utbot.cli.go.util.now +import org.utbot.cli.go.util.toAbsolutePath +import org.utbot.go.logic.GoUtTestsGenerationConfig +import org.utbot.go.logic.TestsGenerationMode +import java.nio.file.Files +import java.nio.file.Paths + +private val logger = KotlinLogging.logger {} + +class GenerateGoTestsCommand : + CliktCommand(name = "generateGo", help = "Generates tests for the specified Go source file") { + + private val sourceFile: String by option( + "-s", "--source", + help = "Specifies Go source file to generate tests for" + ) + .required() + .check("Must exist and ends with *.go suffix") { + it.endsWith(".go") && Files.exists(Paths.get(it)) + } + + private val selectedFunctionNames: List by option( + "-f", "--function", + help = StringBuilder() + .append("Specifies function name to generate tests for. ") + .append("Can be used multiple times to select multiple functions at the same time.") + .toString() + ) + .multiple() + + private val selectedMethodNames: List by option( + "-m", "--method", + help = StringBuilder() + .append("Specifies method name to generate tests for. ") + .append("Can be used multiple times to select multiple methods at the same time.") + .toString() + ) + .multiple() + + private val goExecutablePath: String by option( + "-go", + help = "Specifies path to Go executable. For example, it could be [/usr/local/go/bin/go] for some systems" + ) + .required() // TODO: attempt to find it if not specified + + private val gopath: String by option( + "-gopath", + help = buildString { + appendLine("Specifies path the location of your workspace.") + appendLine("It defaults to a directory named go inside your home directory, so \$HOME/go on Unix, \$home/go on Plan 9, and %USERPROFILE%\\go (usually C:\\Users\\YourName\\go) on Windows.") + } + ).required() // TODO: attempt to find it if not specified + + private val numberOfFuzzingProcesses: Int by option( + "-parallel", + help = "The number of fuzzing processes running at once, default 8." + ) + .int() + .default(8) + .check("Must be positive") { it > 0 } + + private val eachFunctionExecutionTimeoutMillis: Long by option( + "-et", "--each-execution-timeout", + help = StringBuilder() + .append("Specifies a timeout in milliseconds for each fuzzed function execution.") + .append("Default is ${GoUtTestsGenerationConfig.DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS} ms") + .toString() + ) + .long() + .default(GoUtTestsGenerationConfig.DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS) + .check("Must be positive") { it > 0 } + + private val allFunctionExecutionTimeoutMillis: Long by option( + "-at", "--all-execution-timeout", + help = StringBuilder() + .append("Specifies a timeout in milliseconds for all fuzzed function execution.") + .append("Default is ${GoUtTestsGenerationConfig.DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS} ms") + .toString() + ) + .long() + .default(GoUtTestsGenerationConfig.DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS) + .check("Must be positive") { it > 0 } + + private val printToStdOut: Boolean by option( + "-p", + "--print-test", + help = "Specifies whether a test should be printed out to StdOut. Is disabled by default" + ) + .flag(default = false) + + private val overwriteTestFiles: Boolean by option( + "-w", + "--overwrite", + help = "Specifies whether to overwrite the output test file if it already exists. Is disabled by default" + ) + .flag(default = false) + + private val fuzzingMode: Boolean by option( + "-fm", + "--fuzzing-mode", + help = "Stop test generation when a panic or error occurs (only one test will be generated for one of these cases)" + ) + .flag(default = false) + + override fun run() { + if (selectedFunctionNames.isEmpty() && selectedMethodNames.isEmpty()) { + throw IllegalArgumentException("Functions or methods must be passed") + } + + val sourceFileAbsolutePath = sourceFile.toAbsolutePath() + val goExecutableAbsolutePath = goExecutablePath.toAbsolutePath() + val gopathAbsolutePath = gopath.toAbsolutePath() + val mode = if (fuzzingMode) { + TestsGenerationMode.FUZZING_MODE + } else { + TestsGenerationMode.DEFAULT + } + + val testsGenerationStarted = now() + logger.info { "Test file generation for [$sourceFile] - started" } + try { + CliGoUtTestsGenerationController( + printToStdOut = printToStdOut, + overwriteTestFiles = overwriteTestFiles + ).generateTests( + mapOf(sourceFileAbsolutePath to selectedFunctionNames), + mapOf(sourceFileAbsolutePath to selectedMethodNames), + GoUtTestsGenerationConfig( + goExecutableAbsolutePath, + gopathAbsolutePath, + numberOfFuzzingProcesses, + mode, + eachFunctionExecutionTimeoutMillis, + allFunctionExecutionTimeoutMillis + ), + ) + } catch (t: Throwable) { + logger.error { "An error has occurred while generating test for snippet $sourceFile: $t" } + throw t + } finally { + val duration = durationInMillis(testsGenerationStarted) + logger.info { "Test file generation for [$sourceFile] - completed in [$duration] (ms)" } + } + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt new file mode 100644 index 0000000000..b5e5847664 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt @@ -0,0 +1,223 @@ +package org.utbot.cli.go.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.choice +import mu.KotlinLogging +import org.utbot.cli.go.util.* +import org.utbot.go.util.convertObjectToJsonString +import java.io.File + +private val logger = KotlinLogging.logger {} + +class RunGoTestsCommand : CliktCommand(name = "runGo", help = "Runs tests for the specified Go package") { + + private val packageDirectory: String by option( + "-p", "--package", + help = "Specifies Go package to run tests for" + ) + .required() + .check("Must exist and be directory") { + File(it).let { file -> file.exists() && file.isDirectory } + } + + private val goExecutablePath: String by option( + "-go", "--go-path", + help = "Specifies path to Go executable. For example, it could be [/usr/local/go/bin/go] for some systems" + ) + .required() // TODO: attempt to find it if not specified + + private val verbose: Boolean by option( + "-v", "--verbose", + help = "Specifies whether an output should be verbose. Is disabled by default" + ) + .flag(default = false) + + private val json: Boolean by option( + "-j", "--json", + help = "Specifies whether an output should be in JSON format. Is disabled by default" + ) + .flag(default = false) + + private val output: String? by option( + "-o", "--output", + help = "Specifies output file for tests run report. Prints to StdOut by default" + ) + + private enum class CoverageMode(val displayName: String) { + REGIONS_HTML("html"), PERCENTS_BY_FUNCS("func"), REGIONS_JSON("json"); + + override fun toString(): String = displayName + + val fileExtensionValidator: (String) -> Boolean + get() = when (this) { + REGIONS_HTML -> { + { it.substringAfterLast('.') == "html" } + } + + REGIONS_JSON -> { + { it.substringAfterLast('.') == "json" } + } + + PERCENTS_BY_FUNCS -> { + { true } + } + } + } + + private val coverageMode: CoverageMode? by option( + "-cov-mode", "--coverage-mode", + help = StringBuilder() + .append("Specifies whether a test coverage report should be generated and defines its mode. ") + .append("Coverage report generation is disabled by default") + .toString() + ) + .choice( + CoverageMode.REGIONS_HTML.toString() to CoverageMode.REGIONS_HTML, + CoverageMode.PERCENTS_BY_FUNCS.toString() to CoverageMode.PERCENTS_BY_FUNCS, + CoverageMode.REGIONS_JSON.toString() to CoverageMode.REGIONS_JSON, + ) + .check( + StringBuilder() + .append("Test coverage report output file must be set ") + .append("and have an extension that matches the coverage mode") + .toString() + ) { mode -> + coverageOutput?.let { mode.fileExtensionValidator(it) } ?: false + } + + private val coverageOutput: String? by option( + "-cov-out", "--coverage-output", + help = "Specifies output file for test coverage report. Required if [--coverage-mode] is set" + ) + .check("Test coverage report mode must be specified") { + coverageMode != null + } + + override fun run() { + val runningTestsStarted = now() + try { + logger.debug { "Running tests for [$packageDirectory] - started" } + + /* run tests */ + + val packageDirectoryFile = File(packageDirectory).canonicalFile + + val coverProfileFile = if (coverageMode != null) { + createFile(createCoverProfileFileName()) + } else { + null + } + + try { + val runGoTestCommand = mutableListOf( + goExecutablePath.toAbsolutePath().toString(), + "test", + "./" + ) + if (verbose) { + runGoTestCommand.add("-v") + } + if (json) { + runGoTestCommand.add("-json") + } + if (coverageMode != null) { + runGoTestCommand.add("-coverprofile") + runGoTestCommand.add(coverProfileFile!!.canonicalPath) + } + + val outputStream = if (output == null) { + System.out + } else { + createFile(output!!).outputStream() + } + executeCommandAndRedirectStdoutOrFail(runGoTestCommand, packageDirectoryFile, outputStream) + + /* generate coverage report */ + + val coverageOutputFile = coverageOutput?.let { createFile(it) } ?: return + + when (coverageMode) { + null -> { + return + } + + CoverageMode.REGIONS_HTML, CoverageMode.PERCENTS_BY_FUNCS -> { + val runToolCoverCommand = mutableListOf( + "go", + "tool", + "cover", + "-${coverageMode!!.displayName}", + coverProfileFile!!.canonicalPath, + "-o", + coverageOutputFile.canonicalPath + ) + executeCommandAndRedirectStdoutOrFail(runToolCoverCommand, packageDirectoryFile) + } + + CoverageMode.REGIONS_JSON -> { + val coveredSourceFiles = parseCoverProfile(coverProfileFile!!) + val jsonCoverage = convertObjectToJsonString(coveredSourceFiles) + coverageOutputFile.writeText(jsonCoverage) + } + } + } finally { + coverProfileFile?.delete() + } + } catch (t: Throwable) { + logger.error { "An error has occurred while running tests for [$packageDirectory]: $t" } + throw t + } finally { + val duration = durationInMillis(runningTestsStarted) + logger.debug { "Running tests for [$packageDirectory] - completed in [$duration] (ms)" } + } + } + + private fun createCoverProfileFileName(): String { + return "ut_go_cover_profile.out" + } + + private fun parseCoverProfile(coverProfileFile: File): List { + data class CoverageRegions( + val covered: MutableList, + val uncovered: MutableList + ) + + val coverageRegionsBySourceFilesNames = mutableMapOf() + + coverProfileFile.readLines().asSequence() + .drop(1) // drop "mode" value + .forEach { fullLine -> + val (sourceFileFullName, coverageInfoLine) = fullLine.split(":", limit = 2) + val sourceFileName = sourceFileFullName.substringAfterLast("/") + val (regionString, _, countString) = coverageInfoLine.split(" ", limit = 3) + + fun parsePosition(positionString: String): Position { + val (lineNumber, columnNumber) = positionString.split(".", limit = 2).asSequence() + .map { it.toInt() } + .toList() + return Position(lineNumber, columnNumber) + } + val (startString, endString) = regionString.split(",", limit = 2) + val region = CodeRegion(parsePosition(startString), parsePosition(endString)) + + val regions = coverageRegionsBySourceFilesNames.getOrPut(sourceFileName) { + CoverageRegions( + mutableListOf(), + mutableListOf() + ) + } + // it is called "count" in docs, but in reality it is like boolean for covered / uncovered + val count = countString.toInt() + if (count == 0) { + regions.uncovered.add(region) + } else { + regions.covered.add(region) + } + } + + return coverageRegionsBySourceFilesNames.map { (sourceFileName, regions) -> + CoveredSourceFile(sourceFileName, regions.covered, regions.uncovered) + } + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt new file mode 100644 index 0000000000..e10f3e9754 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt @@ -0,0 +1,138 @@ +package org.utbot.cli.go.logic + +import mu.KotlinLogging +import org.utbot.cli.go.util.durationInMillis +import org.utbot.cli.go.util.now +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.gocodeanalyzer.GoSourceCodeAnalyzer +import org.utbot.go.logic.AbstractGoUtTestsGenerationController +import java.io.File +import java.nio.file.Path +import java.time.LocalDateTime + +private val logger = KotlinLogging.logger {} + +class CliGoUtTestsGenerationController( + private val printToStdOut: Boolean, + private val overwriteTestFiles: Boolean +) : AbstractGoUtTestsGenerationController() { + + private lateinit var currentStageStarted: LocalDateTime + + override fun onSourceCodeAnalysisStart( + targetFunctionNamesBySourceFiles: Map>, + targetMethodNamesBySourceFiles: Map> + ): Boolean { + currentStageStarted = now() + logger.debug { "Source code analysis - started" } + + return true + } + + override fun onSourceCodeAnalysisFinished( + analysisResults: Map + ): Boolean { + val stageDuration = durationInMillis(currentStageStarted) + logger.debug { "Source code analysis - completed in [$stageDuration] (ms)" } + + return handleMissingSelectedFunctions(analysisResults) + } + + override fun onPackageInstrumentationStart(): Boolean { + currentStageStarted = now() + logger.debug { "Package instrumentation - started" } + + return true + } + + override fun onPackageInstrumentationFinished(): Boolean { + val stageDuration = durationInMillis(currentStageStarted) + logger.debug { "Package instrumentation - completed in [$stageDuration] (ms)" } + + return true + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsStart( + sourceFile: GoUtFile, + functions: List + ): Boolean { + currentStageStarted = now() + logger.debug { "Test cases generation for [${sourceFile.fileName}] - started" } + + return true + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsFinished( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + val stageDuration = durationInMillis(currentStageStarted) + logger.debug { + "Test cases generation for [${sourceFile.fileName}] functions - completed in [$stageDuration] (ms)" + } + + return true + } + + override fun onTestCasesFileCodeGenerationStart( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + currentStageStarted = now() + logger.debug { "Test cases file code generation for [${sourceFile.fileName}] - started" } + + return true + } + + override fun onTestCasesFileCodeGenerationFinished(sourceFile: GoUtFile, generatedTestsFileCode: String): Boolean { + if (printToStdOut) { + logger.info { generatedTestsFileCode } + return true + } + writeGeneratedCodeToFile(sourceFile, generatedTestsFileCode) + + val stageDuration = durationInMillis(currentStageStarted) + logger.debug { + "Test cases file code generation for [${sourceFile.fileName}] functions - completed in [$stageDuration] (ms)" + } + + return true + } + + private fun handleMissingSelectedFunctions( + analysisResults: Map + ): Boolean { + val missingSelectedFunctionsListMessage = generateMissingSelectedFunctionsListMessage(analysisResults) + val okSelectedFunctionsArePresent = + analysisResults.any { (_, analysisResult) -> analysisResult.functions.isNotEmpty() } + + if (missingSelectedFunctionsListMessage != null) { + logger.warn { "Some selected functions were skipped during source code analysis.$missingSelectedFunctionsListMessage" } + } + if (!okSelectedFunctionsArePresent) { + throw Exception("Nothing to process. No functions were provided") + } + + return true + } + + private fun writeGeneratedCodeToFile(sourceFile: GoUtFile, generatedTestsFileCode: String) { + val testsFileNameWithExtension = createTestsFileNameWithExtension(sourceFile) + val testFile = File(sourceFile.absoluteDirectoryPath).resolve(testsFileNameWithExtension) + if (testFile.exists()) { + val alreadyExistsMessage = "File [${testFile.absolutePath}] already exists" + if (overwriteTestFiles) { + logger.warn { "$alreadyExistsMessage: it will be overwritten" } + } else { + logger.warn { "$alreadyExistsMessage: skipping test generation for [${sourceFile.fileName}]" } + return + } + } + testFile.writeText(generatedTestsFileCode) + } + + private fun createTestsFileNameWithExtension(sourceFile: GoUtFile) = + sourceFile.fileNameWithoutExtension + "_go_ut_test.go" +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt new file mode 100644 index 0000000000..b3d072fd05 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt @@ -0,0 +1,16 @@ +package org.utbot.cli.go.util + +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +fun String.toAbsolutePath(): Path = Paths.get(this).toAbsolutePath() + +fun createFile(filePath: String): File = createFile(File(filePath).canonicalFile) + +fun createFile(file: File): File { + return file.also { + it.parentFile?.mkdirs() + it.createNewFile() + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt new file mode 100644 index 0000000000..220b2f0e5f --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt @@ -0,0 +1,12 @@ +package org.utbot.cli.go.util + +import java.io.InputStream +import java.io.OutputStream + +fun copy(from: InputStream, to: OutputStream?) { + val buffer = ByteArray(10240) + var len: Int + while (from.read(buffer).also { len = it } != -1) { + to?.write(buffer, 0, len) + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt new file mode 100644 index 0000000000..3dbeebd99b --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt @@ -0,0 +1,33 @@ +package org.utbot.cli.go.util + +import java.io.File +import java.io.InputStreamReader +import java.io.OutputStream + +fun executeCommandAndRedirectStdoutOrFail( + command: List, + workingDirectory: File? = null, + redirectStdoutToStream: OutputStream? = null // if null, stdout of process is suppressed +) { + val executedProcess = runCatching { + val process = ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectErrorStream(true) + .directory(workingDirectory) + .start() + copy(process.inputStream, redirectStdoutToStream) + process.waitFor() + process + }.getOrElse { + throw RuntimeException( + "Execution of [${command.joinToString(separator = " ")}] failed with throwable: $it" + ) + } + val exitCode = executedProcess.exitValue() + if (exitCode != 0) { + val processOutput = InputStreamReader(executedProcess.inputStream).readText() + throw RuntimeException( + "Execution of [${command.joinToString(separator = " ")}] failed with non-zero exit code = $exitCode:\n$processOutput" + ) + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt new file mode 100644 index 0000000000..c72601574b --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt @@ -0,0 +1,8 @@ +package org.utbot.cli.go.util + +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +fun now(): LocalDateTime = LocalDateTime.now() + +fun durationInMillis(started: LocalDateTime): Long = ChronoUnit.MILLIS.between(started, now()) \ No newline at end of file diff --git a/utbot-cli-go/src/main/resources/log4j2.xml b/utbot-cli-go/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..3d6ee82bcf --- /dev/null +++ b/utbot-cli-go/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-cli-go/src/main/resources/version.properties b/utbot-cli-go/src/main/resources/version.properties new file mode 100644 index 0000000000..956d6e337a --- /dev/null +++ b/utbot-cli-go/src/main/resources/version.properties @@ -0,0 +1,2 @@ +#to be populated during the build task +version=N/A \ No newline at end of file diff --git a/utbot-cli-js/Dockerfile b/utbot-cli-js/Dockerfile new file mode 100644 index 0000000000..3dcc611e25 --- /dev/null +++ b/utbot-cli-js/Dockerfile @@ -0,0 +1,22 @@ +FROM azul/zulu-openjdk:11.0.15-11.56.19 + +ARG DEBIAN_FRONTEND=noninteractive + +WORKDIR /usr/src/ + +RUN apt-get update \ + && apt-get install -y -q --no-install-recommends \ + curl \ + && curl -sL https://deb.nodesource.com/setup_18.x -o nodesource_setup.sh \ + && /bin/bash nodesource_setup.sh \ + && apt-get install -y -q --no-install-recommends \ + nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Install UTBot Javascript CLI + +ARG ARTIFACT_PATH +COPY ${ARTIFACT_PATH} . + +RUN UTBOT_JS_CLI_PATH="$(find /usr/src -type f -name 'utbot-cli*')" \ + && ln -s "${UTBOT_JS_CLI_PATH}" /usr/src/utbot-cli.jar \ diff --git a/utbot-cli-js/build.gradle b/utbot-cli-js/build.gradle new file mode 100644 index 0000000000..0248806799 --- /dev/null +++ b/utbot-cli-js/build.gradle @@ -0,0 +1,75 @@ +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] + } +} + +tasks.withType(JavaCompile) { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_17 +} + +configurations { + fetchInstrumentationJar +} + +dependencies { + implementation project(':utbot-framework') + implementation project(':utbot-cli') + implementation project(':utbot-js') + + // Without this dependency testng tests do not run. + implementation group: 'com.beust', name: 'jcommander', version: '1.48' + implementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4PlatformVersion + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: cliktVersion + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit5Version + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2Version + implementation group: 'org.json', name: 'json', version: '20220320' + //noinspection GroovyAssignabilityCheck + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration: 'instrumentationArchive') +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } +} + +task createProperties(dependsOn: processResources) { + doLast { + new File("$buildDir/resources/main/version.properties").withWriter { w -> + Properties properties = new Properties() + //noinspection GroovyAssignabilityCheck + properties['version'] = project.version.toString() + properties.store w, null + } + } +} + +classes { + dependsOn createProperties +} + +jar { + manifest { + attributes 'Main-Class': 'org.utbot.cli.js.ApplicationKt' + attributes 'Bundle-SymbolicName': 'org.utbot.cli.js' + attributes 'Bundle-Version': "${project.version}" + attributes 'Implementation-Title': 'UtBot JavaScript CLI' + attributes 'JAR-Type': 'Fat JAR' + } + + archiveVersion.set(project.version as String) + + dependsOn configurations.runtimeClasspath + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt new file mode 100644 index 0000000000..9f1ed44f6c --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt @@ -0,0 +1,35 @@ +package org.utbot.cli.js + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.versionOption +import com.github.ajalt.clikt.parameters.types.enum +import org.slf4j.event.Level +import org.utbot.cli.getVersion +import org.utbot.cli.setVerbosity +import kotlin.system.exitProcess + +class UtBotJsCli : CliktCommand(name = "UnitTestBot JavaScript Command Line Interface") { + private val verbosity by option("--verbosity", help = "Changes verbosity level, case insensitive") + .enum(ignoreCase = true) + .default(Level.INFO) + + override fun run() = setVerbosity(verbosity) + + init { + versionOption(getVersion()) + } +} + +fun main(args: Array) = try { + UtBotJsCli().subcommands( + JsCoverageCommand(), + JsGenerateTestsCommand(), + JsRunTestsCommand(), + ).main(args) +} catch (ex: Throwable) { + ex.printStackTrace() + exitProcess(1) +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt new file mode 100644 index 0000000000..788b63c559 --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt @@ -0,0 +1,163 @@ +package org.utbot.cli.js + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.check +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import mu.KotlinLogging +import org.json.JSONArray +import org.json.JSONObject +import org.utbot.cli.js.JsUtils.makeAbsolutePath +import org.w3c.dom.Document +import org.w3c.dom.Element +import utils.JsCmdExec +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import javax.xml.parsers.DocumentBuilderFactory + +private val logger = KotlinLogging.logger {} + +class JsCoverageCommand : CliktCommand(name = "coverage_js", help = "Get tests coverage for the specified file.") { + + private val testFile by option( + "-s", "--source", + help = "Target test file path." + ).required() + .check("Must exist and ends with .js suffix") { + it.endsWith(".js") && Files.exists(Paths.get(it)) + } + + private val output by option( + "-o", "--output", + help = "Specifies output .json file for generated tests." + ).check("Must end with .json suffix") { + it.endsWith(".json") + } + + private val pathToNYC by option( + "--path-to-nyc", + help = "Sets path to nyc executable, defaults to \"nyc\" shortcut. " + + "As there are many nyc files in the global npm directory, choose one without file extension." + ).default("nyc") + + override fun run() { + val testFileAbsolutePath = makeAbsolutePath(testFile) + val workingDir = testFileAbsolutePath.substringBeforeLast("/") + val coverageDataPath = "$workingDir/coverage" + val outputAbsolutePath = output?.let { makeAbsolutePath(it) } + JsCmdExec.runCommand( + dir = workingDir, + shouldWait = true, + timeout = 20, + cmd = arrayOf( + "\"$pathToNYC\"", + "--report-dir=\"$coverageDataPath\"", + "--reporter=\"clover\"", + "--temp-dir=\"${workingDir}/cache\"", + "mocha", + "\"$testFileAbsolutePath\"" + ) + ) + val coveredList = mutableListOf() + val partiallyCoveredList = mutableListOf() + val uncoveredList = mutableListOf() + val db = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val xmlFile = File("$coverageDataPath/clover.xml") + val doc = db.parse(xmlFile) + buildCoverageLists( + coveredList, + partiallyCoveredList, + uncoveredList, + doc, + ) + val json = createJson( + coveredList, + partiallyCoveredList, + uncoveredList, + ) + processResult(json, outputAbsolutePath) + } + + private fun buildCoverageLists( + coveredList: MutableList, + partiallyCoveredList: MutableList, + uncoveredList: MutableList, + doc: Document, + ) { + doc.documentElement.normalize() + val lineList = try { + (((doc.getElementsByTagName("project").item(0) as Element) + .getElementsByTagName("package").item(0) as Element) + .getElementsByTagName("file").item(0) as Element) + .getElementsByTagName("line") + } catch (e: Exception) { + ((doc.getElementsByTagName("project").item(0) as Element) + .getElementsByTagName("file").item(0) as Element) + .getElementsByTagName("line") + } + for (i in 0 until lineList.length) { + val lineInfo = lineList.item(i) as Element + val num = lineInfo.getAttribute("num").toInt() + val count = lineInfo.getAttribute("count").toInt() + when (lineInfo.getAttribute("type")) { + "stmt" -> { + if (count > 0) coveredList += num + else uncoveredList += num + } + + "cond" -> { + val trueCount = lineInfo.getAttribute("truecount").toInt() + val falseCount = lineInfo.getAttribute("falsecount").toInt() + when { + trueCount == 2 && falseCount == 0 -> coveredList += num + trueCount == 1 && falseCount == 1 -> partiallyCoveredList += num + trueCount == 0 && falseCount == 2 -> uncoveredList += num + } + } + } + } + } + + private fun createJson( + coveredList: List, + partiallyCoveredList: List, + uncoveredList: List, + ): JSONObject { + val coveredArray = JSONArray() + coveredList.forEach { + val obj = JSONObject() + obj.put("start", it) + obj.put("end", it) + coveredArray.put(obj) + } + val partiallyCoveredArray = JSONArray() + partiallyCoveredList.forEach { + val obj = JSONObject() + obj.put("start", it) + obj.put("end", it) + partiallyCoveredArray.put(obj) + } + val uncoveredArray = JSONArray() + uncoveredList.forEach { + val obj = JSONObject() + obj.put("start", it) + obj.put("end", it) + uncoveredArray.put(obj) + } + val json = JSONObject() + json.put("covered", coveredArray) + json.put("notCovered", uncoveredArray) + json.put("partlyCovered", partiallyCoveredArray) + return json + } + + private fun processResult(json: JSONObject, output: String?) { + output?.let { fileName -> + val file = File(fileName) + file.createNewFile() + file.writeText(json.toString()) + } ?: logger.info { json.toString() } + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt new file mode 100644 index 0000000000..2a0e043216 --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt @@ -0,0 +1,149 @@ +package org.utbot.cli.js + +import api.JsTestGenerator +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.choice +import mu.KotlinLogging +import org.utbot.cli.js.JsUtils.makeAbsolutePath +import service.coverage.CoverageMode +import settings.JsDynamicSettings +import settings.JsExportsSettings.endComment +import settings.JsExportsSettings.startComment +import settings.JsTestGenerationSettings.defaultTimeout +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +private val logger = KotlinLogging.logger {} + + +class JsGenerateTestsCommand : + CliktCommand(name = "generate_js", help = "Generates tests for the specified class or toplevel functions.") { + + private val sourceCodeFile by option( + "-s", "--source", + help = "Specifies source code file for a generated test." + ) + .required() + .check("Must exist and ends with .js suffix") { + it.endsWith(".js") && Files.exists(Paths.get(it)) + } + + private val targetClass by option("-c", "--class", help = "Specifies target class to generate tests for.") + + private val output by option("-o", "--output", help = "Specifies output file for generated tests.") + .check("Must end with .js suffix") { + it.endsWith(".js") + } + + private val printToStdOut by option( + "-p", + "--print-test", + help = "Specifies whether test should be printed out to StdOut." + ) + .flag(default = false) + + private val timeout by option( + "-t", + "--timeout", + help = "Timeout for Node.js to run scripts in seconds." + ).default("$defaultTimeout") + + private val coverageMode by option( + "--coverage-mode", + help = "Specifies the coverage mode for test generation. Check docs for more info." + ).choice( + CoverageMode.BASIC.toString() to CoverageMode.BASIC, + CoverageMode.FAST.toString() to CoverageMode.FAST + ).default(CoverageMode.FAST) + + private val pathToNode by option( + "--path-to-node", + help = "Sets path to Node.js executable, defaults to \"node\" shortcut." + ).default("node") + + private val pathToNYC by option( + "--path-to-nyc", + help = "Sets path to nyc executable, defaults to \"nyc\" shortcut. " + + "As there are many nyc files in the global npm directory, choose one without file extension." + ).default("nyc") + + private val pathToNPM by option( + "--path-to-npm", + help = "Sets path to npm executable, defaults to \"npm\" shortcut." + ).default("npm") + + override fun run() { + val started = LocalDateTime.now() + try { + val sourceFileAbsolutePath = makeAbsolutePath(sourceCodeFile) + logger.info { "Generating tests for [$sourceFileAbsolutePath] - started" } + val fileText = File(sourceCodeFile).readText() + currentFileText = fileText + val outputAbsolutePath = output?.let { makeAbsolutePath(it) } + val testGenerator = JsTestGenerator( + fileText = fileText, + sourceFilePath = sourceFileAbsolutePath, + parentClassName = targetClass, + outputFilePath = outputAbsolutePath, + exportsManager = ::manageExports, + settings = JsDynamicSettings( + pathToNode = pathToNode, + pathToNYC = pathToNYC, + pathToNPM = pathToNPM, + timeout = timeout.toLong(), + coverageMode = coverageMode, + + ) + ) + val testCode = testGenerator.run() + + if (printToStdOut || (outputAbsolutePath == null && !printToStdOut)) { + logger.info { "\n$testCode" } + } + outputAbsolutePath?.let { filePath -> + val outputFile = File(filePath) + outputFile.createNewFile() + outputFile.writeText(testCode) + } + + } catch (t: Throwable) { + logger.error { "An error has occurred while generating tests for file $sourceCodeFile : $t" } + throw t + } finally { + val duration = ChronoUnit.MILLIS.between(started, LocalDateTime.now()) + logger.debug { "Generating test for [$sourceCodeFile] - completed in [$duration] (ms)" } + } + } + + // Needed for continuous exports managing + private var currentFileText = "" + + private fun manageExports(swappedText: (String?, String) -> String) { + val file = File(sourceCodeFile) + when { + + currentFileText.contains(startComment) -> { + val regex = Regex("$startComment((\\r\\n|\\n|\\r|.)*)$endComment") + regex.find(currentFileText)?.groups?.get(1)?.value?.let { existingSection -> + val newText = swappedText(existingSection, currentFileText) + file.writeText(newText) + currentFileText = newText + } + } + + else -> { + val line = buildString { + append("\n$startComment") + append(swappedText(null, currentFileText)) + append(endComment) + } + file.appendText(line) + currentFileText = file.readText() + } + } + } +} diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt new file mode 100644 index 0000000000..60d96fc374 --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt @@ -0,0 +1,59 @@ +package org.utbot.cli.js + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.check +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.choice +import mu.KotlinLogging +import org.utbot.cli.js.JsUtils.makeAbsolutePath +import utils.JsCmdExec +import java.io.File + +private val logger = KotlinLogging.logger {} + +class JsRunTestsCommand : CliktCommand(name = "run_js", help = "Runs tests for the specified file or directory.") { + + private val fileWithTests by option( + "--fileOrDir", "-f", + help = "Specifies a file or directory with tests." + ).required() + + private val output by option( + "-o", "--output", + help = "Specifies an output .txt file for test framework result." + ).check("Must end with .txt suffix") { + it.endsWith(".txt") + } + + private val testFramework by option("--test-framework", "-t", help = "Test framework to be used.") + .choice("mocha") + .default("mocha") + + + override fun run() { + val fileWithTestsAbsolutePath = makeAbsolutePath(fileWithTests) + val dir = if (fileWithTestsAbsolutePath.endsWith(".js")) + fileWithTestsAbsolutePath.substringBeforeLast("/") else fileWithTestsAbsolutePath + val outputAbsolutePath = output?.let { makeAbsolutePath(it) } + when (testFramework) { + "mocha" -> { + val (inputText, errorText) = JsCmdExec.runCommand( + dir = dir, + shouldWait = true, + cmd = arrayOf("mocha", "\"$fileWithTestsAbsolutePath\"") + ) + if (errorText.isNotEmpty()) { + logger.error { "An error has occurred while running tests for $fileWithTests: $errorText" } + } else { + outputAbsolutePath?.let { + val file = File(it) + file.createNewFile() + file.writeText(inputText) + } ?: logger.info { "Output absolute path is null with text: $inputText" } + } + } + } + } +} diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt new file mode 100644 index 0000000000..87b135a11a --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt @@ -0,0 +1,14 @@ +package org.utbot.cli.js + +import java.io.File + +internal object JsUtils { + + fun makeAbsolutePath(path: String): String { + val rawPath = when { + File(path).isAbsolute -> path + else -> System.getProperty("user.dir") + "/" + path + } + return rawPath.replace("\\", "/") + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/resources/log4j2.xml b/utbot-cli-js/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..d0f20b10bc --- /dev/null +++ b/utbot-cli-js/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-cli-js/src/main/resources/version.properties b/utbot-cli-js/src/main/resources/version.properties new file mode 100644 index 0000000000..956d6e337a --- /dev/null +++ b/utbot-cli-js/src/main/resources/version.properties @@ -0,0 +1,2 @@ +#to be populated during the build task +version=N/A \ No newline at end of file diff --git a/utbot-cli-python/Dockerfile b/utbot-cli-python/Dockerfile new file mode 100644 index 0000000000..6a76e29d21 --- /dev/null +++ b/utbot-cli-python/Dockerfile @@ -0,0 +1,24 @@ +FROM azul/zulu-openjdk:11.0.15-11.56.19 + +ARG DEBIAN_FRONTEND=noninteractive + +WORKDIR /usr/src/ + +RUN apt-get update \ + && apt-get install -y -q --no-install-recommends \ + curl \ + python3.9 \ + python3.9-distutils \ + && rm -rf /var/lib/apt/lists/* \ + && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \ + && python3.9 get-pip.py \ + && pip install -U \ + pytest + +# Install UTBot Python CLI + +ARG ARTIFACT_PATH +COPY ${ARTIFACT_PATH} . + +RUN UTBOT_PYTHON_CLI_PATH="$(find /usr/src -type f -name 'utbot-cli*')" \ + && ln -s "${UTBOT_PYTHON_CLI_PATH}" /usr/src/utbot-cli.jar diff --git a/utbot-cli-python/build.gradle b/utbot-cli-python/build.gradle new file mode 100644 index 0000000000..fbfa2bdd9d --- /dev/null +++ b/utbot-cli-python/build.gradle @@ -0,0 +1,70 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +tasks.withType(KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] + } +} + +tasks.withType(JavaCompile).configureEach { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_17 +} + +configurations { + fetchInstrumentationJar +} + +dependencies { + implementation project(':utbot-framework') + implementation project(':utbot-python') + + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: cliktVersion + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2Version + implementation group: 'com.github.UnitTestBot', name: 'PythonTypesAPI', version: pythonTypesAPIHash +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } +} + +tasks.register('createProperties') { + dependsOn processResources + doLast { + new File("$buildDir/resources/main/version.properties").withWriter { w -> + Properties properties = new Properties() + //noinspection GroovyAssignabilityCheck + properties['version'] = project.version.toString() + properties.store w, null + } + } +} + +classes { + dependsOn createProperties +} + +jar { + manifest { + attributes 'Main-Class': 'org.utbot.cli.language.python.ApplicationKt' + attributes 'Bundle-SymbolicName': 'org.utbot.cli.language.python' + attributes 'Bundle-Version': "${project.version}" + attributes 'Implementation-Title': 'UtBot Python CLI' + attributes 'JAR-Type': 'Fat JAR' + } + + archiveVersion.set(project.version as String) + + dependsOn configurations.runtimeClasspath + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + diff --git a/utbot-cli-python/src/README.md b/utbot-cli-python/src/README.md new file mode 100644 index 0000000000..29873d592c --- /dev/null +++ b/utbot-cli-python/src/README.md @@ -0,0 +1,105 @@ +## Build + +.jar file can be built in Github Actions with script `publish-plugin-and-cli-from-branch`. + +## Requirements + + - Required Java version: 11. + + - Preferred Python version: 3.10-3.11 (3.9 also supported but with limited functionality). + + Make sure that your Python has `pip` installed (this is usually the case). [Read more about pip installation](https://pip.pypa.io/en/stable/installation/). + + Before running utbot install pip requirements (or use `--install-requirements` flag in `generate_python` command): + + python -m pip install mypy==1.0 utbot_executor==0.9.19 utbot_mypy_runner==0.2.16 + +## Basic usage + +Generate tests: + + java -jar utbot-cli-python.jar generate_python dir/file_with_sources.py -p -o generated_tests.py -s dir + +This will generate tests for top-level functions from `file_with_sources.py`. + +Run generated tests: + + java -jar utbot-cli-python.jar run_python generated_tests.py -p + +### `generate_python` options + +- `-s, --sys-path ,` + + (required) Directories to add to `sys.path`. One of directories must contain the file with the methods under test. + + `sys.path` is a list of strings that specifies the search path for modules. It must include paths for all user modules that are used in imports. + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `-o, --output ` + + (required) File for generated tests. + +- `--coverage ` + + File to write coverage report. + +- `-c, --class ` + + Specify top-level (ordinary, not nested) class under test. Without this option tests will be generated for top-level functions. + +- `-m, --methods ,` + + Specify methods under test. + +- `--install-requirements` + + Install Python requirements if missing. + +- `--do-not-minimize` + + Turn off minimization of the number of generated tests. + +- `--do-not-check-requirements` + + Turn off Python requirements check (to speed up). + +- `-t, --timeout INT` + + Specify the maximum time in milliseconds to spend on generating tests (60000 by default). + +- `--timeout-for-run INT` + + Specify the maximum time in milliseconds to spend on one function run (2000 by default). + +- `--test-framework [pytest|Unittest]` + + Test framework to be used. + +- `--runtime-exception-behaviour [PASS|FAIL]` + + Expected behaviour for runtime exception. + +- `--do-not-generate-regression-suite` + + Generate regression test suite or not. Regression suite and error suite generation is active by default. + +### `run_python` options + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `--test-framework [pytest|Unittest]` + + Test framework of tests to run. + +- `-o, --output ` + + Specify file for report. + +## Problems + +- Unittest can not run tests from parent directories diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt new file mode 100644 index 0000000000..0cad4e6264 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt @@ -0,0 +1,55 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.versionOption +import com.github.ajalt.clikt.parameters.types.enum +import mu.KotlinLogging +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.config.Configurator +import org.slf4j.event.Level +import java.util.* +import kotlin.system.exitProcess + +private val logger = KotlinLogging.logger {} + +class UtBotPythonCli : CliktCommand(name = "UnitTestBot Python Command Line Interface") { + private val verbosity by option("--verbosity", help = "Changes verbosity level, case insensitive") + .enum(ignoreCase = true) + .default(Level.INFO) + + override fun run() = setVerbosity(verbosity) + + init { + versionOption(getVersion()) + } +} + +fun main(args: Array) = try { + UtBotPythonCli().subcommands( + PythonGenerateTestsCommand(), + PythonRunTestsCommand(), + PythonTypeInferenceCommand() + ).main(args) +} catch (ex: Throwable) { + ex.printStackTrace() + exitProcess(1) +} + +fun setVerbosity(verbosity: Level) { + Configurator.setAllLevels(LogManager.getRootLogger().name, level(verbosity)) + logger.debug { "Log Level changed to [$verbosity]" } +} + +private fun level(level: Level) = org.apache.logging.log4j.Level.toLevel(level.name) + +fun getVersion(): String { + val prop = Properties() + Thread.currentThread().contextClassLoader.getResourceAsStream("version.properties") + .use { stream -> + prop.load(stream) + } + return prop.getProperty("version") +} diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/CliRequirementsInstaller.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/CliRequirementsInstaller.kt new file mode 100644 index 0000000000..8653520a30 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/CliRequirementsInstaller.kt @@ -0,0 +1,27 @@ +package org.utbot.cli.language.python + +import mu.KLogger +import org.utbot.python.utils.RequirementsInstaller +import org.utbot.python.utils.RequirementsUtils + +class CliRequirementsInstaller( + private val installRequirementsIfMissing: Boolean, + private val logger: KLogger, +) : RequirementsInstaller { + override fun checkRequirements(pythonPath: String, requirements: List): Boolean { + return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements) + } + + override fun installRequirements(pythonPath: String, requirements: List) { + if (installRequirementsIfMissing) { + val result = RequirementsUtils.installRequirements(pythonPath, requirements) + if (result.exitValue != 0) { + System.err.println(result.stderr) + logger.error("Failed to install requirements.") + } + } else { + logger.error("Missing some requirements. Please add --install-requirements flag or install them manually.") + } + logger.info("Requirements: ${requirements.joinToString()}") + } +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonCliProcessor.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonCliProcessor.kt new file mode 100644 index 0000000000..58aefea470 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonCliProcessor.kt @@ -0,0 +1,42 @@ +package org.utbot.cli.language.python + +import mu.KotlinLogging +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.PythonTestSet + +private val logger = KotlinLogging.logger {} + +class PythonCliProcessor( + override val configuration: PythonTestGenerationConfig, + private val testWriter: TestWriter, + private val coverageOutput: String?, + private val executionCounterOutput: String?, +) : PythonTestGenerationProcessor() { + + override fun saveTests(testsCode: String) { + testWriter.addTestCode(testsCode) +// writeToFileAndSave(output, testsCode) + } + + override fun notGeneratedTestsAction(testedFunctions: List) { + logger.error( + "Couldn't generate tests for the following functions: ${testedFunctions.joinToString()}" + ) + } + + private fun getExecutionsNumber(testSets: List): Int { + return testSets.sumOf { it.executionsNumber } + } + + override fun processCoverageInfo(testSets: List) { + coverageOutput?.let { output -> + val coverageReport = getStringCoverageInfo(testSets) + writeToFileAndSave(output, coverageReport) + } + executionCounterOutput?.let { executionOutput -> + val executionReport = "{\"executions\": ${getExecutionsNumber(testSets)}}" + writeToFileAndSave(executionOutput, executionReport) + } + } +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt new file mode 100644 index 0000000000..a9e2242829 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt @@ -0,0 +1,380 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.options.split +import com.github.ajalt.clikt.parameters.types.choice +import com.github.ajalt.clikt.parameters.types.long +import mu.KotlinLogging +import org.parsers.python.PythonParser +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.python.MypyConfig +import org.utbot.python.PythonMethodHeader +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.PythonTestSet +import org.utbot.python.TestFileInformation +import org.utbot.python.code.PythonCode +import org.utbot.python.coverage.CoverageOutputFormat +import org.utbot.python.coverage.PythonCoverageMode +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.pythonBuiltinsModuleName +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.newtyping.ast.parseClassDefinition +import org.utbot.python.newtyping.ast.parseFunctionDefinition +import org.utbot.python.newtyping.mypy.dropInitFile +import org.utbot.python.utils.Cleaner +import org.utbot.python.utils.Fail +import org.utbot.python.utils.RequirementsInstaller +import org.utbot.python.utils.Success +import org.utbot.python.utils.separateTimeout +import java.io.File +import java.nio.file.Paths +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.system.exitProcess +import kotlin.system.measureTimeMillis + +private const val DEFAULT_TIMEOUT_IN_MILLIS = 60000L +private const val DEFAULT_TIMEOUT_FOR_ONE_RUN_IN_MILLIS = 2000L + +private val logger = KotlinLogging.logger {} + +class PythonGenerateTestsCommand : CliktCommand( + name = "generate_python", + help = "Generate tests for a specified Python class or top-level functions from a specified file." +) { + private val sourceFile by argument( + help = "File with Python code to generate tests for." + ) + + private lateinit var absPathToSourceFile: String + private lateinit var sourceFileContent: String + + private val classesToTest by option( + "-c", "--classes", + help = "Generate tests for all methods of selected classes. Use '-' to specify top-level functions group." + ) + .split(",") + private fun classesToTest() = classesToTest?.map { if (it == "-") null else it } + + private val methodsToTest by option( + "-m", "--methods", + help = "Specify methods under test using full path (use qualified name: only name for top-level function and . for methods. Use '-' to skip test generation for top-level functions" + ) + .split(",") + + private val directoriesForSysPath by option( + "-s", "--sys-path", + help = "(required) Directories to add to sys.path. " + + "One of directories must contain the file with the methods under test." + ).split(",").required() + + private val pythonPath by option( + "-p", "--python-path", + help = "(required) Path to Python interpreter." + ).required() + + private val output by option( + "-o", "--output", help = "(required) File for generated tests." + ).required() + + private val coverageOutput by option( + "--coverage", + help = "File to write coverage report." + ) + + private val executionCounterOutput by option( + "--executions-counter", + help = "File to write number of executions." + ) + + private val installRequirementsIfMissing by option( + "--install-requirements", + help = "Install Python requirements if missing." + ).flag(default = false) + + private val doNotMinimize by option( + "--do-not-minimize", + help = "Turn off minimization of the number of generated tests." + ).flag(default = false) + + private val timeout by option( + "-t", "--timeout", + help = "Specify the maximum time in milliseconds to spend on generating tests ($DEFAULT_TIMEOUT_IN_MILLIS by default)." + ).long().default(DEFAULT_TIMEOUT_IN_MILLIS) + + private val timeoutForRun by option( + "--timeout-for-run", + help = "Specify the maximum time in milliseconds to spend on one function run ($DEFAULT_TIMEOUT_FOR_ONE_RUN_IN_MILLIS by default)." + ).long().default(DEFAULT_TIMEOUT_FOR_ONE_RUN_IN_MILLIS) + + private val includeMypyAnalysisTime by option( + "--include-mypy-analysis-time", + help = "Include mypy static analysis time in the total timeout." + ).flag(default = false) + + private val testFrameworkAsString by option("--test-framework", help = "Test framework to be used.") + .choice(Pytest.toString(), Unittest.toString()) + .default(Unittest.toString()) + + private val runtimeExceptionTestsBehaviour by option("--runtime-exception-behaviour", help = "PASS or FAIL") + .choice("PASS", "FAIL") + .default("FAIL") + + private val doNotGenerateRegressionSuite by option("--do-not-generate-regression-suite", help = "Do not generate regression test suite.") + .flag(default = false) + + private val coverageMeasureMode by option("--coverage-measure-mode", help = "Use LINES or INSTRUCTIONS for coverage measurement.") + .choice("INSTRUCTIONS", "LINES") + .default("INSTRUCTIONS") + + private val doNotSendCoverageContinuously by option("--do-not-send-coverage-continuously", help = "Do not send coverage during execution.") + .flag(default = false) + + private val prohibitedExceptions by option( + "--prohibited-exceptions", + help = "Do not generate tests with these exceptions. Set '-' to generate tests for all exceptions." + ) + .split(",") + .default(PythonTestGenerationConfig.defaultProhibitedExceptions) + + private val testFramework: TestFramework + get() = + when (testFrameworkAsString) { + Unittest.toString() -> Unittest + Pytest.toString() -> Pytest + else -> error("Not reachable") + } + + private val forbiddenMethods = listOf("__init__", "__new__") + + private fun getPythonMethods(): List> { + val parsedModule = PythonParser(sourceFileContent).Module() + + val topLevelFunctions = PythonCode.getTopLevelFunctions(parsedModule) + val topLevelClasses = PythonCode.getTopLevelClasses(parsedModule) + + val functions = topLevelFunctions + .mapNotNull { parseFunctionDefinition(it) } + .map { PythonMethodHeader(it.name.toString(), absPathToSourceFile, null) } + val methods = topLevelClasses + .mapNotNull { cls -> + val parsedClass = parseClassDefinition(cls) ?: return@mapNotNull null + val innerClasses = PythonCode.getInnerClasses(cls) + (listOf(parsedClass to null) + innerClasses.mapNotNull { innerClass -> parseClassDefinition(innerClass)?.let { it to parsedClass } }).map { (cls, parent) -> + PythonCode.getClassMethods(cls.body) + .mapNotNull { parseFunctionDefinition(it) } + .map { function -> + val clsName = (parent?.let { "${it.name}." } ?: "") + cls.name.toString() + val parsedClassName = PythonClassId(pythonBuiltinsModuleName, clsName) + PythonMethodHeader(function.name.toString(), absPathToSourceFile, parsedClassName) + } + } + } + .flatten() + + fun functionsFilter(group: List, methodFilter: List? = methodsToTest): List { + return methodFilter?.let { + if (it.isEmpty()) group + else group.filter { method -> method.fullname in it } + } ?: group + } + + fun methodsFilter(group: List, containingClass: PythonClassId): List { + val localMethodFilter = methodsToTest?.let { it.filter { name -> name.startsWith(containingClass.typeName) } } + return functionsFilter(group, localMethodFilter) + } + + fun groupFilter(group: List, classFilter: List?): List { + if (group.isEmpty()) return emptyList() + val groupClass = group.first().containingPythonClassId + if (classFilter != null && groupClass?.typeName !in classFilter) return emptyList() + return if (groupClass == null) functionsFilter(group) + else methodsFilter(group, groupClass) + } + + val methodGroups = (methods + listOf(functions)) + .map { groupFilter(it, classesToTest()) } + .map { + it.filter { forbiddenMethods.all { name -> !it.name.endsWith(name) } } + } + .filter { it.isNotEmpty() } + + methodsToTest?.forEach { name -> + require(methodGroups.flatten().any { it.fullname == name }) { "Cannot find function '$name' in file '$absPathToSourceFile'" } + } + classesToTest()?.forEach { name -> + require(methodGroups.flatten().any { it.containingPythonClassId?.typeName == name }) { "Cannot find class '$name' or methods in file '$absPathToSourceFile'" } + } + return methodGroups + } + + private val shutdown: AtomicBoolean = AtomicBoolean(false) + private val alreadySaved: AtomicBoolean = AtomicBoolean(false) + + private val shutdownThread = + object : Thread() { + override fun run() { + shutdown.set(true) + try { + if (!alreadySaved.get()) { + saveTests() + } + } catch (_: InterruptedException) { + logger.warn { "Interrupted exception" } + } + } + } + + private fun addShutdownHook() { + Runtime.getRuntime().addShutdownHook(shutdownThread) + } + + private fun removeShutdownHook() { + Runtime.getRuntime().removeShutdownHook(shutdownThread) + } + + private val testWriter = TestWriter() + + private fun saveTests() { + logger.info("Saving tests...") + val testCode = testWriter.generateTestCode() + writeToFileAndSave(output, testCode) + + Cleaner.doCleaning() + alreadySaved.set(true) + } + + private fun initialize() { + absPathToSourceFile = sourceFile.toAbsolutePath() + sourceFileContent = File(absPathToSourceFile).readText() + } + + override fun run() { + initialize() + + val sysPathDirectories = directoriesForSysPath.map { it.toAbsolutePath() }.toSet() + val currentPythonModule = when (val module = findCurrentPythonModule(sysPathDirectories, absPathToSourceFile)) { + is Success -> { + module.value + } + + is Fail -> { + logger.error { module.message } + return + } + } + + logger.info("Checking requirements...") + val installer = CliRequirementsInstaller(installRequirementsIfMissing, logger) + val requirementsAreInstalled = RequirementsInstaller.checkRequirements( + installer, + pythonPath, + if (testFramework.isInstalled) emptyList() else listOf(testFramework.mainPackage) + ) + if (!requirementsAreInstalled) { + return + } + + val pythonMethodGroups = getPythonMethods() + + val testFile = TestFileInformation(absPathToSourceFile, sourceFileContent, currentPythonModule.dropInitFile()) + + val mypyConfig: MypyConfig + val mypyTime = measureTimeMillis { + logger.info("Loading information about Python types...") + mypyConfig = PythonTestGenerationProcessor.sourceCodeAnalyze( + sysPathDirectories, + pythonPath, + testFile, + ) + } + logger.info { "Mypy time: $mypyTime" } + + addShutdownHook() + + val startTime = System.currentTimeMillis() + val countOfFunctions = pythonMethodGroups.sumOf { it.size } + val timeoutAfterMypy = if (includeMypyAnalysisTime) timeout - mypyTime else timeout + val oneFunctionTimeout = separateTimeout(timeoutAfterMypy, countOfFunctions) + logger.info { "One function timeout: ${oneFunctionTimeout}ms. x${countOfFunctions}" } + pythonMethodGroups.forEachIndexed { index, pythonMethods -> + val usedTime = System.currentTimeMillis() - startTime + val countOfTestedFunctions = pythonMethodGroups.take(index).sumOf { it.size } + val expectedTime = countOfTestedFunctions * oneFunctionTimeout + val localOneFunctionTimeout = if (usedTime < expectedTime) { + separateTimeout(timeoutAfterMypy - usedTime, countOfFunctions - countOfTestedFunctions) + } else { + oneFunctionTimeout + } + val localTimeout = pythonMethods.size * localOneFunctionTimeout + logger.info { "Timeout for current group: ${localTimeout}ms" } + + val config = PythonTestGenerationConfig( + pythonPath = pythonPath, + testFileInformation = testFile, + sysPathDirectories = sysPathDirectories, + testedMethods = pythonMethods, + timeout = localTimeout, + timeoutForRun = timeoutForRun, + testFramework = testFramework, + testSourceRootPath = Paths.get(output.toAbsolutePath()).parent.toAbsolutePath(), + withMinimization = !doNotMinimize, + isCanceled = { shutdown.get() }, + runtimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.valueOf(runtimeExceptionTestsBehaviour), + coverageMeasureMode = PythonCoverageMode.parse(coverageMeasureMode), + sendCoverageContinuously = !doNotSendCoverageContinuously, + coverageOutputFormat = CoverageOutputFormat.Lines, + prohibitedExceptions = if (prohibitedExceptions == listOf("-")) emptyList() else prohibitedExceptions, + ) + val processor = PythonCliProcessor( + config, + testWriter, + coverageOutput, + executionCounterOutput, + ) + + logger.info("Generating tests...") + val testSets = processor.testGenerate(mypyConfig).let { + return@let if (doNotGenerateRegressionSuite) { + it.map { testSet -> + PythonTestSet( + testSet.method, + testSet.executions.filterNot { execution -> execution.result is UtExecutionSuccess }, + testSet.errors, + testSet.executionsNumber, + testSet.clustersInfo, + ) + } + } else { + it + } + } + if (testSets.isNotEmpty()) { + logger.info("Saving tests...") + val testCode = processor.testCodeGenerate(testSets) + processor.saveTests(testCode) + + + logger.info("Saving coverage report...") + processor.processCoverageInfo(testSets) + + logger.info( + "Finished test generation for the following functions: ${ + testSets.joinToString { it.method.name } + }" + ) + } + } + saveTests() + removeShutdownHook() + exitProcess(0) + } +} diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt new file mode 100644 index 0000000000..ffa9d08462 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt @@ -0,0 +1,82 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.choice +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.utils.CmdResult +import org.utbot.python.utils.runCommand +import java.io.File +import java.nio.file.Paths + +class PythonRunTestsCommand : CliktCommand(name = "run_python", help = "Run tests in the specified file") { + + private val sourceFile by argument( + help = "File with Python tests to run." + ) + + private val pythonPath by option( + "-p", "--python-path", + help = "Path to Python interpreter." + ).required() + + private val output by option( + "-o", "--output", + help = "Specify file for report." + ) + + private val testFrameworkAsString by option("--test-framework", help = "Test framework of tests to run") + .choice(Pytest.toString(), Unittest.toString()) + .default(Unittest.toString()) + + private fun runUnittest(): CmdResult { + val currentPath = Paths.get("").toAbsolutePath().toString() + val sourceFilePath = Paths.get(sourceFile).toAbsolutePath().toString() + return if (sourceFilePath.startsWith(currentPath)) { + runCommand( + listOf( + pythonPath, + "-m", + "unittest", + sourceFile + ) + ) + } else CmdResult( + "", + "File $sourceFile can not be imported from Unittest. Move test file to child directory or use pytest.", + 1 + ) + } + + private fun runPytest(): CmdResult = + runCommand( + listOf( + pythonPath, + "-m", + "pytest", + sourceFile + ) + ) + + override fun run() { + val result = + when (testFrameworkAsString) { + Unittest.toString() -> runUnittest() + Pytest.toString() -> runPytest() + else -> error("Not reachable") + } + + output?.let { + val file = File(it) + file.writeText(result.stderr + result.stdout) + file.parentFile?.mkdirs() + file.createNewFile() + } + println(result.stderr) + println(result.stdout) + } +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonTypeInferenceCommand.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonTypeInferenceCommand.kt new file mode 100644 index 0000000000..124cd6a36e --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonTypeInferenceCommand.kt @@ -0,0 +1,89 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.options.split +import com.github.ajalt.clikt.parameters.types.long +import mu.KotlinLogging +import org.utbot.python.newtyping.inference.TypeInferenceProcessor +import org.utpython.types.pythonTypeRepresentation +import org.utbot.python.utils.Fail +import org.utbot.python.utils.RequirementsUtils.requirements +import org.utbot.python.utils.Success + +private val logger = KotlinLogging.logger {} + +class PythonTypeInferenceCommand : CliktCommand( + name = "infer_types", + help = "Infer types for the specified Python top-level function." +) { + private val sourceFile by argument( + help = "File with Python code." + ) + + private val function by argument( + help = "Function to infer types for." + ) + + private val className by option( + "-c", "--class", + help = "Class of the function" + ) + + private val pythonPath by option( + "-p", "--python-path", + help = "(required) Path to Python interpreter. Use only UNC format on Windows." + ).required() + + private val timeout by option( + "-t", "--timout", + help = "(required) Timeout in milliseconds for type inference." + ).long().required() + + private val directoriesForSysPath by option( + "-s", "--sys-path", + help = "(required) Directories to add to sys.path. Use only UNC format on Windows. " + + "One of directories must contain the file with the methods under test." + ).split(",").required() + + private var startTime: Long = 0 + + override fun run() { + val moduleOpt = findCurrentPythonModule( + directoriesForSysPath.map { it.toAbsolutePath() }, + sourceFile.toAbsolutePath() + ) + if (moduleOpt is Fail) { + logger.error(moduleOpt.message) + } + val module = (moduleOpt as Success).value + + val result = TypeInferenceProcessor( + pythonPath, + directoriesForSysPath.map{ it.toAbsolutePath() }.toSet(), + sourceFile, + module, + function, + className + ).inferTypes( + startingTypeInferenceAction = { + startTime = System.currentTimeMillis() + logger.info("Starting type inference...") + }, + processSignature = { logger.info("Found signature: " + it.pythonTypeRepresentation()) }, + cancel = { System.currentTimeMillis() - startTime > timeout }, + checkRequirementsAction = { logger.info("Checking Python requirements...") }, + missingRequirementsAction = { + logger.error("Some of the following Python requirements are missing: " + + "${requirements.joinToString()}. Please install them.") + }, + loadingInfoAboutTypesAction = { logger.info("Loading information about types...") }, + analyzingCodeAction = { logger.info("Analyzing code...") }, + pythonMethodExtractionFailAction = { logger.error(it) } + ) + + result.forEach { println(it.pythonTypeRepresentation()) } + } +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/TestWriter.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/TestWriter.kt new file mode 100644 index 0000000000..9857fdbe3e --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/TestWriter.kt @@ -0,0 +1,28 @@ +package org.utbot.cli.language.python + +class TestWriter { + private val testCode: MutableList = mutableListOf() + + fun addTestCode(code: String) { + testCode.add(code) + } + + fun generateTestCode(): String { + val (importLines, code) = testCode.fold(mutableListOf() to StringBuilder()) { acc, s -> +// val lines = s.split(System.lineSeparator()) + val lines = s.split("(\\r\\n|\\r|\\n)".toRegex()) + val firstClassIndex = lines.indexOfFirst { it.startsWith("class") } + lines.take(firstClassIndex).forEach { line -> if (line !in acc.first) acc.first.add(line) } + lines.drop(firstClassIndex).forEach { line -> acc.second.append(line + System.lineSeparator()) } + acc.first to acc.second + } + val codeBuilder = StringBuilder() + importLines.filter { it.isNotEmpty() }.forEach { + codeBuilder.append(it) + codeBuilder.append(System.lineSeparator()) + } + codeBuilder.append(System.lineSeparator()) + codeBuilder.append(code) + return codeBuilder.toString() + } +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Utils.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Utils.kt new file mode 100644 index 0000000000..708cfa6131 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Utils.kt @@ -0,0 +1,28 @@ +package org.utbot.cli.language.python + +import org.utbot.python.utils.Fail +import org.utbot.python.utils.Optional +import org.utbot.python.utils.Success +import org.utbot.python.utils.getModuleName +import java.io.File + +fun findCurrentPythonModule( + directoriesForSysPath: Collection, + sourceFile: String +): Optional { + directoriesForSysPath.forEach { path -> + val module = getModuleName(path.toAbsolutePath(), sourceFile.toAbsolutePath()) + if (module != null) + return Success(module) + } + return Fail("Couldn't find path for $sourceFile in --sys-path option. Please, specify it.") +} + +fun String.toAbsolutePath(): String = + File(this).canonicalPath + +fun writeToFileAndSave(filename: String, fileContent: String) { + val file = File(filename) + file.parentFile?.mkdirs() + file.writeText(fileContent) +} diff --git a/utbot-cli-python/src/main/resources/log4j2.xml b/utbot-cli-python/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..3d6ee82bcf --- /dev/null +++ b/utbot-cli-python/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-cli-python/src/main/resources/version.properties b/utbot-cli-python/src/main/resources/version.properties new file mode 100644 index 0000000000..956d6e337a --- /dev/null +++ b/utbot-cli-python/src/main/resources/version.properties @@ -0,0 +1,2 @@ +#to be populated during the build task +version=N/A \ No newline at end of file diff --git a/utbot-cli/Dockerfile b/utbot-cli/Dockerfile new file mode 100644 index 0000000000..9e4e97e1b8 --- /dev/null +++ b/utbot-cli/Dockerfile @@ -0,0 +1,28 @@ +FROM azul/zulu-openjdk:11.0.15-11.56.19 + +ARG DEBIAN_FRONTEND=noninteractive + +WORKDIR /usr/src/ + +RUN apt-get update \ + && apt-get install -y -q --no-install-recommends \ + wget \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +# Install Kotlin compiler + +ENV KOTLIN_COMPILER_VERSION=1.8.0 +ENV KOTLIN_HOME="/opt/kotlin/kotlinc" +ENV PATH="${KOTLIN_HOME}/bin:${PATH}" + +RUN wget --no-verbose https://github.com/JetBrains/kotlin/releases/download/v${KOTLIN_COMPILER_VERSION}/kotlin-compiler-${KOTLIN_COMPILER_VERSION}.zip -O /tmp/${KOTLIN_COMPILER_VERSION}.zip \ + && unzip -q -d /opt/kotlin /tmp/${KOTLIN_COMPILER_VERSION}.zip + +# Install UTBot Java CLI + +ARG ARTIFACT_PATH +COPY ${ARTIFACT_PATH} . + +RUN UTBOT_JAVA_CLI_PATH="$(find /usr/src -type f -name 'utbot-cli*')" \ + && ln -s "${UTBOT_JAVA_CLI_PATH}" /usr/src/utbot-cli.jar diff --git a/utbot-cli/build.gradle b/utbot-cli/build.gradle index bf7b18b3c1..139bc81c4c 100644 --- a/utbot-cli/build.gradle +++ b/utbot-cli/build.gradle @@ -1,13 +1,13 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { - jvmTarget = JavaVersion.VERSION_11 + jvmTarget = JavaVersion.VERSION_17 freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] } } tasks.withType(JavaCompile) { sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_17 } configurations { diff --git a/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsAbstractCommand.kt b/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsAbstractCommand.kt index 5a81242b8c..4d39fd2c1c 100644 --- a/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsAbstractCommand.kt +++ b/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsAbstractCommand.kt @@ -16,12 +16,15 @@ import org.utbot.common.PathUtil.toURL import org.utbot.common.toPath import org.utbot.engine.Mocker import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.MockitoStaticMocking -import org.utbot.framework.codegen.NoStaticMocking -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.model.CodeGenerator -import org.utbot.framework.codegen.testFrameworkByName +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.MockitoStaticMocking +import org.utbot.framework.codegen.domain.NoStaticMocking +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.testFrameworkByName +import org.utbot.framework.codegen.generator.CodeGenerator +import org.utbot.framework.codegen.generator.CodeGeneratorParams +import org.utbot.framework.codegen.services.language.CgLanguageAssistant import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.ExecutableId @@ -31,7 +34,7 @@ import org.utbot.framework.plugin.api.TestCaseGenerator import org.utbot.framework.plugin.api.TreatOverflowAsError import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.plugin.services.JdkInfoDefaultProvider -import org.utbot.summary.summarize +import org.utbot.summary.summarizeAll import java.io.File import java.net.URLClassLoader import java.nio.file.Files @@ -160,8 +163,8 @@ abstract class GenerateTestsAbstractCommand(name: String, help: String) : mockStrategy, chosenClassesToMockAlways, generationTimeout - ).map { - if (sourceCodeFile != null) it.summarize(sourceCodeFile.toFile(), searchDirectory) else it + ).let { + if (sourceCodeFile != null) it.summarizeAll(searchDirectory, sourceCodeFile.toFile()) else it } @@ -208,12 +211,17 @@ abstract class GenerateTestsAbstractCommand(name: String, help: String) : val generateWarningsForStaticMocking = forceStaticMocking == ForceStaticMocking.FORCE && staticsMocking is NoStaticMocking return CodeGenerator( - testFramework = testFrameworkByName(testFramework), - classUnderTest = classUnderTest, - codegenLanguage = codegenLanguage, - staticsMocking = staticsMocking, - forceStaticMocking = forceStaticMocking, - generateWarningsForStaticMocking = generateWarningsForStaticMocking, + CodeGeneratorParams( + testFramework = testFrameworkByName(testFramework), + classUnderTest = classUnderTest, + //TODO: Support Spring projects in utbot-cli if requested + projectType = ProjectType.PureJvm, + codegenLanguage = codegenLanguage, + cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(codegenLanguage), + staticsMocking = staticsMocking, + forceStaticMocking = forceStaticMocking, + generateWarningsForStaticMocking = generateWarningsForStaticMocking, + ) ) } 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 35d69dcd8a..a6837ca80f 100644 --- a/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt +++ b/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt @@ -17,8 +17,9 @@ import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.isAbstract +import org.utbot.framework.plugin.api.util.isSynthetic import org.utbot.framework.plugin.api.util.withUtContext -import org.utbot.framework.util.isKnownSyntheticMethod +import org.utbot.framework.util.isKnownImplicitlyDeclaredMethod import org.utbot.sarif.SarifReport import org.utbot.sarif.SourceFindingStrategyDefault import java.nio.file.Files @@ -50,8 +51,8 @@ class GenerateTestsCommand : help = "Specifies source code file for a generated test" ) .required() - .check("Must exist and ends with *.java suffix") { - it.endsWith(".java") && Files.exists(Paths.get(it)) + .check("Must exist and end with .java or .kt suffix") { + (it.endsWith(".java") || it.endsWith(".kt")) && Files.exists(Paths.get(it)) } private val projectRoot by option( @@ -97,7 +98,9 @@ class GenerateTestsCommand : withUtContext(UtContext(classLoader)) { val classIdUnderTest = ClassId(targetClassFqn) val targetMethods = classIdUnderTest.targetMethods() - .filterWhen(UtSettings.skipTestGenerationForSyntheticMethods) { !isKnownSyntheticMethod(it) } + .filterWhen(UtSettings.skipTestGenerationForSyntheticAndImplicitlyDeclaredMethods) { + !it.isSynthetic && !it.isKnownImplicitlyDeclaredMethod + } .filterNot { it.isAbstract } val testCaseGenerator = initializeGenerator(workingDirectory) @@ -149,7 +152,7 @@ class GenerateTestsCommand : else -> { val sourceFinding = SourceFindingStrategyDefault(classFqn, sourceCodeFile, testsFilePath, projectRootPath) - val report = SarifReport(testSets, testClassBody, sourceFinding).createReport() + val report = SarifReport(testSets, testClassBody, sourceFinding).createReport().toJson() saveToFile(report, sarifReport) println("The report was saved to \"$sarifReport\".") } diff --git a/utbot-core/build.gradle.kts b/utbot-core/build.gradle.kts index 7f957695f4..faada50cce 100644 --- a/utbot-core/build.gradle.kts +++ b/utbot-core/build.gradle.kts @@ -1,5 +1,3 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - val kotlinLoggingVersion: String by rootProject val junit4Version: String by rootProject @@ -12,11 +10,4 @@ dependencies { implementation(group = "net.java.dev.jna", name = "jna-platform", version = "5.5.0") testImplementation(group = "junit", name = "junit", version = junit4Version) -} - -tasks { - withType { - archiveClassifier.set(" ") - minimize() - } } \ No newline at end of file diff --git a/utbot-core/src/main/kotlin/org/utbot/common/AbstractSettings.kt b/utbot-core/src/main/kotlin/org/utbot/common/AbstractSettings.kt index f564d98a20..3c5e4bb78f 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/AbstractSettings.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/AbstractSettings.kt @@ -2,7 +2,9 @@ package org.utbot.common import java.io.FileInputStream import java.io.IOException +import java.io.InputStream import java.util.* +import kotlin.Comparator import mu.KLogger import org.utbot.common.PathUtil.toPath import kotlin.properties.PropertyDelegateProvider @@ -10,7 +12,16 @@ import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty interface SettingsContainer { - fun settingFor(defaultValue: T, converter: (String) -> T): PropertyDelegateProvider> + fun settingFor( + defaultValue: T, + range : Triple>? = null, + converter: (String) -> T + ): PropertyDelegateProvider> + + fun getInputStream() : InputStream? = null + + // Returns true iff some properties have non-default values + fun isCustomized() = false } interface SettingsContainerFactory { @@ -20,10 +31,10 @@ interface SettingsContainerFactory { defaultSettingsPath: String? = null) : SettingsContainer } -class PropertiesSettingsContainer( +internal open class PropertiesSettingsContainer( private val logger: KLogger, - defaultKeyForSettingsPath: String, - defaultSettingsPath: String? = null): SettingsContainer { + val defaultKeyForSettingsPath: String, + val defaultSettingsPath: String? = null): SettingsContainer { companion object: SettingsContainerFactory { override fun createSettingsContainer( logger: KLogger, @@ -33,20 +44,23 @@ class PropertiesSettingsContainer( } private val properties = Properties().also { props -> - val settingsPath = System.getProperty(defaultKeyForSettingsPath) ?: defaultSettingsPath - val settingsPathFile = settingsPath?.toPath()?.toFile() - if (settingsPathFile?.exists() == true) { - try { - FileInputStream(settingsPathFile).use { reader -> - props.load(reader) - } - } catch (e: IOException) { - logger.info(e) { e.message } + try { + getInputStream()?.use { + props.load(it) } + } catch (e: IOException) { + logger.info(e) { e.message } } } + override fun getInputStream() : InputStream? { + val settingsPath = System.getProperty(defaultKeyForSettingsPath) ?: defaultSettingsPath + val settingsPathFile = settingsPath?.toPath()?.toFile() + return if (settingsPathFile?.exists() == true) FileInputStream(settingsPathFile) else null + } + private val settingsValues: MutableMap, Any?> = mutableMapOf() + private var customized: Boolean = false inner class SettingDelegate(val property: KProperty<*>, val initializer: () -> T): ReadWriteProperty { private var value = initializer() @@ -69,20 +83,35 @@ class PropertiesSettingsContainer( override fun settingFor( defaultValue: T, + range : Triple>?, converter: (String) -> T ): PropertyDelegateProvider> { return PropertyDelegateProvider { _, property -> SettingDelegate(property) { try { - properties.getProperty(property.name)?.let(converter) ?: defaultValue + properties.getProperty(property.name)?.let { + var parsedValue = converter.invoke(it) + range?.let { + // Coerce parsed value into the specified range + parsedValue = maxOf(parsedValue, range.first, range.third) + parsedValue = minOf(parsedValue, range.second, range.third) + } + customized = customized or (parsedValue != defaultValue) + return@SettingDelegate parsedValue + } + defaultValue } catch (e: Throwable) { - logger.info(e) { e.message } + logger.warn("Cannot parse value for ${property.name}, default value [$defaultValue] will be used instead") { e } defaultValue } } } } + override fun isCustomized(): Boolean { + return customized + } + override fun toString(): String = settingsValues .mapKeys { it.key.name } @@ -115,19 +144,39 @@ abstract class AbstractSettings( } } + fun areCustomized(): Boolean = container.isCustomized() + + protected fun getProperty( + defaultValue: T, + range : Triple>?, + converter: (String) -> T + ): PropertyDelegateProvider> where T: Comparable { + return container.settingFor(defaultValue, range, converter) + } + protected fun getProperty( defaultValue: T, converter: (String) -> T ): PropertyDelegateProvider> { - return container.settingFor(defaultValue, converter) + return container.settingFor(defaultValue, null, converter) } - protected fun getBooleanProperty(defaultValue: Boolean) = getProperty(defaultValue, String::toBoolean) - protected fun getIntProperty(defaultValue: Int) = getProperty(defaultValue, String::toInt) - protected fun getLongProperty(defaultValue: Long) = getProperty(defaultValue, String::toLong) + protected fun getBooleanProperty(defaultValue: Boolean) = getProperty(defaultValue, converter = { + //Invalid values shouldn't be parsed as "false" + if (it.equals("true", true)) true + else if (it.equals("false", true)) false + else defaultValue + }) + protected fun getIntProperty(defaultValue: Int) = getProperty(defaultValue, converter = String::toInt) + protected fun getIntProperty(defaultValue: Int, minValue: Int, maxValue: Int) = getProperty(defaultValue, Triple(minValue, maxValue, Comparator(Integer::compare)), String::toInt) + protected fun getLongProperty(defaultValue: Long) = getProperty(defaultValue, converter = String::toLong) + protected fun getLongProperty(defaultValue: Long, minValue: Long, maxValue: Long) = getProperty(defaultValue, Triple(minValue, maxValue, Comparator(Long::compareTo)), String::toLong) protected fun getStringProperty(defaultValue: String) = getProperty(defaultValue) { it } protected inline fun > getEnumProperty(defaultValue: T) = getProperty(defaultValue) { enumValueOf(it) } - + protected fun getListProperty(defaultValue: List) = + getProperty(defaultValue) { it.split(';') } + protected inline fun getListProperty(defaultValue: List, crossinline elementTransform: (String) -> T) = + getProperty(defaultValue) { it.split(';').map(elementTransform) } override fun toString(): String = container.toString() } \ No newline at end of file diff --git a/utbot-core/src/main/kotlin/org/utbot/common/AnnotationUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/AnnotationUtil.kt new file mode 100644 index 0000000000..e396e00d28 --- /dev/null +++ b/utbot-core/src/main/kotlin/org/utbot/common/AnnotationUtil.kt @@ -0,0 +1,33 @@ +package org.utbot.common + +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Proxy + +/** + * Assigns [newValue] to specified [property] of [annotation]. + * + * NOTE! [annotation] instance is expected to be a [Proxy] + * using [sun.reflect.annotation.AnnotationInvocationHandler] + * making this function depend on JDK vendor and version. + * + * Example: `@ImportResource -> @ImportResource(value = "classpath:shark-config.xml")` + */ +fun patchAnnotation( + annotation: Annotation, + property: String, + newValue: Any? +) { + val proxyClass = Proxy::class.java + val hField = proxyClass.getDeclaredField("h") + hField.isAccessible = true + + val invocationHandler = hField[annotation] as InvocationHandler + + val annotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler") + val memberValuesField = annotationInvocationHandlerClass.getDeclaredField("memberValues") + memberValuesField.isAccessible = true + + @Suppress("UNCHECKED_CAST") // unavoidable because of reflection + val memberValues = memberValuesField[invocationHandler] as MutableMap + memberValues[property] = newValue +} diff --git a/utbot-core/src/main/kotlin/org/utbot/common/ClassLoaderUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/ClassLoaderUtil.kt new file mode 100644 index 0000000000..ce7652a48a --- /dev/null +++ b/utbot-core/src/main/kotlin/org/utbot/common/ClassLoaderUtil.kt @@ -0,0 +1,14 @@ +package org.utbot.common + +import java.net.URLClassLoader + +/** + * Checks that the class given by its binary name is on classpath of this classloader. + * + * Note: if the specified class is on classpath, `true` is returned even when + * superclass (or implemented interfaces) aren't on the classpath. + */ +fun URLClassLoader.hasOnClasspath(classBinaryName: String): Boolean { + val classFqn = classBinaryName.replace('.', '/').plus(".class") + return this.findResource(classFqn) != null +} \ No newline at end of file diff --git a/utbot-core/src/main/kotlin/org/utbot/common/DynamicProperties.kt b/utbot-core/src/main/kotlin/org/utbot/common/DynamicProperties.kt new file mode 100644 index 0000000000..666929bbbb --- /dev/null +++ b/utbot-core/src/main/kotlin/org/utbot/common/DynamicProperties.kt @@ -0,0 +1,110 @@ +package org.utbot.common + +import java.util.* + +/** + * @param TOwner used purely to make type system enforce the use of properties with correct receiver, + * e.g. if property `NotEmptyTypeFlag` is defined for `FuzzedType` it can't be used on `CgContext`. + * + * **See also:** [this post](https://stackoverflow.com/a/58219723/10536125). + */ +interface DynamicProperty + +data class InitialisedDynamicProperty( + val property: DynamicProperty, + val value: T +) + +fun DynamicProperty.withValue(value: T) = + InitialisedDynamicProperty(this, value) + +interface DynamicProperties { + val entries: Set> + + operator fun get(property: DynamicProperty): T? + fun getValue(property: DynamicProperty): T + operator fun contains(property: DynamicProperty): Boolean + + /** + * Two instances of [DynamicProperties] implementations are equal iff their [entries] are equal + */ + override fun equals(other: Any?): Boolean + override fun hashCode(): Int +} + +interface MutableDynamicProperties : DynamicProperties { + operator fun set(property: DynamicProperty, value: T) +} + +fun MutableDynamicProperties.getOrPut(property: DynamicProperty, default: () -> T): T { + if (property !in this) set(property, default()) + return getValue(property) +} + +fun Iterable>.toMutableDynamicProperties(): MutableDynamicProperties = + DynamicPropertiesImpl().also { properties -> + forEach { properties.add(it) } + } + +fun Iterable>.toDynamicProperties(): DynamicProperties = + toMutableDynamicProperties() + +fun mutableDynamicPropertiesOf( + vararg initialisedDynamicProperties: InitialisedDynamicProperty +): MutableDynamicProperties = initialisedDynamicProperties.asIterable().toMutableDynamicProperties() + +fun dynamicPropertiesOf( + vararg initialisedDynamicProperties: InitialisedDynamicProperty +): DynamicProperties = initialisedDynamicProperties.asIterable().toDynamicProperties() + +fun DynamicProperties.withoutProperty(property: DynamicProperty): DynamicProperties = + entries.filterNot { it.property == property }.toMutableDynamicProperties() + +operator fun DynamicProperties.plus(other: DynamicProperties): DynamicProperties = + (entries + other.entries).toMutableDynamicProperties() + +class DynamicPropertiesImpl : MutableDynamicProperties { + /** + * Actual type of [properties] should be `Map, T>`, but + * such type is not representable withing kotlin type system, hence unchecked casts are + * used later. + */ + private val properties = IdentityHashMap, Any?>() + override val entries: Set> + get() = properties.mapTo(mutableSetOf()) { unsafeInitializedDynamicProperty(it.key, it.value) } + + @Suppress("UNCHECKED_CAST") + private fun unsafeInitializedDynamicProperty(property: DynamicProperty, value: Any?) = + InitialisedDynamicProperty(property, value as T) + + @Suppress("UNCHECKED_CAST") + override fun get(property: DynamicProperty): T? = + properties[property] as T? + + @Suppress("UNCHECKED_CAST") + override fun getValue(property: DynamicProperty): T = + properties.getValue(property) as T + + override fun set(property: DynamicProperty, value: T) { + properties[property] = value + } + + override fun contains(property: DynamicProperty): Boolean = + property in properties + + fun add(initialisedDynamicProperty: InitialisedDynamicProperty) = + set(initialisedDynamicProperty.property, initialisedDynamicProperty.value) + + override fun toString(): String { + return "DynamicPropertiesImpl(properties=$properties)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DynamicProperties<*>) return false + return entries == other.entries + } + + override fun hashCode(): Int = + properties.hashCode() +} diff --git a/utbot-core/src/main/kotlin/org/utbot/common/FallbackClassLoader.kt b/utbot-core/src/main/kotlin/org/utbot/common/FallbackClassLoader.kt new file mode 100644 index 0000000000..044bdb93e2 --- /dev/null +++ b/utbot-core/src/main/kotlin/org/utbot/common/FallbackClassLoader.kt @@ -0,0 +1,99 @@ +package org.utbot.common + +import java.io.IOException +import java.net.URL +import java.net.URLClassLoader +import java.util.* + +/** + * [ClassLoader] implementation, that + * - first, attempts to load class/resource with [commonParent] class loader + * - next, attempts to load class/resource from `urls` + * - finally, attempts to load class/resource with `fallback` class loader + * + * More details can be found in [this post](https://medium.com/@isuru89/java-a-child-first-class-loader-cbd9c3d0305). + */ +class FallbackClassLoader( + urls: Array, + fallback: ClassLoader, + private val commonParent: ClassLoader = fallback.parent, +) : URLClassLoader(urls, fallback) { + + @Throws(ClassNotFoundException::class) + override fun loadClass(name: String, resolve: Boolean): Class<*>? { + // has the class loaded already? + var loadedClass = findLoadedClass(name) + if (loadedClass == null) { + try { + loadedClass = commonParent.loadClass(name) + } catch (ex: ClassNotFoundException) { + // class not found in common parent loader... silently skipping + } + try { + // find the class from given jar urls as in first constructor parameter. + if (loadedClass == null) { + loadedClass = findClass(name) + } + } catch (e: ClassNotFoundException) { + // class is not found in the given urls. + // Let's try it in fallback classloader. + // If class is still not found, then this method will throw class not found ex. + loadedClass = super.loadClass(name, resolve) + } + } + if (resolve) { // marked to resolve + resolveClass(loadedClass) + } + return loadedClass + } + + @Throws(IOException::class) + override fun getResources(name: String): Enumeration { + val allRes: MutableList = LinkedList() + + // load resources from common parent loader + val commonParentResources: Enumeration? = commonParent.getResources(name) + if (commonParentResources != null) { + while (commonParentResources.hasMoreElements()) { + allRes.add(commonParentResources.nextElement()) + } + } + + // load resource from this classloader + val thisRes: Enumeration? = findResources(name) + if (thisRes != null) { + while (thisRes.hasMoreElements()) { + allRes.add(thisRes.nextElement()) + } + } + + // then try finding resources from fallback classloaders + val parentRes: Enumeration? = super.findResources(name) + if (parentRes != null) { + while (parentRes.hasMoreElements()) { + allRes.add(parentRes.nextElement()) + } + } + return object : Enumeration { + var it: Iterator = allRes.iterator() + override fun hasMoreElements(): Boolean { + return it.hasNext() + } + + override fun nextElement(): URL { + return it.next() + } + } + } + + override fun getResource(name: String): URL? { + var res: URL? = commonParent.getResource(name) + if (res === null) { + res = findResource(name) + } + if (res === null) { + res = super.getResource(name) + } + return res + } +} \ 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 4bb386ae7b..84ce9f0f0a 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt @@ -17,6 +17,7 @@ import java.util.zip.ZipFile import kotlin.concurrent.thread import kotlin.streams.asSequence import mu.KotlinLogging +import java.util.zip.ZipEntry fun Class<*>.toClassFilePath(): String { val name = requireNotNull(name) { "Class is local or anonymous" } @@ -30,12 +31,14 @@ object FileUtil { fun extractArchive( archiveFile: Path, destPath: Path, - vararg options: CopyOption = arrayOf(StandardCopyOption.REPLACE_EXISTING) + vararg options: CopyOption = arrayOf(StandardCopyOption.REPLACE_EXISTING), + extractOnlySuchEntriesPredicate: (ZipEntry) -> Boolean = { true } ) { Files.createDirectories(destPath) ZipFile(archiveFile.toFile()).use { archive -> val entries = archive.stream().asSequence() + .filter(extractOnlySuchEntriesPredicate) .sortedBy { it.name } .toList() @@ -77,7 +80,7 @@ object FileUtil { return createTempDirectory(utBotTempDirectory, prefix) } - fun createTempFile(prefix: String, suffix: String) : Path { + fun createTempFile(prefix: String, suffix: String): Path { return Files.createTempFile(utBotTempDirectory, prefix, suffix) } @@ -175,12 +178,26 @@ object FileUtil { /** * Extracts archive to temp directory and returns path to directory. */ - fun extractArchive(archiveFile: Path): Path { + fun extractArchive(archiveFile: Path, extractOnlySuchEntriesPredicate: (ZipEntry) -> Boolean = { true }): Path { val tempDir = createTempDirectory(TEMP_DIR_NAME).toFile().apply { deleteOnExit() } - extractArchive(archiveFile, tempDir.toPath()) + extractArchive(archiveFile, tempDir.toPath(), extractOnlySuchEntriesPredicate = extractOnlySuchEntriesPredicate) return tempDir.toPath() } + /** + * Extracts specified directory (with all contents) from archive to temp directory and returns path to it. + */ + fun extractDirectoryFromArchive(archiveFile: Path, directoryName: String): Path? { + val extractedJarDirectory = extractArchive(archiveFile) { entry -> + entry.name.normalizePath().startsWith(directoryName) + } + val extractedTargetDirectoryPath = extractedJarDirectory.resolve(directoryName) + if (!extractedTargetDirectoryPath.toFile().exists()) { + return null + } + return extractedTargetDirectoryPath + } + /** * Returns the path to the class files for the given ClassLocation. */ @@ -234,7 +251,7 @@ object FileUtil { bytesInDouble >= 1 shl 30 -> "%.1f GB".format(bytesInDouble / (1 shl 30)) bytesInDouble >= 1 shl 20 -> "%.1f MB".format(bytesInDouble / (1 shl 20)) bytesInDouble >= 1 shl 10 -> "%.0f kB".format(bytesInDouble / (1 shl 10)) - else -> "$bytesInDouble bytes" + else -> "$bytes bytes" } } } diff --git a/utbot-core/src/main/kotlin/org/utbot/common/HackUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/HackUtil.kt index 881b708f55..e375e2ee0b 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/HackUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/HackUtil.kt @@ -67,4 +67,13 @@ enum class WorkaroundReason { * requires thorough [investigation](https://github.com/UnitTestBot/UTBotJava/issues/716). */ IGNORE_STATICS_FROM_TRUSTED_LIBRARIES, + /** + * Methods that return [java.util.stream.BaseStream] as a result, can return them ”dirty” - consuming of them lead to the exception. + * The symbolic engine and concrete execution create UtStreamConsumingFailure executions in such cases. To warn a + * user about unsafety of using such “dirty” streams, code generation consumes them (mostly with `toArray` methods) + * and asserts exception. Unfortunately, it doesn't work well for parametrized tests - they create assertions relying on + * such-called “generic execution”, so resulted tests always contain `deepEquals` for streams, and we cannot easily + * construct `toArray` invocation (because streams cannot be consumed twice). + */ + CONSUME_DIRTY_STREAMS, } \ No newline at end of file diff --git a/utbot-core/src/main/kotlin/org/utbot/common/JarUtils.kt b/utbot-core/src/main/kotlin/org/utbot/common/JarUtils.kt new file mode 100644 index 0000000000..765d520514 --- /dev/null +++ b/utbot-core/src/main/kotlin/org/utbot/common/JarUtils.kt @@ -0,0 +1,46 @@ +package org.utbot.common + +import java.io.File +import java.net.URL +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import java.util.* + +object JarUtils { + private const val UNKNOWN_MODIFICATION_TIME = 0L + + fun extractJarFileFromResources(jarFileName: String, jarResourcePath: String, targetDirectoryName: String): File { + val resource = this::class.java.classLoader.getResource(jarResourcePath) + ?: error("Unable to find \"$jarResourcePath\" in resources, make sure it's on the classpath") + + val targetDirectory = utBotTempDirectory.toFile().resolve(targetDirectoryName).toPath() + fun extractToSubDir(subDir: String) = + Files.createDirectories(targetDirectory.resolve(subDir)).toFile().resolve(jarFileName).also { jarFile -> + updateJarIfRequired(jarFile, resource) + } + + // We attempt to always extract jars to same locations, to avoid eating up drive space with + // every UtBot launch, but we may fail to do so if multiple processes are running in parallel. + repeat(10) { i -> + runCatching { + return extractToSubDir(i.toString()) + } + } + return extractToSubDir(UUID.randomUUID().toString()) + } + + private fun updateJarIfRequired(jarFile: File, resource: URL) { + val resourceConnection = resource.openConnection() + resourceConnection.getInputStream().use { inputStream -> + val lastResourceModification = resourceConnection.lastModified + if ( + !jarFile.exists() || + jarFile.lastModified() == UNKNOWN_MODIFICATION_TIME || + lastResourceModification == UNKNOWN_MODIFICATION_TIME || + jarFile.lastModified() < lastResourceModification + ) { + Files.copy(inputStream, jarFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } + } +} \ No newline at end of file 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 1c0a88d02a..92e4d8d398 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/KClassUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/KClassUtil.kt @@ -2,15 +2,15 @@ package org.utbot.common import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method -import kotlin.reflect.KClass - -val Class<*>.nameOfPackage: String get() = `package`?.name?:"" +/** + * Invokes [this] method of passed [obj] instance (null for static methods) with the passed [args] arguments. + * NOTE: vararg parameters must be passed as an array of the corresponding type. + */ fun Method.invokeCatching(obj: Any?, args: List) = try { - Result.success(invoke(obj, *args.toTypedArray())) + val invocation = invoke(obj, *args.toTypedArray()) + + Result.success(invocation) } catch (e: InvocationTargetException) { Result.failure(e.targetException) -} - -val KClass<*>.allNestedClasses: List> - get() = listOf(this) + nestedClasses.flatMap { it.allNestedClasses } +} \ No newline at end of file diff --git a/utbot-core/src/main/kotlin/org/utbot/common/Logging.kt b/utbot-core/src/main/kotlin/org/utbot/common/Logging.kt index fe94851557..bb24c9142c 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/Logging.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/Logging.kt @@ -3,26 +3,21 @@ package org.utbot.common import mu.KLogger import java.time.format.DateTimeFormatter -val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS") +val timeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS") +val dateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy_HH-mm-ss") -class LoggerWithLogMethod(val logger: KLogger, val logMethod: (() -> Any?) -> Unit) +class LoggerWithLogMethod(val logMethod: (() -> Any?) -> Unit) -fun KLogger.info(): LoggerWithLogMethod = LoggerWithLogMethod(this, this::info) -fun KLogger.debug(): LoggerWithLogMethod = LoggerWithLogMethod(this, this::debug) -fun KLogger.trace(): LoggerWithLogMethod = LoggerWithLogMethod(this, this::trace) +fun KLogger.info(): LoggerWithLogMethod = LoggerWithLogMethod(this::info) +fun KLogger.debug(): LoggerWithLogMethod = LoggerWithLogMethod(this::debug) +fun KLogger.trace(): LoggerWithLogMethod = LoggerWithLogMethod(this::trace) - -/** - * - */ fun elapsedSecFrom(startNano: Long): String { val elapsedNano = System.nanoTime() - startNano val elapsedS = elapsedNano.toDouble() / 1_000_000_000 return String.format("%.3f", elapsedS) + " sec" } - - /** * Structured logging. * @@ -40,16 +35,22 @@ fun elapsedSecFrom(startNano: Long): String { * You can use [closingComment] to add some result-depending comment to "Finished:" message. Special "" comment * is added if non local return happened in [block] */ -inline fun LoggerWithLogMethod.bracket( - msg: String, +inline fun LoggerWithLogMethod.measureTime( + crossinline msgBlock: () -> String, crossinline closingComment: (Result) -> Any? = { "" }, block: () -> T ): T { - logMethod { "Started: $msg" } + var msg = "" + + logMethod { + msg = msgBlock() + "Started: $msg" + } + val startNano = System.nanoTime() var alreadyLogged = false - var res : Maybe = Maybe.empty() + var res: Maybe = Maybe.empty() try { // Note: don't replace this one with runCatching, otherwise return from lambda breaks "finished" logging. res = Maybe(block()) @@ -68,11 +69,28 @@ inline fun LoggerWithLogMethod.bracket( } } -inline fun KLogger.catch(block: () -> T): T? { +inline fun KLogger.catchException(message: String = "Isolated", block: () -> T): T? { return try { block() } catch (e: Throwable) { - this.error(e) { "Isolated" } + this.error(message, e) null } } + +inline fun KLogger.logException(message: String = "Exception occurred", block: () -> T): T { + return try { + block() + } catch (e: Throwable) { + this.error(message, e) + throw e + } +} + +inline fun silent(block: () -> T): T? { + return try { + block() + } catch (_: Throwable) { + null + } +} \ No newline at end of file diff --git a/utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt index 762eff83e7..b74cb39a56 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/PathUtil.kt @@ -10,6 +10,12 @@ import java.util.* object PathUtil { + fun String.toPathOrNull(): Path? = try { + Paths.get(this) + } catch (e: Throwable) { + null + } + /** * Creates a Path from the String. */ diff --git a/utbot-core/src/main/kotlin/org/utbot/common/ProcessUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/ProcessUtil.kt index a0a4b136c3..3371fe29c8 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/ProcessUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/ProcessUtil.kt @@ -54,10 +54,9 @@ val currentProcessPid: Long get() = try { if (isJvm9Plus) { - ClassLoader.getSystemClassLoader().loadClass("java.lang.ProcessHandle").let { - val handle = it.getDeclaredMethod("current").invoke(it) - it.getDeclaredMethod("pid").invoke(handle) as Long - } + val handleClass = ClassLoader.getSystemClassLoader().loadClass("java.lang.ProcessHandle") + val handle = handleClass.getDeclaredMethod("current").invoke(handleClass) + handleClass.getDeclaredMethod("pid").invoke(handle) as Long } else { if (isWindows) { Kernel32.INSTANCE.GetCurrentProcessId() diff --git a/utbot-core/src/main/kotlin/org/utbot/common/ReflectionUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/ReflectionUtil.kt index c0e92d97ef..211e13a037 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/ReflectionUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/ReflectionUtil.kt @@ -17,17 +17,18 @@ object Reflection { unsafe = f.get(null) as Unsafe } - private val getDeclaredFields0Method: Method = + val getDeclaredFields0Method: Method = Class::class.java.getDeclaredMethod("getDeclaredFields0", Boolean::class.java).apply { isAccessible = true } + @Suppress("UNCHECKED_CAST") - private val fields: Array = + val fields: Array = getDeclaredFields0Method.invoke(Field::class.java, false) as Array // TODO: works on JDK 8-17. Doesn't work on JDK 18 - private val modifiersField: Field = + val modifiersField: Field = fields.first { it.name == "modifiers" } init { @@ -37,9 +38,13 @@ object Reflection { fun setModifiers(field: Field, modifiers: Int) { modifiersField.set(field, modifiers) } + + fun isModifiersAccessible(): Boolean { + return modifiersField.isAccessible + } } -inline fun AccessibleObject.withAccessibility(block: () -> R): R { +inline fun T.withAccessibility(block: T.() -> R): R { val prevAccessibility = isAccessible isAccessible = true @@ -60,7 +65,7 @@ inline fun AccessibleObject.withAccessibility(block: () -> R): R { * * Also note, that primitive static final fields may be inlined, so may not be possible to change. */ -inline fun Field.withAccessibility(block: () -> R): R { +inline fun Field.withAccessibility(block: Field.() -> R): R { val prevModifiers = modifiers val prevAccessibility = isAccessible @@ -111,4 +116,10 @@ val Class<*>.isFinal get() = Modifier.isFinal(modifiers) val Class<*>.isProtected - get() = Modifier.isProtected(modifiers) \ No newline at end of file + get() = Modifier.isProtected(modifiers) + +val Class<*>.nameOfPackage: String + get() = `package`?.name?:"" + +val Class<*>.allNestedClasses: List> + get() = listOf(this) + this.declaredClasses.flatMap { it.allNestedClasses } \ No newline at end of file diff --git a/utbot-core/src/main/kotlin/org/utbot/common/StopWatch.kt b/utbot-core/src/main/kotlin/org/utbot/common/StopWatch.kt index 83ec7bb00c..1ff1b149fa 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/StopWatch.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/StopWatch.kt @@ -28,12 +28,19 @@ class StopWatch { startTime = System.currentTimeMillis() } } - - fun stop() { + + /** + * @param compensationMillis the duration in millis that should be subtracted from [elapsedMillis] to compensate + * for stopping and restarting [StopWatch] taking some time, can also be used to compensate for some activities, + * that are hard to directly detect (e.g. class loading). + * + * NOTE: [compensationMillis] will never cause [elapsedMillis] become negative. + */ + fun stop(compensationMillis: Long = 0) { lock.withLockInterruptibly { - startTime?.let { - elapsedMillis += (System.currentTimeMillis() - it) - startTime = null + startTime?.let { startTime -> + elapsedMillis += ((System.currentTimeMillis() - startTime) - compensationMillis).coerceAtLeast(0) + this.startTime = null } } } @@ -52,7 +59,7 @@ class StopWatch { } } - fun get(unit: TimeUnit) = lock.withLockInterruptibly { + fun get(unit: TimeUnit = TimeUnit.MILLISECONDS) = lock.withLockInterruptibly { unsafeUpdate() unit.convert(elapsedMillis, TimeUnit.MILLISECONDS) } diff --git a/utbot-core/src/main/kotlin/org/utbot/common/ThreadUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/ThreadUtil.kt index 453f2d2468..2dd7fb9969 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/ThreadUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/ThreadUtil.kt @@ -2,8 +2,11 @@ package org.utbot.common import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.thread +import kotlin.concurrent.withLock import kotlin.properties.ReadOnlyProperty +import kotlin.random.Random import kotlin.reflect.KProperty @@ -24,31 +27,47 @@ class ThreadBasedExecutor { val threadLocal by threadLocalLazy { ThreadBasedExecutor() } } + /** + * Used to avoid calling [Thread.stop] during clean up. + * + * @see runCleanUpIfTimedOut + */ + private val timeOutCleanUpLock = ReentrantLock() + + /** + * `null` when either: + * - no tasks have yet been run + * - current task timed out, and we are waiting for its thread to die + */ + @Volatile private var thread: Thread? = null private var requestQueue = ArrayBlockingQueue<() -> Any?>(1) private var responseQueue = ArrayBlockingQueue>(1) + /** + * Can be called from lambda passed to [invokeWithTimeout]. + * [ThreadBasedExecutor] guarantees that it won't attempt to terminate [cleanUpBlock] with [Thread.stop]. + */ + fun runCleanUpIfTimedOut(cleanUpBlock: () -> Unit) { + timeOutCleanUpLock.withLock { + if (thread == null) + cleanUpBlock() + } + } /** * Invoke [action] with timeout. * * [stopWatch] is used to respect specific situations (such as class loading and transforming) while invoking. */ - fun invokeWithTimeout(timeoutMillis: Long, stopWatch: StopWatch? = null, action:() -> Any?) : Result? { - if (thread?.isAlive != true) { - requestQueue = ArrayBlockingQueue<() -> Any?>(1) - responseQueue = ArrayBlockingQueue>(1) - - thread = thread(name = "executor", isDaemon = true) { - try { - while (true) { - val next = requestQueue.take() - responseQueue.offer(kotlin.runCatching { next() }) - } - } catch (_: InterruptedException) {} - } - } + fun invokeWithTimeout( + timeoutMillis: Long, + stopWatch: StopWatch? = null, + threadDeathThrowPeriodMillis: Long = 10_000, + action:() -> Any? + ) : Result? { + ensureThreadIsAlive() requestQueue.offer { try { @@ -71,16 +90,46 @@ class ThreadBasedExecutor { if (res == null) { try { val t = thread ?: return res + thread = null t.interrupt() t.join(10) - if (t.isAlive) - @Suppress("DEPRECATION") - t.stop() + // to avoid race condition we need to wait for `t` to die + while (t.isAlive) { + timeOutCleanUpLock.withLock { + @Suppress("DEPRECATION") + t.stop() + } + // If somebody catches `ThreadDeath`, for now we + // just wait for [threadDeathThrowPeriod] and throw another one. + // + // A better approach may be to kill instrumented process. + t.join(threadDeathThrowPeriodMillis) + } } catch (_: Throwable) {} + } + return res + } + + fun invokeWithoutTimeout(action:() -> Any?) : Result { + ensureThreadIsAlive() - thread = null + requestQueue.offer(action) + return responseQueue.take() + } + private fun ensureThreadIsAlive() { + if (thread?.isAlive != true) { + requestQueue = ArrayBlockingQueue<() -> Any?>(1) + responseQueue = ArrayBlockingQueue>(1) + + thread = thread(name = "executor @${Random.nextInt(10_000)}", isDaemon = true) { + try { + while (thread === Thread.currentThread()) { + val next = requestQueue.take() + responseQueue.offer(kotlin.runCatching { next() }) + } + } catch (_: InterruptedException) {} + } } - return res } } \ No newline at end of file diff --git a/utbot-core/src/test/kotlin/org/utbot/common/AbstractSettingsTest.kt b/utbot-core/src/test/kotlin/org/utbot/common/AbstractSettingsTest.kt new file mode 100644 index 0000000000..8290771693 --- /dev/null +++ b/utbot-core/src/test/kotlin/org/utbot/common/AbstractSettingsTest.kt @@ -0,0 +1,51 @@ +package org.utbot.common + +import java.io.InputStream +import mu.KLogger +import mu.KotlinLogging +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + + +internal class AbstractSettingsTest { + @Test + fun testParsing() { + var settings = createSettings("testBoolean=false\ntestInt=3\ntestIntRange=2\ntestLong=333\ntestLongRange=222") + Assertions.assertFalse(settings.testBoolean) + Assertions.assertEquals(3, settings.testInt) + Assertions.assertEquals(2, settings.testIntRange) + Assertions.assertEquals(333, settings.testLong) + Assertions.assertEquals(222, settings.testLongRange) + + settings = createSettings("testBoolean=invalid\ntestInt=-1\ntestIntRange=-1\ntestLong=-111\ntestLongRange=-111") + Assertions.assertTrue(settings.testBoolean) + Assertions.assertEquals(-1, settings.testInt) + Assertions.assertEquals(1, settings.testIntRange) + Assertions.assertEquals(-111, settings.testLong) + Assertions.assertEquals(111, settings.testLongRange) + + settings = createSettings("") + Assertions.assertTrue(settings.testBoolean) + Assertions.assertEquals(3, settings.testInt) + Assertions.assertEquals(333, settings.testLongRange) + } + + private fun createSettings(propertiesText: String): TestSettings { + AbstractSettings.setupFactory(object : SettingsContainerFactory { + override fun createSettingsContainer( + logger: KLogger, defaultKeyForSettingsPath: String, defaultSettingsPath: String? + ) = object : PropertiesSettingsContainer(logger, defaultKeyForSettingsPath, defaultSettingsPath) { + override fun getInputStream(): InputStream = propertiesText.byteInputStream() + } + }) + return TestSettings() + } + + internal class TestSettings : AbstractSettings(KotlinLogging.logger {}, "") { + val testBoolean: Boolean by getBooleanProperty(true) + val testInt: Int by getIntProperty(3) + val testIntRange: Int by getIntProperty(3, 1, 5) + val testLong: Long by getLongProperty(333) + val testLongRange: Long by getLongProperty(333, 111, 555) + } +} \ No newline at end of file diff --git a/utbot-framework-api/build.gradle.kts b/utbot-framework-api/build.gradle.kts index 607b10e69a..f94f8a1b7f 100644 --- a/utbot-framework-api/build.gradle.kts +++ b/utbot-framework-api/build.gradle.kts @@ -1,32 +1,31 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - val junit4Version: String by rootProject -val sootCommitHash: String by rootProject +val sootVersion: String by rootProject val commonsLangVersion: String by rootProject val kotlinLoggingVersion: String? by rootProject - -plugins { - id("com.github.johnrengelman.shadow") version "7.1.2" -} +val rdVersion: String? by rootProject +val kryoVersion: String? by rootProject +val kryoSerializersVersion: String? by rootProject dependencies { api(project(":utbot-core")) api(project(":utbot-api")) api(project(":utbot-rd")) - implementation(group ="com.jetbrains.rd", name = "rd-framework", version = "2022.3.1") - implementation(group ="com.jetbrains.rd", name = "rd-core", version = "2022.3.1") - implementation("com.github.UnitTestBot:soot:${sootCommitHash}") + implementation(group ="com.jetbrains.rd", name = "rd-framework", version = rdVersion) + implementation(group ="com.jetbrains.rd", name = "rd-core", version = rdVersion) + implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { + exclude(group="com.google.guava", module="guava") + } implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) // TODO do we really need apache commons? implementation(group = "org.apache.commons", name = "commons-lang3", version = commonsLangVersion) + implementation(group = "com.esotericsoftware.kryo", name = "kryo5", version = kryoVersion) + // this is necessary for serialization of some collections + implementation(group = "de.javakaffee", name = "kryo-serializers", version = kryoSerializersVersion) testImplementation(group = "junit", name = "junit", version = junit4Version) } -tasks { - withType { - archiveClassifier.set(" ") - minimize() - } +java { + withSourcesJar() } tasks { diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/TrustedPackages.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/TrustedPackages.kt new file mode 100644 index 0000000000..45da218a54 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/TrustedPackages.kt @@ -0,0 +1,32 @@ +package org.utbot.framework + +import org.utbot.framework.plugin.api.ClassId +import soot.SootClass + +/** + * Cache for already discovered trusted/untrusted packages. + */ +private val isPackageTrusted: MutableMap = mutableMapOf() + +/** + * Determines whether [this] class is from trusted libraries as defined in [TrustedLibraries]. + */ +fun SootClass.isFromTrustedLibrary(): Boolean = isFromTrustedLibrary(packageName) + +/** + * Determines whether [this] class is from trusted libraries as defined in [TrustedLibraries]. + */ +fun ClassId.isFromTrustedLibrary(): Boolean = isFromTrustedLibrary(packageName) + +/** + * Determines whether [packageName] is from trusted libraries as defined in [TrustedLibraries]. + */ +fun isFromTrustedLibrary(packageName: String): Boolean { + isPackageTrusted[packageName]?.let { + return it + } + + val isTrusted = TrustedLibraries.trustedLibraries.any { packageName.startsWith(it, ignoreCase = false) } + + return isTrusted.also { isPackageTrusted[packageName] = it } +} 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 8ce3cf15ee..112c5bfb7e 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,34 +1,38 @@ package org.utbot.framework -import com.jetbrains.rd.util.LogLevel +import java.io.File import mu.KotlinLogging import org.utbot.common.AbstractSettings +import java.lang.reflect.Executable private val logger = KotlinLogging.logger {} /** * Path to the utbot home folder. */ -internal val utbotHomePath = "${System.getProperty("user.home")}/.utbot" +internal val utbotHomePath = "${System.getProperty("user.home")}${File.separatorChar}.utbot" /** * Default path for properties file */ -private val defaultSettingsPath = "$utbotHomePath/settings.properties" +private val defaultSettingsPath = "$utbotHomePath${File.separatorChar}settings.properties" private const val defaultKeyForSettingsPath = "utbot.settings.path" /** * Default concrete execution timeout (in milliseconds). */ -const val DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS = 1000L +const val DEFAULT_EXECUTION_TIMEOUT_IN_INSTRUMENTED_PROCESS_MS = 1000L -object UtSettings : AbstractSettings( - logger, defaultKeyForSettingsPath, defaultSettingsPath -) { +object UtSettings : AbstractSettings(logger, defaultKeyForSettingsPath, defaultSettingsPath) { + + fun defaultSettingsPath() = defaultSettingsPath + + @JvmStatic + fun getPath(): String = System.getProperty(defaultKeyForSettingsPath)?: defaultSettingsPath /** * Setting to disable coroutines debug explicitly. * - * True by default, set it to false if debug info is required. + * Set it to false if debug info is required. */ var disableCoroutinesDebug: Boolean by getBooleanProperty(true) @@ -42,13 +46,13 @@ object UtSettings : AbstractSettings( * * Set it to 0 to disable timeout. */ - var checkSolverTimeoutMillis: Int by getIntProperty(1000) + var checkSolverTimeoutMillis: Int by getIntProperty(1000, 0, Int.MAX_VALUE) /** * Timeout for symbolic execution * */ - var utBotGenerationTimeoutInMillis by getLongProperty(60000L) + var utBotGenerationTimeoutInMillis by getLongProperty(60000L, 1000L, Int.MAX_VALUE.toLong()) /** * Random seed in path selector. @@ -86,7 +90,7 @@ object UtSettings : AbstractSettings( /** * Use debug visualization. * - * False by default, set it to true if debug visualization is needed. + * Set it to true if debug visualization is needed. */ var useDebugVisualization by getBooleanProperty(false) @@ -100,32 +104,49 @@ object UtSettings : AbstractSettings( /** * Set the value to true to show library classes' graphs in visualization. - * - * False by default. */ val showLibraryClassesInVisualization by getBooleanProperty(false) /** - * Method is paused after this timeout to give an opportunity other methods - * to work + * Use simplification of UtExpressions. + * + * Set it to false to disable expression simplification. + * @see UtBot Expression Optimizations */ - var timeslotForOneToplevelMethodTraversalMs by getIntProperty(2000) + var useExpressionSimplification by getBooleanProperty(true) /** - * Use simplification of UtExpressions. + * Enable the Summarization module to generate summaries for methods under test. * - * True by default, set it to false to disable expression simplification. - * @see - * UtBot Expression Optimizations + * Note: if it is [SummariesGenerationType.NONE], + * all the execution for a particular method will be stored at the same nameless region. */ - var useExpressionSimplification by getBooleanProperty(true) + var summaryGenerationType by getEnumProperty(SummariesGenerationType.FULL) - /* - * Activate or deactivate tests on comments && names/displayNames - * */ - var testSummary by getBooleanProperty(true) - var testName by getBooleanProperty(true) - var testDisplayName by getBooleanProperty(true) + /** + * If True test comments will be generated. + */ + var enableJavaDocGeneration by getBooleanProperty(true) + + /** + * If True cluster comments will be generated. + */ + var enableClusterCommentsGeneration by getBooleanProperty(true) + + /** + * If True names for tests will be generated. + */ + var enableTestNamesGeneration by getBooleanProperty(true) + + /** + * If True display names for tests will be generated. + */ + var enableDisplayNameGeneration by getBooleanProperty(true) + + /** + * If True display name in from -> to style will be generated. + */ + var useDisplayNameArrowStyle by getBooleanProperty(true) /** * Generate summaries using plugin's custom JavaDoc tags. @@ -133,19 +154,17 @@ object UtSettings : AbstractSettings( var useCustomJavaDocTags by getBooleanProperty(true) /** - * Enable the machine learning module to generate summaries for methods under test. - * True by default. + * This option regulates which [NullPointerException] check should be performed for nested methods. * - * Note: if it is false, all the execution for a particular method will be stored at the same nameless region. + * Set an option in true if you want to perform NPE check in the corresponding situations, otherwise set false. */ - var enableMachineLearningModule by getBooleanProperty(true) + var checkNpeInNestedMethods by getBooleanProperty(true) /** - * Options below regulate which [NullPointerException] check should be performed. + * This option regulates which [NullPointerException] check should be performed for nested not private methods. * * Set an option in true if you want to perform NPE check in the corresponding situations, otherwise set false. */ - var checkNpeInNestedMethods by getBooleanProperty(true) var checkNpeInNestedNotPrivateMethods by getBooleanProperty(false) /** @@ -153,7 +172,7 @@ object UtSettings : AbstractSettings( * in non-application classes. Set by true, this option highly decreases test's readability in some cases * because of using reflection API for setting final/non-public fields in non-application classes. * - * NOTE: default false value loses some executions with NPE in system classes, but often most of these executions + * NOTE: With false value loses some executions with NPE in system classes, but often most of these executions * are not expected by user. */ var maximizeCoverageUsingReflection by getBooleanProperty(false) @@ -164,32 +183,21 @@ object UtSettings : AbstractSettings( */ var substituteStaticsWithSymbolicVariable by getBooleanProperty(true) - /** * Use concrete execution. - * - * True by default. */ var useConcreteExecution by getBooleanProperty(true) - /** - * Enable check of full coverage for methods with code generations tests. - * - * TODO doesn't work for now JIRA:1407 - */ - var checkCoverageInCodeGenerationTests by getBooleanProperty(true) - /** * Enable code generation tests with every possible configuration * for every method in samples. * - * Important: disabled by default. This check requires enormous amount of time. + * Important: is enabled generation requires enormous amount of time. */ var checkAllCombinationsForEveryTestInSamples by getBooleanProperty(false) /** * Enable transformation UtCompositeModels into UtAssembleModels using AssembleModelGenerator. - * True by default. * * Note: false doesn't mean that there will be no assemble models, it means that the generator will be turned off. * Assemble models will present for lists, sets, etc. @@ -200,12 +208,10 @@ object UtSettings : AbstractSettings( * Test related files from the temp directory that are older than [daysLimitForTempFiles] * will be removed at the beginning of the test run. */ - var daysLimitForTempFiles by getIntProperty(3) + var daysLimitForTempFiles by getIntProperty(3, 0, 30) /** * Enables soft constraints in the engine. - * - * True by default. */ var preferredCexOption by getBooleanProperty(true) @@ -223,27 +229,33 @@ object UtSettings : AbstractSettings( /** * Set the total attempts to improve coverage by fuzzer. */ - var fuzzingMaxAttempts: Int by getIntProperty(Int.MAX_VALUE) + var fuzzingMaxAttempts: Int by getIntProperty(Int.MAX_VALUE, 0, Int.MAX_VALUE) /** * Fuzzer tries to generate and run tests during this time. */ - var fuzzingTimeoutInMillis: Long by getLongProperty(3_000L) + var fuzzingTimeoutInMillis: Long by getLongProperty(3_000L, 0, Long.MAX_VALUE) + + /** + * Find implementations of interfaces and abstract classes to fuzz. + */ + var fuzzingImplementationOfAbstractClasses: Boolean by getBooleanProperty(true) + + /** + * Use methods to mutate fields of classes different from class under test or not. + */ + var tryMutateOtherClassesFieldsWithMethods: Boolean by getBooleanProperty(false) /** * Generate tests that treat possible overflows in arithmetic operations as errors * that throw Arithmetic Exception. - * - * False by default. */ var treatOverflowAsError: Boolean by getBooleanProperty(false) /** * Generate tests that treat assertions as error suits. - * - * True by default. */ - var treatAssertAsErrorSuit: Boolean by getBooleanProperty(true) + var treatAssertAsErrorSuite: Boolean by getBooleanProperty(true) /** * Instrument all classes before start @@ -259,27 +271,117 @@ object UtSettings : AbstractSettings( /** * Timeout for specific concrete execution (in milliseconds). */ - var concreteExecutionTimeoutInChildProcess: Long by getLongProperty( - DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS + var concreteExecutionDefaultTimeoutInInstrumentedProcessMillis: Long by getLongProperty( + DEFAULT_EXECUTION_TIMEOUT_IN_INSTRUMENTED_PROCESS_MS ) /** - * Log level for engine process, which started in idea on generate tests action. + * Enable taint analysis or not. + */ + var useTaintAnalysis by getBooleanProperty(false) + + /** + * If it is true, [Traverser] forks the state and creates checks for each taint mark separately, + * otherwise, it processes all available taint marks in one [Traverser.implicitlyThrowException] request. + * + * @see [org.utbot.engine.Traverser.processTaintSink] + */ + var throwTaintErrorForEachMarkSeparately = true + + /** + * How deep we should analyze the throwables. + */ + val exploreThrowableDepth: ExploreThrowableDepth + get() = + if (useTaintAnalysis) { + ExploreThrowableDepth.EXPLORE_ALL_STATEMENTS + } else { + ExploreThrowableDepth.SKIP_ALL_STATEMENTS + } + +// region engine process debug + + /** + * Path to custom log4j2 configuration file for EngineProcess. + * By default utbot-intellij/src/main/resources/log4j2.xml is used. + * Also default value is used if provided value is not a file. + */ + var engineProcessLogConfigFile by getStringProperty("") + + /** + * The property is useful only for the IntelliJ IDEs. + * If the property is set in true the engine process opens a debug port. + * @see runInstrumentedProcessWithDebug + * @see org.utbot.intellij.plugin.process.EngineProcess + */ + var runEngineProcessWithDebug by getBooleanProperty(false) + + /** + * The engine process JDWP agent's port of the engine process. + * A debugger attaches to the port in order to debug the process. + */ + var engineProcessDebugPort by getIntProperty(5005) + + /** + * Value of the suspend mode for the JDWP agent of the engine process. + * If the value is true, the engine process will suspend until a debugger attaches to it. + */ + var suspendEngineProcessExecutionInDebugMode by getBooleanProperty(true) + +// endregion + +// region spring analyzer process debug + /** + * The property is useful only for the IntelliJ IDEs. + * If the property is set in true the spring analyzer process opens a debug port. + * @see runInstrumentedProcessWithDebug + * @see org.utbot.spring.process.SpringAnalyzerProcess + */ + var runSpringAnalyzerProcessWithDebug by getBooleanProperty(false) + + /** + * The spring analyzer process JDWP agent's port. + * A debugger attaches to the port in order to debug the process. + */ + var springAnalyzerProcessDebugPort by getIntProperty(5007) + + /** + * Value of the suspend mode for the JDWP agent of the spring analyzer process. + * If the value is true, the spring analyzer process will suspend until a debugger attaches to it. + */ + var suspendSpringAnalyzerProcessExecutionInDebugMode by getBooleanProperty(true) + +// endregion + +// region instrumented process debug + /** + * The instrumented process JDWP agent's port of the instrumented process. + * A debugger attaches to the port in order to debug the process. */ - var engineProcessLogLevel by getEnumProperty(LogLevel.Info) + var instrumentedProcessDebugPort by getIntProperty(5006, 0, 65535) /** - * Log level for concrete executor process. + * Value of the suspend mode for the JDWP agent of the instrumented process. + * If the value is true, the instrumented process will suspend until a debugger attaches to it. */ - var childProcessLogLevel by getEnumProperty(LogLevel.Info) + var suspendInstrumentedProcessExecutionInDebugMode by getBooleanProperty(true) /** - * 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. + * If true, runs the instrumented process with the ability to attach a debugger. + * + * To debug the instrumented process, set the breakpoint in the + * [org.utbot.instrumentation.rd.InstrumentedProcess.Companion.invoke] + * and in the instrumented process's main function and run the main process. + * Then run the remote JVM debug configuration in IDEA. + * If you see the message in console about successful connection, then + * the debugger is attached successfully. + * Now you can put the breakpoints in the instrumented process and debug + * both processes simultaneously. * - * False by default (for saving disk space). + * @see [org.utbot.instrumentation.rd.InstrumentedProcess.Companion.invoke] */ - var logConcreteExecutionErrors by getBooleanProperty(false) + var runInstrumentedProcessWithDebug by getBooleanProperty(false) +// endregion /** * Number of branch instructions using for clustering executions in the test minimization phase. @@ -291,21 +393,23 @@ object UtSettings : AbstractSettings( */ var minimizeCrashExecutions by getBooleanProperty(true) + /** + * Determines maximum number of executions with unknown coverage per method per result type. + * In [ContestUsvm] it is useful if concrete fails, so we use symbolic execution result without trace. + */ + var maxUnknownCoverageExecutionsPerMethodPerResultType by getIntProperty(10) + /** * Enable it to calculate unsat cores for hard constraints as well. * It may be usefull during debug. * * Note: it might highly impact performance, so do not enable it in release mode. - * - * False by default. */ 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) @@ -315,11 +419,6 @@ object UtSettings : AbstractSettings( */ 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 */ @@ -356,14 +455,9 @@ object UtSettings : AbstractSettings( var testCounter by getIntProperty(0) /** - * Flag for Subpath and NN selectors whether they are combined (Subpath use several indexes, NN use several models) + * Flag that indicates whether tests for synthetic (see [Executable.isSynthetic]) and implicitly declared methods (like values, valueOf in enums) should be generated, or not */ - var singleSelector by getBooleanProperty(true) - - /** - * Flag that indicates whether tests for synthetic methods (values, valueOf in enums) should be generated, or not - */ - var skipTestGenerationForSyntheticMethods by getBooleanProperty(true) + var skipTestGenerationForSyntheticAndImplicitlyDeclaredMethods by getBooleanProperty(true) /** * Flag that indicates whether should we branch on and set static fields from trusted libraries or not. @@ -373,15 +467,167 @@ object UtSettings : AbstractSettings( var ignoreStaticsFromTrustedLibraries by getBooleanProperty(true) /** - * Use the sandbox in the concrete executor. + * Use the sandbox in the instrumented process. * - * If true (default), the sandbox will prevent potentially dangerous calls, e.g., file access, reading + * If true, the sandbox will prevent potentially dangerous calls, e.g., file access, reading * or modifying the environment, calls to `Unsafe` methods etc. * * If false, all these operations will be enabled and may lead to data loss during code analysis * and test generation. */ var useSandbox by getBooleanProperty(true) + + /** + * Transform bytecode in the instrumented process. + * + * If true, bytecode transformation will help fuzzing to find interesting input data, but the size of bytecode can increase. + * + * If false, bytecode won`t be changed. + */ + var useBytecodeTransformation by getBooleanProperty(false) + + /** + * Limit for number of generated tests per method (in each region) + */ + var maxTestsPerMethodInRegion by getIntProperty(50, 1, Integer.MAX_VALUE) + + /** + * Max file length for generated test file + */ + const val DEFAULT_MAX_FILE_SIZE = 1000000 + var maxTestFileSize by getProperty(DEFAULT_MAX_FILE_SIZE, ::parseFileSize) + + + fun parseFileSize(s: String): Int { + val suffix = StringBuilder() + var value = 0 + for (ch in s) { + (ch - '0').let { + if (it in 0..9) { + value = value * 10 + it + } else suffix.append(ch) + } + } + when (suffix.toString().trim().lowercase()) { + "k", "kb" -> value *= 1000 + "m", "mb" -> value *= 1000000 + } + return if (value > 0) value else DEFAULT_MAX_FILE_SIZE // fallback for incorrect value + } + + /** + * If this options set in true, all soot classes will be removed from a Soot Scene, + * therefore, you will be unable to test soot classes. + */ + var removeSootClassesFromHierarchy by getBooleanProperty(true) + + /** + * If this options set in true, all UtBot classes will be removed from a Soot Scene, + * therefore, you will be unable to test UtBot classes. + */ + var removeUtBotClassesFromHierarchy by getBooleanProperty(true) + + /** + * Use this option to enable calculation and logging of MD5 for dropped states by statistics. + * Example of such logging: + * Dropping state (lastStatus=UNDEFINED) by the distance statistics. MD5: 5d0bccc242e87d53578ca0ef64aa5864 + */ + var enableLoggingForDroppedStates by getBooleanProperty(false) + + /** + * If this option set in true, depending on the number of possible types for + * a particular object will be used either type system based on conjunction + * or on bit vectors. + * + * @see useBitVecBasedTypeSystem + */ + var useBitVecBasedTypeSystem by getBooleanProperty(true) + + /** + * The number of types on which the choice of the type system depends. + */ + var maxTypeNumberForEnumeration by getIntProperty(64) + + /** + * The threshold for numbers of types for which they will be encoded into solver. + * It is used to do not encode big type storages due to significand performance degradation. + */ + var maxNumberOfTypesToEncode by getIntProperty(512) + + /** + * The behaviour of further analysis if tests generation cancellation is requested. + */ + var cancellationStrategyType by getEnumProperty(CancellationStrategyType.SAVE_PROCESSED_RESULTS) + + /** + * Depending on this option, sections might be analyzed or not. + * Note that some clinit sections still will be initialized using runtime information. + */ + var enableClinitSectionsAnalysis by getBooleanProperty(true) + + /** + * Process all clinit sections concretely. + * + * If [enableClinitSectionsAnalysis] is false, it disables effect of this option as well. + * Note that values processed concretely won't be replaced with unbounded symbolic variables. + */ + var processAllClinitSectionsConcretely by getBooleanProperty(false) + + /** + * In cases where we don't have a body for a method, we can either throw an exception + * or treat this a method as a source of an unbounded symbolic variable returned as a result. + * + * If this option is set in true, instead of analysis we will return an unbounded symbolic + * variable with a corresponding type. Otherwise, an exception will be thrown. + * + * Default value is false since it is not a common situation when you cannot retrieve a body + * from a regular method. Setting this option in true might be suitable in situations when + * it is more important not to fall at all rather than work precisely. + */ + var treatAbsentMethodsAsUnboundedValue by getBooleanProperty(false) + + // region preferred size options + // Changes in this region might severe influence on the performance of symbolic execution. + + /** + * A maximum size for any array in the program. Note that input arrays might be less than this value + * due to the symbolic engine limitation, see `org.utbot.engine.Traverser.softMaxArraySize`. + */ + var maxArraySize by getIntProperty(1024) + + // endregion + + // region UTBot light related options + // Changes to improve symbolic engine for light version + + var disableUnsatChecking by getBooleanProperty(false) + + // endregion + + // region Spring-related options + + /** + * When generating integration tests we only partially reset context in between executions to save time. + * For example, entity id generators do not get reset. It may lead to non-reproduceable results if + * IDs leak to the output of the method under test. + * + * To cope with that, we rerun executions that are left after minimization, fully resetting Spring context + * between executions. However, full context reset is slow, so we use this setting to limit number of + * tests per method that are rerun with full context reset in case minimization outputs too many tests. + */ + var maxSpringContextResetsPerMethod by getIntProperty(25, 0, Int.MAX_VALUE) + + // endregion + + // region codegen options + + /** + * Add "test method start marker" and "test method end marker" around each test, can be used to + * detect uncompilable tests and remove them. + */ + var addTestMethodMarkers by getBooleanProperty(false) + + // endregion } /** @@ -398,6 +644,11 @@ enum class PathSelectorType { */ INHERITORS_SELECTOR, + /** + * [BFSSelector] + */ + BFS_SELECTOR, + /** * [SubpathGuidedSelector] */ @@ -435,8 +686,35 @@ enum class PathSelectorType { } enum class TestSelectionStrategyType { - DO_NOT_MINIMIZE_STRATEGY, // Always adds new test - COVERAGE_STRATEGY // Adds new test only if it increases coverage + /** + * Always adds new test + */ + DO_NOT_MINIMIZE_STRATEGY, + + /** + * Adds new test only if it increases coverage + */ + COVERAGE_STRATEGY, +} + +/** + * Describes the behaviour if test generation is canceled. + */ +enum class CancellationStrategyType { + /** + * Do not react on cancellation + */ + NONE, + + /** + * Clear all generated test classes + */ + CANCEL_EVERYTHING, + + /** + * Show already processed test classes + */ + SAVE_PROCESSED_RESULTS } /** @@ -468,3 +746,44 @@ enum class MLPredictorType { */ LINREG } + +/** + * Enum to describe how we analyze code to obtain summaries. + */ +enum class SummariesGenerationType { + /** + * All possible analysis actions are taken + */ + FULL, + + /** + * Analysis actions based on sources are NOT taken + */ + LIGHT, + + /** + * No summaries are generated + */ + NONE, +} + +/** + * Enum to describe how deep we should analyze the throwables. + */ +enum class ExploreThrowableDepth { + + /** + * Skip all statements between throwable's `new` and `` statements. + */ + SKIP_ALL_STATEMENTS, + + /** + * Skip only throwable's statement. + */ + SKIP_INIT_STATEMENT, + + /** + * Do not skip statements. + */ + EXPLORE_ALL_STATEMENTS +} diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/fuzzer/IdGenerator.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/fuzzer/IdGenerator.kt new file mode 100644 index 0000000000..1447694569 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/fuzzer/IdGenerator.kt @@ -0,0 +1,81 @@ +package org.utbot.framework.fuzzer + +import java.util.* +import java.util.concurrent.atomic.AtomicInteger + +/** + * Identifier generator interface for fuzzer model providers. + * + * Provides fresh identifiers for generated models. + * + * Warning: specific generators are not guaranteed to be thread-safe. + * + * @param Id the identifier type (e.g., [Int] for [UtReferenceModel] providers) + */ +interface IdGenerator { + /** + * Create a fresh identifier. Each subsequent call should return a different value. + * + * The method is not guaranteed to be thread-safe, unless an implementation makes such a guarantee. + */ + fun createId(): Id +} + +/** + * Identity preserving identifier generator interface. + * + * It allows to optionally save identifiers assigned to specific objects and later get the same identifiers + * for these objects instead of fresh identifiers. This feature is necessary, for example, to implement reference + * equality for enum models. + * + * Warning: specific generators are not guaranteed to be thread-safe. + * + * @param Id the identifier type (e.g., [Int] for [UtReferenceModel] providers) + */ +interface IdentityPreservingIdGenerator : IdGenerator { + /** + * Return an identifier for a specified non-null object. If an identifier has already been assigned + * to an object, subsequent calls should return the same identifier for this object. + * + * Note: the interface does not specify whether reference equality or regular `equals`/`compareTo` equality + * will be used to compare objects. Each implementation may provide these guarantees by itself. + * + * The method is not guaranteed to be thread-safe, unless an implementation makes such a guarantee. + */ + fun getOrCreateIdForValue(value: Any): Id +} + +/** + * An identity preserving id generator for fuzzer value providers that returns identifiers of type [Int]. + * + * When identity-preserving identifier is requested, objects are compared by reference. + * The generator is not thread-safe. + * + * @param lowerBound an integer value so that any generated identifier is strictly greater than it. + * + * Warning: when generating [UtReferenceModel] identifiers, no identifier should be equal to zero, + * as this value is reserved for [UtNullModel]. To guarantee it, [lowerBound] should never be negative. + * Avoid using custom lower bounds (maybe except fuzzer unit tests), use the predefined default value instead. + */ +class ReferencePreservingIntIdGenerator(lowerBound: Int = DEFAULT_LOWER_BOUND) : IdentityPreservingIdGenerator { + private val lastId: AtomicInteger = AtomicInteger(lowerBound) + private val cache: IdentityHashMap = IdentityHashMap() + + override fun getOrCreateIdForValue(value: Any): Int { + return cache.getOrPut(value) { createId() } + } + + override fun createId(): Int { + return lastId.incrementAndGet() + } + + companion object { + /** + * The default lower bound (all generated integer identifiers will be greater than it). + * + * It is defined as a large value because all synthetic [UtModel] instances + * must have greater identifiers than the real models. + */ + const val DEFAULT_LOWER_BOUND: Int = 1500_000_000 + } +} 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 ae9921257e..ec289f2b23 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 @@ -8,11 +8,11 @@ package org.utbot.framework.plugin.api +import org.utbot.common.FileUtil import org.utbot.common.isDefaultValue import org.utbot.common.withToStringThreadLocalReentrancyGuard import org.utbot.framework.UtSettings -import org.utbot.framework.plugin.api.impl.FieldIdReflectionStrategy -import org.utbot.framework.plugin.api.impl.FieldIdSootStrategy +import org.utbot.framework.plugin.api.util.ModifierFactory import org.utbot.framework.plugin.api.util.booleanClassId import org.utbot.framework.plugin.api.util.byteClassId import org.utbot.framework.plugin.api.util.charClassId @@ -24,7 +24,10 @@ 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.isPrimitive +import org.utbot.framework.plugin.api.util.isStatic import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.jField +import org.utbot.framework.plugin.api.util.kClass import org.utbot.framework.plugin.api.util.longClassId import org.utbot.framework.plugin.api.util.method import org.utbot.framework.plugin.api.util.primitiveTypeJvmNameOrNull @@ -33,14 +36,31 @@ import org.utbot.framework.plugin.api.util.shortClassId import org.utbot.framework.plugin.api.util.supertypeOfAnonymousClass import org.utbot.framework.plugin.api.util.toReferenceTypeBytecodeSignature import org.utbot.framework.plugin.api.util.voidClassId -import soot.* +import soot.ArrayType +import soot.BooleanType +import soot.ByteType +import soot.CharType +import soot.DoubleType +import soot.FloatType +import soot.IntType +import soot.LongType +import soot.RefType +import soot.ShortType +import soot.SootClass +import soot.Type +import soot.VoidType import soot.jimple.JimpleBody import soot.jimple.Stmt import java.io.File -import java.lang.reflect.Modifier import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract -import org.utbot.common.FileUtil +import org.utbot.common.isAbstract +import org.utbot.framework.plugin.api.mapper.UtModelMapper +import org.utbot.framework.plugin.api.mapper.map +import org.utbot.framework.plugin.api.mapper.mapPreservingType +import org.utbot.framework.plugin.api.util.SpringModelUtils +import org.utbot.framework.process.OpenModulesContainer +import soot.SootMethod const val SYMBOLIC_NULL_ADDR: Int = 0 @@ -77,6 +97,17 @@ data class Step( } } +/** + * One symbolic step. + * + * @see UtSymbolicExecution.symbolicSteps + */ +data class SymbolicStep( + val method: SootMethod, + val lineNumber: Int, + val callDepth: Int, +) + /** * Traverse result. @@ -103,7 +134,23 @@ abstract class UtExecution( var summary: List? = null, var testMethodName: String? = null, var displayName: String? = null -) : UtResult() +) : UtResult() { + abstract fun copy( + stateBefore: EnvironmentModels = this.stateBefore, + stateAfter: EnvironmentModels = this.stateAfter, + result: UtExecutionResult = this.result, + coverage: Coverage? = this.coverage, + summary: List? = this.summary, + testMethodName: String? = this.testMethodName, + displayName: String? = this.displayName, + ): UtExecution + + val executableToCall get() = stateBefore.executableToCall +} + +interface UtExecutionWithInstrumentation { + val instrumentation: List +} /** * Symbolic execution. @@ -120,14 +167,15 @@ class UtSymbolicExecution( stateBefore: EnvironmentModels, stateAfter: EnvironmentModels, result: UtExecutionResult, - val instrumentation: List, + override val instrumentation: List, val path: MutableList, val fullPath: List, coverage: Coverage? = null, summary: List? = null, testMethodName: String? = null, - displayName: String? = null -) : UtExecution(stateBefore, stateAfter, result, coverage, summary, testMethodName, displayName) { + displayName: String? = null, + /** Convenient view of the full symbolic path */ val symbolicSteps: List = listOf(), +) : UtExecution(stateBefore, stateAfter, result, coverage, summary, testMethodName, displayName), UtExecutionWithInstrumentation { /** * By design the 'before' and 'after' states contain info about the same fields. * It means that it is not possible for a field to be present at 'before' and to be absent at 'after'. @@ -136,6 +184,29 @@ class UtSymbolicExecution( val staticFields: Set get() = stateBefore.statics.keys + var containsMocking: Boolean = false + + override fun copy( + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + result: UtExecutionResult, + coverage: Coverage?, + summary: List?, + testMethodName: String?, + displayName: String? + ): UtExecution = UtSymbolicExecution( + stateBefore = stateBefore, + stateAfter = stateAfter, + result = result, + instrumentation = instrumentation, + path = path, + fullPath = fullPath, + coverage = coverage, + summary = summary, + testMethodName = testMethodName, + displayName = displayName + ) + override fun toString(): String = buildString { append("UtSymbolicExecution(") appendLine() @@ -159,20 +230,29 @@ class UtSymbolicExecution( append(")") } - fun copy(stateAfter: EnvironmentModels, result: UtExecutionResult, coverage: Coverage): UtResult { - return UtSymbolicExecution( - stateBefore, - stateAfter, - result, - instrumentation, - path, - fullPath, - coverage, - summary, - testMethodName, - displayName - ) - } + fun copy( + stateBefore: EnvironmentModels = this.stateBefore, + stateAfter: EnvironmentModels = this.stateAfter, + result: UtExecutionResult = this.result, + coverage: Coverage? = this.coverage, + summary: List? = this.summary, + testMethodName: String? = this.testMethodName, + displayName: String? = this.displayName, + instrumentation: List = this.instrumentation, + path: MutableList = this.path, + fullPath: List = this.fullPath + ): UtExecution = UtSymbolicExecution( + stateBefore = stateBefore, + stateAfter = stateAfter, + result = result, + instrumentation = instrumentation, + path = path, + fullPath = fullPath, + coverage = coverage, + summary = summary, + testMethodName = testMethodName, + displayName = displayName + ) } /** @@ -189,18 +269,59 @@ class UtSymbolicExecution( */ class UtFailedExecution( stateBefore: EnvironmentModels, - result: UtExecutionFailure, + result: UtExecutionResult, coverage: Coverage? = null, summary: List? = null, testMethodName: String? = null, displayName: String? = null -) : UtExecution(stateBefore, MissingState, result, coverage, summary, testMethodName, displayName) +) : UtExecution(stateBefore, MissingState, result, coverage, summary, testMethodName, displayName) { + override fun copy( + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + result: UtExecutionResult, + coverage: Coverage?, + summary: List?, + testMethodName: String?, + displayName: String? + ): UtExecution { + return UtFailedExecution( + stateBefore, + result, + coverage, + summary, + testMethodName, + displayName + ) + } +} +/** + * @property executableToCall executable that is called in the test body and whose result is used in asserts as `actual`. + * + * Most often [executableToCall] is just method under test, but it may be changed to different executable if more + * appropriate way of calling specific method is found (for example, Spring controller methods are called via `MockMvc`). + * + * `null` value of [executableToCall] indicates that method under test should be called in the test body. + */ open class EnvironmentModels( val thisInstance: UtModel?, val parameters: List, - val statics: Map + val statics: Map, + val executableToCall: ExecutableId?, ) { + @Deprecated("Now `executableToCall` also need to be passed to `EnvironmentModels` constructor " + + "(see more details in `EnvironmentModels` class documentation)", level = DeprecationLevel.ERROR) + constructor( + thisInstance: UtModel?, + parameters: List, + statics: Map, + ) : this( + thisInstance = thisInstance, + parameters = parameters, + statics = statics, + executableToCall = null, + ) + override fun toString() = buildString { append("this=$thisInstance") appendOptional("parameters", parameters) @@ -210,6 +331,13 @@ open class EnvironmentModels( operator fun component1(): UtModel? = thisInstance operator fun component2(): List = parameters operator fun component3(): Map = statics + + fun copy( + thisInstance: UtModel? = this.thisInstance, + parameters: List = this.parameters, + statics: Map = this.statics, + executableToCall: ExecutableId? = this.executableToCall, + ) = EnvironmentModels(thisInstance, parameters, statics, executableToCall) } /** @@ -218,11 +346,12 @@ open class EnvironmentModels( object MissingState : EnvironmentModels( thisInstance = null, parameters = emptyList(), - statics = emptyMap() + statics = emptyMap(), + executableToCall = null, ) /** - * Error happened in traverse. + * Error happened during test cases generation. */ data class UtError( val description: String, @@ -234,7 +363,7 @@ data class UtError( * * UtNullModel represents nulls, other models represent not-nullable entities. */ -sealed class UtModel( +open class UtModel( open val classId: ClassId ) @@ -275,6 +404,15 @@ fun UtModel.hasDefaultValue() = */ fun UtModel.isMockModel() = this is UtCompositeModel && isMock +/** + * Checks that this [UtModel] must be constructed with @Spy annotation in generated tests. + * Used only for construct variables with @Spy annotation. + */ +fun UtModel.canBeSpied(): Boolean = + this is UtAssembleModel && spiedTypes.any { type -> type.isAssignableFrom(this.classId.jClass)} + +val spiedTypes = setOf(Collection::class.java, Map::class.java) + /** * Get model id (symbolic null value for UtNullModel) * or null if model has no id (e.g., a primitive model) or the id is null. @@ -337,7 +475,7 @@ object UtVoidModel : UtModel(voidClassId) * Model for enum constant */ data class UtEnumConstantModel( - override val id: Int?, + override val id: Int, override val classId: ClassId, val value: Enum<*> ) : UtReferenceModel(id, classId) { @@ -349,9 +487,9 @@ data class UtEnumConstantModel( * Model for class reference */ data class UtClassRefModel( - override val id: Int?, + override val id: Int, override val classId: ClassId, - val value: Class<*> + val value: ClassId ) : UtReferenceModel(id, classId) { // Model id is included for debugging purposes override fun toString(): String = "$value@$id" @@ -364,6 +502,12 @@ data class UtClassRefModel( * - isMock flag * - calculated field values (models) * - mocks for methods with return values + * - [canHaveRedundantOrMissingMocks] flag, which is set to `true` for mocks + * created by: + * - fuzzer which doesn't know which methods will actually be called + * - engine which also doesn't know which methods will actually be + * called during concrete execution that may be only **partially** + * backed up by the symbolic analysis * * [fields] contains non-static fields */ @@ -373,6 +517,7 @@ data class UtCompositeModel( val isMock: Boolean, val fields: MutableMap = mutableMapOf(), val mocks: MutableMap> = mutableMapOf(), + val canHaveRedundantOrMissingMocks: Boolean = true, ) : UtReferenceModel(id, classId) { //TODO: SAT-891 - rewrite toString() method override fun toString() = withToStringThreadLocalReentrancyGuard { @@ -412,10 +557,7 @@ data class UtCompositeModel( other as UtCompositeModel - if (id != other.id) return false - if (classId != other.classId) return false - - return true + return id == other.id } override fun hashCode(): Int { @@ -453,11 +595,24 @@ data class UtArrayModel( return true } - override fun hashCode(): Int { - return id - } + override fun hashCode(): Int = id } +/** + * Wrapper of [origin] model, that can be handled in a different + * way in some situations (e.g. during value construction). + */ +sealed class UtModelWithCompositeOrigin( + id: Int?, + classId: ClassId, + modelName: String = id.toString(), + open val origin: UtCompositeModel?, +) : UtReferenceModel( + id = id, + classId = classId, + modelName = modelName +) + /** * Model for complex objects with assemble instructions. * @@ -470,10 +625,10 @@ data class UtAssembleModel private constructor( override val id: Int?, override val classId: ClassId, override val modelName: String, - val instantiationCall: UtExecutableCallModel, + val instantiationCall: UtStatementCallModel, val modificationsChain: List, - val origin: UtCompositeModel? -) : UtReferenceModel(id, classId, modelName) { + override val origin: UtCompositeModel? +) : UtModelWithCompositeOrigin(id, classId, modelName, origin) { /** * Creates a new [UtAssembleModel]. @@ -495,7 +650,7 @@ data class UtAssembleModel private constructor( id: Int?, classId: ClassId, modelName: String, - instantiationCall: UtExecutableCallModel, + instantiationCall: UtStatementCallModel, origin: UtCompositeModel? = null, modificationsChainProvider: UtAssembleModel.() -> List = { emptyList() } ) : this(id, classId, modelName, instantiationCall, mutableListOf(), origin) { @@ -522,9 +677,7 @@ data class UtAssembleModel private constructor( return id == other.id } - override fun hashCode(): Int { - return id ?: 0 - } + override fun hashCode(): Int = id ?: 0 } /** @@ -552,21 +705,133 @@ data class UtAssembleModel private constructor( */ // TODO: what about support for Kotlin lambdas and function types? See https://github.com/UnitTestBot/UTBotJava/issues/852 class UtLambdaModel( - override val id: Int?, + override val id: Int, val samType: ClassId, val declaringClass: ClassId, val lambdaName: String, val capturedValues: MutableList = mutableListOf(), ) : UtReferenceModel(id, samType) { + val isFake: Boolean = lambdaName == fakeName + val lambdaMethodId: MethodId - get() = declaringClass.jClass - .declaredMethods - .singleOrNull { it.name == lambdaName } - ?.executableId // synthetic lambda methods should not have overloads, so we always expect there to be only one method with the given name - ?: error("More than one method with name $lambdaName found in class: ${declaringClass.canonicalName}") + get() { + if (isFake) { + val targetMethod = samType.jClass.declaredMethods.single { it.isAbstract } + return object : MethodId( + declaringClass, + fakeName, + targetMethod.returnType.id, + targetMethod.parameterTypes.map { it.id } + ) { + override val modifiers: Int = ModifierFactory.invoke { + public = true + static = true + final = true + } + } + } + return declaringClass.jClass + .declaredMethods + .singleOrNull { it.name == lambdaName } + ?.executableId // synthetic lambda methods should not have overloads, so we always expect there to be only one method with the given name + ?: error("More than one method with name $lambdaName found in class: ${declaringClass.canonicalName}") + } override fun toString(): String = "Anonymous function $lambdaName implementing functional interface $declaringClass" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UtLambdaModel + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int = id + + companion object { + private const val fakeName = "" + + /** + * Create a non-existent lambda with fake method. + * + * That's temporary solution for building lambdas from concrete values. + */ + fun createFake(id: Int, samType: ClassId, declaringClass: ClassId) = + UtLambdaModel(id, samType, declaringClass, fakeName) + } +} + +/** + * Common parent of all framework-specific models (e.g. Spring-specific models) + */ +abstract class UtCustomModel( + id: Int?, + classId: ClassId, + modelName: String = id.toString(), + override val origin: UtCompositeModel? = null, +) : UtModelWithCompositeOrigin(id, classId, modelName, origin) { + abstract fun shallowMap(mapper: UtModelMapper): UtCustomModel +} + +object UtSpringContextModel : UtCustomModel( + id = null, + classId = SpringModelUtils.applicationContextClassId, + modelName = "applicationContext" +) { + override fun shallowMap(mapper: UtModelMapper) = this + + // NOTE that overriding equals is required just because without it + // we will lose equality for objects after deserialization + override fun equals(other: Any?): Boolean = other is UtSpringContextModel + + override fun hashCode(): Int = 0 +} + +class UtSpringEntityManagerModel : UtCustomModel( + id = null, + classId = SpringModelUtils.entityManagerClassIds.first(), + modelName = "entityManager" +) { + override fun shallowMap(mapper: UtModelMapper) = this + + // NOTE that overriding equals is required just because without it + // we will lose equality for objects after deserialization + override fun equals(other: Any?): Boolean = other is UtSpringEntityManagerModel + + override fun hashCode(): Int = 0 +} + +data class SpringRepositoryId( + val repositoryBeanName: String, + val repositoryClassId: ClassId, + val entityClassId: ClassId, +) + +data class UtSpringMockMvcResultActionsModel( + override val id: Int?, + override val origin: UtCompositeModel?, + val status: Int, + val errorMessage: String?, + val contentAsString: String, + val viewName: String?, + // model for mvcResult.modelAndView?.model + val model: UtModel? + // TODO add headers and other data +) : UtCustomModel( + origin = origin, + classId = SpringModelUtils.resultActionsClassId, + id = id, + modelName = "mockMvcResultActions@$id" +) { + override fun shallowMap(mapper: UtModelMapper) = copy( + origin = origin?.mapPreservingType(mapper), + model = model?.map(mapper) + ) } /** @@ -577,28 +842,24 @@ sealed class UtStatementModel( ) /** - * Step of assemble instruction that calls executable. - * - * Contains executable to call, call parameters and an instance model before call. - * - * @param [instance] **must be** `null` for static methods and constructors + * Step of assemble instruction that calls executable or accesses the field. */ -data class UtExecutableCallModel( +sealed class UtStatementCallModel( override val instance: UtReferenceModel?, - val executable: ExecutableId, - val params: List, -) : UtStatementModel(instance) { + open val statement: StatementId, + open val params: List, + var thrownConcreteException: ClassId? = null +): UtStatementModel(instance) { override fun toString() = withToStringThreadLocalReentrancyGuard { buildString { - val executableName = when (executable) { - is ConstructorId -> executable.classId.name - is MethodId -> executable.name + val executableName = when (statement) { + is ConstructorId -> statement.classId.name + is DirectFieldAccessId -> statement.name + is MethodId -> statement.name } - if (instance != null) { - append("${instance.modelName}.") - } + instance?.let { append("${it.modelName}.") } append(executableName) val paramsRepresentation = params.map { param -> @@ -612,6 +873,32 @@ data class UtExecutableCallModel( } } +/** + * Step of assemble instruction that calls executable. + * + * Contains executable to call, call parameters and an instance model before call. + * @param [instance] **must be** `null` for static methods and constructors + */ +data class UtExecutableCallModel( + override val instance: UtReferenceModel?, + val executable: ExecutableId, + override val params: List, +) : UtStatementCallModel(instance, executable, params) { + override fun toString(): String = super.toString() +} + +/** + * Step of assemble instruction that directly accesses a field. + * + * Contains parameter value to set and an instance model before call. + */ +data class UtDirectGetFieldModel( + override val instance: UtReferenceModel, + val fieldAccess: DirectFieldAccessId, +) : UtStatementCallModel(instance, fieldAccess, emptyList()) { + override fun toString(): String = super.toString() +} + /** * Step of assemble instruction that sets public field with direct setter. * @@ -719,6 +1006,8 @@ open class ClassId @JvmOverloads constructor( // Treat simple class ids as non-nullable open val isNullable: Boolean = false ) { + open val modifiers: Int + get() = jClass.modifiers open val canonicalName: String get() = jClass.canonicalName ?: error("ClassId $name does not have canonical name") @@ -740,7 +1029,7 @@ open class ClassId @JvmOverloads constructor( // so we create a specific name for them isAnonymous -> "Anonymous${supertypeOfAnonymousClass.prettifiedName}" // in other cases where canonical name is still null, we use ClassId.name instead - else -> jClass.canonicalName ?: name // Explicit jClass reference to get null instead of exception + else -> runCatching { canonicalName }.getOrElse { name } } return baseName .substringAfterLast(".") @@ -753,27 +1042,6 @@ open class ClassId @JvmOverloads constructor( open val isInDefaultPackage: Boolean get() = packageName.isEmpty() - open val isPublic: Boolean - get() = Modifier.isPublic(jClass.modifiers) - - open val isProtected: Boolean - get() = Modifier.isProtected(jClass.modifiers) - - open val isPrivate: Boolean - get() = Modifier.isPrivate(jClass.modifiers) - - val isPackagePrivate: Boolean - get() = !(isPublic || isProtected || isPrivate) - - open val isFinal: Boolean - get() = Modifier.isFinal(jClass.modifiers) - - open val isStatic: Boolean - get() = Modifier.isStatic(jClass.modifiers) - - open val isAbstract: Boolean - get() = Modifier.isAbstract(jClass.modifiers) - open val isAnonymous: Boolean get() = jClass.isAnonymousClass @@ -789,11 +1057,15 @@ open class ClassId @JvmOverloads constructor( open val isSynthetic: Boolean get() = jClass.isSynthetic + open val isKotlinObject: Boolean + get() = kClass.objectInstance != null + /** * Collects all declared methods (including private and protected) from class and all its superclasses to sequence */ open val allMethods: Sequence get() = generateSequence(jClass) { it.superclass } + .flatMap { it.interfaces.toMutableList() + it } .mapNotNull { it.declaredMethods } .flatMap { it.toList() } .map { it.executableId } @@ -825,11 +1097,11 @@ open class ClassId @JvmOverloads constructor( * It is needed because [simpleName] for inner classes does not * take into account enclosing classes' names. */ - open val simpleNameWithEnclosings: String + open val simpleNameWithEnclosingClasses: String get() { val clazz = jClass return if (clazz.isMemberClass) { - "${clazz.enclosingClass.id.simpleNameWithEnclosings}.$simpleName" + "${clazz.enclosingClass.id.simpleNameWithEnclosingClasses}.$simpleName" } else { simpleName } @@ -864,24 +1136,26 @@ open class ClassId @JvmOverloads constructor( * (it is important because name for nested classes contains $ as a delimiter between nested and outer classes) */ class BuiltinClassId( - name: String, elementClassId: ClassId? = null, override val canonicalName: String, override val simpleName: String, + // set name manually only if it differs from canonical (e.g. for nested classes) + name: String = canonicalName, // by default, we assume that the class is not a member class - override val simpleNameWithEnclosings: String = simpleName, + override val simpleNameWithEnclosingClasses: String = simpleName, override val isNullable: Boolean = false, - override val isPublic: Boolean = true, - override val isProtected: Boolean = false, - override val isPrivate: Boolean = false, - override val isFinal: Boolean = false, - override val isStatic: Boolean = false, - override val isAbstract: Boolean = false, + isPublic: Boolean = true, + isProtected: Boolean = false, + isPrivate: Boolean = false, + isFinal: Boolean = false, + isStatic: Boolean = false, + isAbstract: Boolean = false, override val isAnonymous: Boolean = false, override val isLocal: Boolean = false, override val isInner: Boolean = false, override val isNested: Boolean = false, override val isSynthetic: Boolean = false, + override val isKotlinObject: Boolean = false, override val typeParameters: TypeParameters = TypeParameters(), override val allMethods: Sequence = emptySequence(), override val allConstructors: Sequence = emptySequence(), @@ -895,7 +1169,20 @@ class BuiltinClassId( -1, 0 -> "" else -> canonicalName.substring(0, index) }, -) : ClassId(name = name, isNullable = isNullable, elementClassId = elementClassId) { +) : ClassId( + name = name, + elementClassId = elementClassId, + isNullable = isNullable, +) { + override val modifiers: Int = ModifierFactory { + public = isPublic + protected = isProtected + private = isPrivate + final = isFinal + static = isStatic + abstract = isAbstract + } + init { BUILTIN_CLASSES_BY_NAMES[name] = this } @@ -914,9 +1201,17 @@ class BuiltinClassId( } } -enum class FieldIdStrategyValues { - Reflection, - Soot +class CgClassId( + name: String, + elementClassId: ClassId? = null, + override val typeParameters: TypeParameters = TypeParameters(), + override val isNullable: Boolean = true, +) : ClassId(name, elementClassId) { + constructor( + classId: ClassId, + typeParameters: TypeParameters = TypeParameters(), + isNullable: Boolean = true, + ) : this(classId.name, classId.elementClassId, typeParameters, isNullable) } /** @@ -925,38 +1220,14 @@ enum class FieldIdStrategyValues { * Created to avoid usage String objects as a key. */ open class FieldId(val declaringClass: ClassId, val name: String) { - - object Strategy { - var value: FieldIdStrategyValues = FieldIdStrategyValues.Soot - } - - private val strategy - get() = if (Strategy.value == FieldIdStrategyValues.Soot) - FieldIdSootStrategy(declaringClass, this) else FieldIdReflectionStrategy(this) - - open val isPublic: Boolean - get() = strategy.isPublic - - open val isProtected: Boolean - get() = strategy.isProtected - - open val isPrivate: Boolean - get() = strategy.isPrivate - - open val isPackagePrivate: Boolean - get() = strategy.isPackagePrivate - - open val isFinal: Boolean - get() = strategy.isFinal - - open val isStatic: Boolean - get() = strategy.isStatic + open val modifiers: Int + get() = jField.modifiers open val isSynthetic: Boolean - get() = strategy.isSynthetic + get() = jField.isSynthetic open val type: ClassId - get() = strategy.type + get() = jField.type.id override fun equals(other: Any?): Boolean { if (this === other) return true @@ -979,16 +1250,6 @@ open class FieldId(val declaringClass: ClassId, val name: String) { override fun toString() = safeJField?.toString() ?: "[unresolved] $declaringClass.$name" } -inline fun withReflection(block: () -> T): T { - val prevStrategy = FieldId.Strategy.value - try { - FieldId.Strategy.value = FieldIdStrategyValues.Reflection - return block() - } finally { - FieldId.Strategy.value = prevStrategy - } -} - /** * The same as [FieldId], except it represents the fields * of classes that may not be present on the classpath. @@ -1002,11 +1263,18 @@ class BuiltinFieldId( name: String, override val type: ClassId, // by default we assume that the builtin field is public and non-final - override val isPublic: Boolean = true, - override val isPrivate: Boolean = false, - override val isFinal: Boolean = false, + isPublic: Boolean = true, + isPrivate: Boolean = false, + isFinal: Boolean = false, override val isSynthetic: Boolean = false, -) : FieldId(declaringClass, name) +) : FieldId(declaringClass, name) { + override val modifiers = ModifierFactory { + public = isPublic + private = isPrivate + final = isFinal + } + +} sealed class StatementId { abstract val classId: ClassId @@ -1029,6 +1297,14 @@ sealed class ExecutableId : StatementId() { abstract val returnType: ClassId abstract val parameters: List + /** + * Normally during concrete execution every executable is executed in a + * [sandbox](https://github.com/UnitTestBot/UTBotJava/blob/main/docs/Sandboxing.md). + * + * However, if this flag is set to `true`, then `this` particular executable is executed without a sandbox. + */ + abstract val bypassesSandbox: Boolean + abstract val modifiers: Int val signature: String @@ -1038,16 +1314,15 @@ sealed class ExecutableId : StatementId() { return "$name($args)$retType" } + fun describesSameMethodAs(other: ExecutableId): Boolean { + return classId == other.classId && signature == other.signature + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - other as ExecutableId - - if (classId != other.classId) return false - if (signature != other.signature) return false - - return true + return describesSameMethodAs(other as ExecutableId) } override fun hashCode(): Int { @@ -1070,6 +1345,7 @@ open class MethodId( override val name: String, override val returnType: ClassId, override val parameters: List, + override val bypassesSandbox: Boolean = false, ) : ExecutableId() { override val modifiers: Int get() = method.modifiers @@ -1077,7 +1353,8 @@ open class MethodId( open class ConstructorId( override val classId: ClassId, - override val parameters: List + override val parameters: List, + override val bypassesSandbox: Boolean = false, ) : ExecutableId() { final override val name: String = "" final override val returnType: ClassId = voidClassId @@ -1092,39 +1369,218 @@ class BuiltinMethodId( name: String, returnType: ClassId, parameters: List, + bypassesSandbox: Boolean = false, // by default we assume that the builtin method is non-static and public isStatic: Boolean = false, isPublic: Boolean = true, isProtected: Boolean = false, isPrivate: Boolean = false -) : MethodId(classId, name, returnType, parameters) { - override val modifiers: Int = - (if (isStatic) Modifier.STATIC else 0) or - (if (isPublic) Modifier.PUBLIC else 0) or - (if (isProtected) Modifier.PROTECTED else 0) or - (if (isPrivate) Modifier.PRIVATE else 0) +) : MethodId(classId, name, returnType, parameters, bypassesSandbox) { + override val modifiers: Int = ModifierFactory { + static = isStatic + public = isPublic + private = isPrivate + protected = isProtected + } } class BuiltinConstructorId( classId: ClassId, parameters: List, + bypassesSandbox: Boolean = false, // by default, we assume that the builtin constructor is public isPublic: Boolean = true, isProtected: Boolean = false, isPrivate: Boolean = false -) : ConstructorId(classId, parameters) { - override val modifiers: Int = - (if (isPublic) Modifier.PUBLIC else 0) or - (if (isProtected) Modifier.PROTECTED else 0) or - (if (isPrivate) Modifier.PRIVATE else 0) +) : ConstructorId(classId, parameters, bypassesSandbox) { + override val modifiers: Int = ModifierFactory { + public = isPublic + private = isPrivate + protected = isProtected + } +} + +open class TypeParameters(val parameters: List = emptyList()) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TypeParameters + + if (parameters != other.parameters) return false + + return true + } + + override fun hashCode(): Int { + return parameters.hashCode() + } +} + +object WildcardTypeParameter : TypeParameters(emptyList()) + +/** + * Describes the way to replace abstract types with concrete implementors. + */ +enum class TypeReplacementMode { + /** + * Any possible implementor (that is preferred by solver) may be used. + */ + AnyImplementor, + + /** + * There is a known implementor to be used. + * For example, it is obtained from bean definitions in Spring application. + */ + KnownImplementor, + + /** + * Using implementors is not allowed. + * If mocking is allowed, mock of this type will be used. + * Otherwise, branch will be pruned as unsatisfiable. + */ + NoImplementors, } -open class TypeParameters(val parameters: List = emptyList()) +sealed class SpringConfiguration(val fullDisplayName: String) { + abstract class JavaBasedConfiguration(open val configBinaryName: String) : SpringConfiguration(configBinaryName) + + class JavaConfiguration(override val configBinaryName: String) : JavaBasedConfiguration(configBinaryName) + + class SpringBootConfiguration( + override val configBinaryName: String, + val isDefinitelyUnique: Boolean + ) : JavaBasedConfiguration(configBinaryName) + + class XMLConfiguration(val absolutePath: String) : SpringConfiguration(absolutePath) +} + +sealed interface SpringSettings { + companion object AbsentSpringSettings : SpringSettings { + // NOTE that overriding equals is required just because without it + // we will lose equality for objects after deserialization + override fun equals(other: Any?): Boolean = other is AbsentSpringSettings + + override fun hashCode(): Int = 0 + } + + data class PresentSpringSettings( + val configuration: SpringConfiguration, + val profiles: List + ) : SpringSettings +} -class WildcardTypeParameter : TypeParameters(emptyList()) +/** + * Result of loading concrete execution context (e.g. Spring application context). + * + * [contextLoaded] can be `true` while [exceptions] is not empty. For example, we may fail + * to load context with most specific SpringApi available (e.g. SpringBoot), + * but successfully fall back to less specific SpringApi (e.g. PureSpring). + */ +class ConcreteContextLoadingResult( + val contextLoaded: Boolean, + val exceptions: List +) { + val utErrors: List get() = + exceptions.map { UtError(it.message ?: "Concrete context loading failed", it) } + + fun andThen(onSuccess: () -> ConcreteContextLoadingResult) = + if (contextLoaded) { + val otherResult = onSuccess() + ConcreteContextLoadingResult( + contextLoaded = otherResult.contextLoaded, + exceptions = exceptions + otherResult.exceptions + ) + } else this + + companion object { + fun successWithoutExceptions() = ConcreteContextLoadingResult( + contextLoaded = true, + exceptions = emptyList() + ) + } +} + +enum class SpringTestType( + override val id: String, + override val displayName: String, + override val description: String, +) : CodeGenerationSettingItem { + UNIT_TEST( + "Unit tests", + "Unit tests", + "Generate unit tests mocking other classes" + ), + INTEGRATION_TEST( + "Integration tests", + "Integration tests", + "Generate integration tests autowiring real instance" + ); + + override fun toString() = id + + companion object : CodeGenerationSettingBox { + override val defaultItem = UNIT_TEST + override val allItems: List = values().toList() + } +} + +class SpringProfileNames( + override val defaultItem: String, +) : CodeGenerationSettingTextField { + + override fun toString() = defaultItem + + companion object : CodeGenerationSettingTextField { + override val defaultItem = "default" + } +} + +const val NO_SPRING_CONFIGURATION_OPTION = "No configuration" + +class SpringConfig( + override val defaultItem: String, +) : CodeGenerationSettingTextField { + + override fun toString() = defaultItem + + companion object : CodeGenerationSettingTextField { + override val defaultItem = NO_SPRING_CONFIGURATION_OPTION + } +} + +/** + * Describes information about beans obtained from Spring analysis process. + * + * Contains the name of the bean, its type (class or interface) and optional additional data. + * + * @param beanTypeName a name in a form obtained by [java.lang.Class.getName] method. + */ +data class BeanDefinitionData( + val beanName: String, + val beanTypeName: String, + val additionalData: BeanAdditionalData?, +) + +/** + * Describes some additional information about beans obtained from Spring analysis process. + * + * Sometimes the actual type of the bean can not be obtained from bean definition. + * Then we try to recover it by method and class defining bean (e.g. using Psi elements). + * + * @param configClassName a name in a form obtained by [java.lang.Class.getName] method. + */ +data class BeanAdditionalData( + val factoryMethodName: String, + val parameterTypes: List, + val configClassName: String, +) + +val RefType.isAbstractType + get() = this.sootClass.isAbstract || this.sootClass.isInterface interface CodeGenerationSettingItem { - val id : String + val id: String val displayName: String val description: String } @@ -1136,8 +1592,12 @@ interface CodeGenerationSettingBox { fun labels(): Array = allItems.map { it.displayName }.toTypedArray() } +interface CodeGenerationSettingTextField { + val defaultItem: String +} + enum class MockStrategyApi( - override val id : String, + override val id: String, override val displayName: String, override val description: String ) : CodeGenerationSettingItem { @@ -1160,11 +1620,18 @@ enum class MockStrategyApi( companion object : CodeGenerationSettingBox { override val defaultItem = OTHER_PACKAGES override val allItems: List = values().toList() + + // Mock strategy gains more meaning in Spring Projects. + // We use OTHER_CLASSES strategy as default one in `No configuration` mode + // and as unique acceptable in other modes (combined with type replacement). + val springDefaultItem = OTHER_CLASSES + // We use NO_MOCKS strategy in integration tests because they are based on fuzzer that is not compatible with mocks + val springIntegrationTestItem = NO_MOCKS } } enum class TreatOverflowAsError( - override val id : String, + override val id: String, override val displayName: String, override val description: String, ) : CodeGenerationSettingItem { @@ -1282,10 +1749,14 @@ enum class CodegenLanguage( JAVA -> listOf( "-d", buildDirectory, "-cp", classPath, - "-XDignore.symbol.file" // to let javac use classes from rt.jar - ).plus(sourcesFiles) - - KOTLIN -> listOf("-d", buildDirectory, "-jvm-target", jvmTarget, "-cp", classPath).plus(sourcesFiles) + "-XDignore.symbol.file", // to let javac use classes from rt.jar + ).plus(OpenModulesContainer.javaVersionSpecificArguments.toMutableList().apply { + if (last().contains("illegal")) + removeLast() + }).plus(sourcesFiles) + + // TODO: -Xskip-prerelease-check is needed to handle #1262, check if this is good enough solution + KOTLIN -> listOf("-d", buildDirectory, "-jvm-target", jvmTarget, "-cp", classPath, "-Xskip-prerelease-check").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") @@ -1301,6 +1772,8 @@ enum class CodegenLanguage( override val allItems: List = values().toList() } } +//TODO #1279 +fun CodegenLanguage?.isSummarizationCompatible() = this == CodegenLanguage.JAVA // https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#commandlineargfile fun isolateCommandLineArgumentsToArgumentFile(arguments: List): String { @@ -1399,3 +1872,12 @@ class DocRegularStmt(val stmt: String) : DocStatement() { override fun hashCode(): Int = stmt.hashCode() } + +class DocRegularLineStmt(val stmt: String) : DocStatement() { + override fun toString(): String = stmt + + override fun equals(other: Any?): Boolean = + if (other is DocRegularLineStmt) this.hashCode() == other.hashCode() else false + + override fun hashCode(): Int = stmt.hashCode() +} diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/ArtificialErrors.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/ArtificialErrors.kt new file mode 100644 index 0000000000..f1578afd76 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/ArtificialErrors.kt @@ -0,0 +1,31 @@ +package org.utbot.framework.plugin.api + +/** + * Represents an error that may be detected or not + * during analysis in accordance with custom settings. + * + * Usually execution may be continued somehow after such error, + * but the result may be different from basic expectations. + */ +sealed class ArtificialError(message: String): Error(message) + +/** + * Represents overflow detection errors in symbolic engine, + * if a mode to detect them is turned on. + * + * See [TraversalContext.intOverflowCheck] for more details. + */ +class OverflowDetectionError(message: String): ArtificialError(message) + +/** + * An artificial error that could be implicitly thrown by the symbolic engine during taint sink processing. + */ +class TaintAnalysisError( + /** Sink method name: "${classId.simpleName}.${methodId.name}". */ + val sinkName: String, + /** Some information about a tainted var, for example, its type. */ + val taintedVar: String, + /** Name of the taint mark. */ + val taintMark: String, + message: String = "'$taintedVar' marked '$taintMark' was passed into '$sinkName' method" +) : ArtificialError(message) diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt index 6ab14e5aac..3c3f31cb08 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt @@ -3,19 +3,21 @@ package org.utbot.framework.plugin.api /** * Represents a covered bytecode instruction. * - * @param className a fqn of the class. + * @param internalName the fqn in internal form, i.e. com/rest/order/services/OrderService$InnerClass. * @param methodSignature the signature of the method. * @param lineNumber a number of the line in the source file. * @param id a unique identifier among all instructions in all classes. * * @see Test minimization */ -data class Instruction( - val className: String, +open class Instruction( + val internalName: String, val methodSignature: String, val lineNumber: Int, val id: Long -) +) { + val className: String get() = internalName.replace('/', '.') +} /** * Represents coverage information. Some other @@ -24,9 +26,11 @@ data class Instruction( * * @param coveredInstructions a list of the covered instructions in the order they are visited. * @param instructionsCount a number of all instructions in the current class. + * @param missedInstructions a list of the missed instructions. * */ data class Coverage( val coveredInstructions: List = emptyList(), - val instructionsCount: Long? = null + val instructionsCount: Long? = null, + val missedInstructions: List = emptyList(), ) \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/UtExecutionResult.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/UtExecutionResult.kt index b242ab39ec..3c8c9612df 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/UtExecutionResult.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/UtExecutionResult.kt @@ -1,98 +1,110 @@ -package org.utbot.framework.plugin.api - -import java.io.File -import java.util.LinkedList - -sealed class UtExecutionResult - -data class UtExecutionSuccess(val model: UtModel) : UtExecutionResult() { - override fun toString() = "$model" -} - -sealed class UtExecutionFailure : UtExecutionResult() { - abstract val exception: Throwable -} - -data class UtOverflowFailure( - override val exception: Throwable, -) : UtExecutionFailure() - -data class UtSandboxFailure( - override val exception: Throwable -) : UtExecutionFailure() - -/** - * unexpectedFail (when exceptions such as NPE, IOBE, etc. appear, but not thrown by a user, applies both for function under test and nested calls ) - * expectedCheckedThrow (when function under test or nested call explicitly says that checked exception could be thrown and throws it) - * expectedUncheckedThrow (when there is a throw statement for unchecked exception inside of function under test) - * unexpectedUncheckedThrow (in case when there is unchecked exception thrown from nested call) - */ -data class UtExplicitlyThrownException( - override val exception: Throwable, - val fromNestedMethod: Boolean -) : UtExecutionFailure() - -data class UtImplicitlyThrownException( - override val exception: Throwable, - val fromNestedMethod: Boolean -) : UtExecutionFailure() - -class TimeoutException(s: String) : Exception(s) - -data class UtTimeoutException(override val exception: TimeoutException) : UtExecutionFailure() - -/** - * Indicates failure in concrete execution. - * For now it is explicitly throwing by ConcreteExecutor in case child process death. - */ -class ConcreteExecutionFailureException(cause: Throwable, errorFile: File, val processStdout: List) : - Exception( - buildString { - appendLine() - appendLine("----------------------------------------") - appendLine("The child process is dead") - appendLine("Cause:\n${cause.message}") - appendLine("Last 1000 lines of the error log ${errorFile.absolutePath}:") - appendLine("----------------------------------------") - errorFile.useLines { lines -> - val lastLines = LinkedList() - for (line in lines) { - lastLines.add(line) - if (lastLines.size > 1000) { - lastLines.removeFirst() - } - } - lastLines.forEach { appendLine(it) } - } - appendLine("----------------------------------------") - }, - cause - ) - -data class UtConcreteExecutionFailure(override val exception: ConcreteExecutionFailureException) : UtExecutionFailure() - -val UtExecutionResult.isSuccess: Boolean - get() = this is UtExecutionSuccess - -val UtExecutionResult.isFailure: Boolean - get() = this is UtExecutionFailure - -inline fun UtExecutionResult.onSuccess(action: (model: UtModel) -> Unit): UtExecutionResult { - if (this is UtExecutionSuccess) action(model) - return this -} - -inline fun UtExecutionResult.onFailure(action: (exception: Throwable) -> Unit): UtExecutionResult { - if (this is UtExecutionFailure) action(exception) - return this -} - -fun UtExecutionResult.getOrThrow(): UtModel = when (this) { - is UtExecutionSuccess -> model - is UtExecutionFailure -> throw exception -} - -fun UtExecutionResult.exceptionOrNull(): Throwable? = when (this) { - is UtExecutionFailure -> exception - is UtExecutionSuccess -> null -} +package org.utbot.framework.plugin.api + +import org.utbot.framework.plugin.api.visible.UtStreamConsumingException + +sealed class UtExecutionResult + +data class UtExecutionSuccess(val model: UtModel) : UtExecutionResult() { + override fun toString() = "$model" +} + +sealed class UtExecutionFailure : UtExecutionResult() { + abstract val exception: Throwable + + /** + * Represents the most inner exception in the failure. + * Often equals to [exception], but is wrapped exception in [UtStreamConsumingException]. + */ + open val rootCauseException: Throwable + get() = exception +} + +data class UtOverflowFailure( + override val exception: Throwable, +) : UtExecutionFailure() + +data class UtTaintAnalysisFailure( + override val exception: Throwable +) : UtExecutionFailure() + +data class UtSandboxFailure( + override val exception: Throwable +) : UtExecutionFailure() + +data class UtStreamConsumingFailure( + override val exception: UtStreamConsumingException, +) : UtExecutionFailure() { + override val rootCauseException: Throwable + get() = exception.innerExceptionOrAny +} + +/** + * unexpectedFail (when exceptions such as NPE, IOBE, etc. appear, but not thrown by a user, applies both for function under test and nested calls ) + * + * expectedCheckedThrow (when function under test or nested call explicitly says that checked exception could be thrown and throws it) + * + * expectedUncheckedThrow (when there is a throw statement for unchecked exception inside of function under test) + * + * unexpectedUncheckedThrow (in case when there is unchecked exception thrown from nested call) + */ +data class UtExplicitlyThrownException( + override val exception: Throwable, + val fromNestedMethod: Boolean +) : UtExecutionFailure() + +data class UtImplicitlyThrownException( + override val exception: Throwable, + val fromNestedMethod: Boolean +) : UtExecutionFailure() + +class TimeoutException(s: String) : Exception(s) + +data class UtTimeoutException(override val exception: TimeoutException) : UtExecutionFailure() + +/** + * Indicates failure in concrete execution. + * For now it is explicitly throwing by ConcreteExecutor in case instrumented process death. + */ +class InstrumentedProcessDeathException(cause: Throwable) : + Exception( + buildString { + appendLine() + appendLine("----------------------------------------") + appendLine("The instrumented process is dead") + appendLine("Cause:\n${cause.message}") + appendLine("----------------------------------------") + }, + cause + ) + +data class UtConcreteExecutionFailure(override val exception: Throwable) : UtExecutionFailure() + +/** + * Represents a failure in instrumented process + * that is not actually caused by concrete method under test call. + * + * For example, failure may have occurred during method arguments preparation + * or statics initializers processing during object instance creation. + */ +data class UtConcreteExecutionProcessedFailure(override val exception: Throwable) : UtExecutionFailure() + +val UtExecutionResult.isSuccess: Boolean + get() = this is UtExecutionSuccess + +val UtExecutionResult.isFailure: Boolean + get() = this is UtExecutionFailure + +inline fun UtExecutionResult.onSuccess(action: (model: UtModel) -> Unit): UtExecutionResult { + if (this is UtExecutionSuccess) action(model) + return this +} + +inline fun UtExecutionResult.onFailure(action: (exception: Throwable) -> Unit): UtExecutionResult { + if (this is UtExecutionFailure) action(rootCauseException) + return this +} + +fun UtExecutionResult.exceptionOrNull(): Throwable? = when (this) { + is UtExecutionFailure -> rootCauseException + is UtExecutionSuccess -> null +} \ No newline at end of file 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 deleted file mode 100644 index 21e23f0b26..0000000000 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/impl/FieldIdStrategies.kt +++ /dev/null @@ -1,90 +0,0 @@ -package org.utbot.framework.plugin.api.impl - -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.classId -import org.utbot.framework.plugin.api.util.jField -import org.utbot.framework.plugin.api.util.id -import java.lang.reflect.Modifier -import soot.Scene -import soot.SootClass - -interface FieldIdStrategy { - - val isPublic: Boolean - - val isProtected: Boolean - - val isPrivate: Boolean - - val isPackagePrivate: Boolean - get() = !(isPublic || isProtected || isPrivate) - - val isFinal: Boolean - - val isStatic: Boolean - - val isSynthetic: Boolean - - val type: ClassId -} - -class FieldIdReflectionStrategy(val fieldId: FieldId) : FieldIdStrategy { - - override val isPublic: Boolean - get() = Modifier.isPublic(fieldId.jField.modifiers) - - override val isProtected: Boolean - get() = Modifier.isProtected(fieldId.jField.modifiers) - - override val isPrivate: Boolean - get() = Modifier.isPrivate(fieldId.jField.modifiers) - - override val isFinal: Boolean - get() = Modifier.isFinal(fieldId.jField.modifiers) - - override val isStatic: Boolean - get() = Modifier.isStatic(fieldId.jField.modifiers) - - override val isSynthetic: Boolean - get() = fieldId.jField.isSynthetic - - override val type: ClassId - get() = fieldId.jField.type.id -} - -class FieldIdSootStrategy(val declaringClass: ClassId, val fieldId: FieldId) : FieldIdStrategy { - - private val declaringSootClass: SootClass - get() = Scene.v().getSootClass(declaringClass.name) - - /** - * For hidden field (fields with the same names but different types in one class) produces RuntimeException. - * [SAT-315](JIRA:315) - */ - private val modifiers: Int - get() = declaringSootClass.getFieldByName(fieldId.name).modifiers - - - override val isPublic: Boolean - get() = soot.Modifier.isPublic(modifiers) - - override val isProtected: Boolean - get() = soot.Modifier.isProtected(modifiers) - - override val isPrivate: Boolean - get() = soot.Modifier.isPrivate(modifiers) - - override val isFinal: Boolean - get() = soot.Modifier.isFinal(modifiers) - - override val isStatic: Boolean - get() = soot.Modifier.isStatic(modifiers) - - override val isSynthetic: Boolean - get() = soot.Modifier.isSynthetic(modifiers) - - override val type: ClassId - get() = declaringSootClass.getFieldByName(fieldId.name).type.classId - -} diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelDeepMapper.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelDeepMapper.kt new file mode 100644 index 0000000000..1d207f118d --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelDeepMapper.kt @@ -0,0 +1,124 @@ +package org.utbot.framework.plugin.api.mapper + +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtClassRefModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtCustomModel +import org.utbot.framework.plugin.api.UtEnumConstantModel +import org.utbot.framework.plugin.api.UtLambdaModel +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.UtReferenceModel +import org.utbot.framework.plugin.api.UtVoidModel +import java.util.IdentityHashMap + +/** + * Performs deep mapping of [UtModel]s. + * + * NOTE: + * - [shallowMapper] is invoked on models **before** mapping their sub models. + * - [shallowMapper] is responsible for caching own results (it may be called repeatedly on same models). + */ +class UtModelDeepMapper private constructor( + private val shallowMapper: UtModelMapper +) : UtModelMapper { + constructor(shallowMapper: (UtModel) -> UtModel) : this(UtModelSafeCastingCachingShallowMapper(shallowMapper)) + + /** + * Keys are models that have been shallowly mapped by [shallowMapper]. + * Values are models that have been deeply mapped by this [UtModelDeepMapper]. + * Models are only associated with models of the same type (i.e. the cache type is actually `MutableMap`) + */ + private val cache = IdentityHashMap() + + private val allInputtedModels get() = cache.keys + private val allOutputtedModels get() = cache.values + + override fun map(model: T, clazz: Class): T = + clazz.cast(mapNestedModels(shallowMapper.map(model, clazz))) + + /** + * Maps models contained inside [model], but not the [model] itself. + */ + private fun mapNestedModels(model: UtModel): UtModel = cache.getOrPut(model) { + when (model) { + is UtNullModel, + is UtPrimitiveModel, + is UtEnumConstantModel, + is UtClassRefModel, + is UtVoidModel -> model + is UtArrayModel -> mapNestedModels(model) + is UtCompositeModel -> mapNestedModels(model) + is UtLambdaModel -> mapNestedModels(model) + is UtAssembleModel -> mapNestedModels(model) + is UtCustomModel -> mapNestedModels(model) + + // PythonModel, JsUtModel may be here + else -> throw UnsupportedOperationException("UtModel $this cannot be mapped") + } + } + + private fun mapNestedModels(model: UtArrayModel): UtReferenceModel { + val mappedModel = UtArrayModel( + id = model.id, + classId = model.classId, + length = model.length, + constModel = model.constModel, + stores = model.stores, + ) + cache[model] = mappedModel + + mappedModel.constModel = model.constModel.map(this) + mappedModel.stores.putAll(model.stores.mapModelValues(this)) + + return mappedModel + } + + private fun mapNestedModels(model: UtCompositeModel): UtCompositeModel { + val mappedModel = UtCompositeModel( + id = model.id, + classId = model.classId, + isMock = model.isMock, + ) + cache[model] = mappedModel + + mappedModel.fields.putAll(model.fields.mapModelValues(this)) + mappedModel.mocks.putAll(model.mocks.mapValuesTo(mutableMapOf()) { it.value.mapModels(this@UtModelDeepMapper) }) + + return mappedModel + } + + private fun mapNestedModels(model: UtLambdaModel): UtReferenceModel = UtLambdaModel( + id = model.id, + samType = model.samType, + declaringClass = model.declaringClass, + lambdaName = model.lambdaName, + capturedValues = model.capturedValues.mapModels(this@UtModelDeepMapper).toMutableList() + ) + + private fun mapNestedModels(model: UtAssembleModel): UtReferenceModel = UtAssembleModel( + id = model.id, + classId = model.classId, + modelName = model.modelName, + instantiationCall = model.instantiationCall.mapModels(this), + modificationsChainProvider = { + cache[model] = this@UtAssembleModel + model.modificationsChain.map { it.mapModels(this@UtModelDeepMapper) } + }, + origin = model.origin?.mapPreservingType(this) + ) + + private fun mapNestedModels(model: UtCustomModel): UtReferenceModel = + model.shallowMap(this) + + companion object { + /** + * Creates identity deep mapper, runs [block] on it, and returns the set of all models that + * were mapped (i.e. deeply collects all models reachable from models passed to `collector`). + */ + fun collectAllModels(block: (collector: UtModelDeepMapper) -> Unit): Set = + UtModelDeepMapper(UtModelNoopMapper).also(block).allInputtedModels + } +} diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelMapper.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelMapper.kt new file mode 100644 index 0000000000..8db21f8baf --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelMapper.kt @@ -0,0 +1,18 @@ +package org.utbot.framework.plugin.api.mapper + +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtModelWithCompositeOrigin + +interface UtModelMapper { + /** + * Performs depending on the implementation deep or shallow mapping of the [model]. + * + * In some cases (e.g. when mapping [UtModelWithCompositeOrigin.origin]) you may want to get result + * of some specific type (e.g. [UtCompositeModel]), only then you should specify specific value for [clazz]. + * + * NOTE: if you are fine with result model and [model] having different types, then you should + * use `UtModel::class.java` as a value for [clazz] or just use [UtModel.map]. + */ + fun map(model: T, clazz: Class): T +} diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelNoopMapper.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelNoopMapper.kt new file mode 100644 index 0000000000..0325b52343 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelNoopMapper.kt @@ -0,0 +1,7 @@ +package org.utbot.framework.plugin.api.mapper + +import org.utbot.framework.plugin.api.UtModel + +object UtModelNoopMapper : UtModelMapper { + override fun map(model: T, clazz: Class): T = model +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelSafeCastingCachingShallowMapper.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelSafeCastingCachingShallowMapper.kt new file mode 100644 index 0000000000..e511450550 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/UtModelSafeCastingCachingShallowMapper.kt @@ -0,0 +1,16 @@ +package org.utbot.framework.plugin.api.mapper + +import org.utbot.framework.plugin.api.UtModel +import java.util.IdentityHashMap + +class UtModelSafeCastingCachingShallowMapper( + val mapper: (UtModel) -> UtModel +) : UtModelMapper { + private val cache = IdentityHashMap() + + override fun map(model: T, clazz: Class): T { + val mapped = cache.getOrPut(model) { mapper(model) } + return if (clazz.isInstance(mapped)) clazz.cast(mapped) + else model + } +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/Utils.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/Utils.kt new file mode 100644 index 0000000000..9c8ed80a8a --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/mapper/Utils.kt @@ -0,0 +1,75 @@ +package org.utbot.framework.plugin.api.mapper + +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.UtDirectGetFieldModel +import org.utbot.framework.plugin.api.UtDirectSetFieldModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.UtExecutionResult +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtInstrumentation +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation +import org.utbot.framework.plugin.api.UtReferenceModel +import org.utbot.framework.plugin.api.UtStatementCallModel +import org.utbot.framework.plugin.api.UtStatementModel +import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation +import org.utbot.framework.plugin.api.isSuccess + +inline fun T.mapPreservingType(mapper: UtModelMapper): T = + mapper.map(this, T::class.java) + +fun UtModel.map(mapper: UtModelMapper) = mapPreservingType(mapper) + +fun List.mapModels(mapper: UtModelMapper): List = + map { model -> model.map(mapper) } + +fun Map.mapModelValues(mapper: UtModelMapper): Map = + mapValues { (_, model) -> model.map(mapper) } + +fun UtStatementModel.mapModels(mapper: UtModelMapper): UtStatementModel = + when(this) { + is UtStatementCallModel -> mapModels(mapper) + is UtDirectSetFieldModel -> UtDirectSetFieldModel( + instance = instance.mapPreservingType(mapper), + fieldId = fieldId, + fieldModel = fieldModel.map(mapper) + ) + } + +fun UtStatementCallModel.mapModels(mapper: UtModelMapper): UtStatementCallModel = + when(this) { + is UtDirectGetFieldModel -> UtDirectGetFieldModel( + instance = instance.mapPreservingType(mapper), + fieldAccess = fieldAccess, + ) + is UtExecutableCallModel -> UtExecutableCallModel( + instance = instance?.mapPreservingType(mapper), + executable = executable, + params = params.mapModels(mapper) + ) + } + +fun EnvironmentModels.mapModels(mapper: UtModelMapper) = EnvironmentModels( + thisInstance = thisInstance?.map(mapper), + statics = statics.mapModelValues(mapper), + parameters = parameters.mapModels(mapper), + executableToCall = executableToCall, +) + +fun UtExecutionResult.mapModelIfExists(mapper: UtModelMapper) = if (this.isSuccess) { + val successResult = this as UtExecutionSuccess + UtExecutionSuccess(successResult.model.map(mapper)) +} else { + this +} + + +fun UtInstrumentation.mapModels(mapper: UtModelMapper) = when (this) { + is UtNewInstanceInstrumentation -> copy(instances = instances.mapModels(mapper)) + is UtStaticMethodInstrumentation -> copy(values = values.mapModels(mapper)) +} + +fun UtExecution.mapStateBeforeModels(mapper: UtModelMapper) = copy( + stateBefore = stateBefore.mapModels(mapper) +) diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt index 62e077cfdc..908e2a8ca5 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt @@ -11,11 +11,13 @@ import org.utbot.framework.plugin.api.MethodId 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.UtVoidModel +import org.utbot.framework.plugin.api.id +import soot.SootField import java.lang.reflect.Constructor import java.lang.reflect.Executable import java.lang.reflect.Field import java.lang.reflect.Method -import java.lang.reflect.Modifier import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.util.concurrent.atomic.AtomicInteger @@ -24,11 +26,14 @@ import kotlin.reflect.KCallable import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KProperty +import kotlin.reflect.full.extensionReceiverParameter import kotlin.reflect.full.instanceParameter +import kotlin.reflect.jvm.internal.impl.load.kotlin.header.KotlinClassHeader import kotlin.reflect.jvm.javaConstructor import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaGetter import kotlin.reflect.jvm.javaMethod +import kotlin.reflect.jvm.kotlinFunction // ClassId utils @@ -115,12 +120,20 @@ infix fun ClassId.isSubtypeOf(type: ClassId): Boolean { // unwrap primitive wrappers val left = primitiveByWrapper[this] ?: this val right = primitiveByWrapper[type] ?: type + if (left == right) { return true } + val leftClass = this + val superTypes = leftClass.allSuperTypes() + + return right in superTypes +} + +fun ClassId.allSuperTypes(): Sequence { val interfaces = sequence { - var types = listOf(leftClass) + var types = listOf(this@allSuperTypes) while (types.isNotEmpty()) { yieldAll(types) types = types @@ -128,9 +141,10 @@ infix fun ClassId.isSubtypeOf(type: ClassId): Boolean { .map { it.id } } } - val superclasses = generateSequence(leftClass) { it.superclass?.id } - val superTypes = interfaces + superclasses - return right in superTypes + + val superclasses = generateSequence(this) { it.superclass?.id } + + return interfaces + superclasses } infix fun ClassId.isNotSubtypeOf(type: ClassId): Boolean = !(this isSubtypeOf type) @@ -178,6 +192,29 @@ val ClassId.isDoubleType: Boolean val ClassId.isClassType: Boolean get() = this == classClassId +/** + * Returns [Metadata] annotation if this is a Kotlin class, null otherwise + */ +val ClassId.kotlinMetadata: Metadata? + get() = jClass.annotations.filterIsInstance().singleOrNull() + +val ClassId.isFromKotlin: Boolean + get() = kotlinMetadata != null + +/** + * Checks if the class is a Kotlin class with kind File (see [Metadata.kind] for more details) + */ +val ClassId.isKotlinFile: Boolean + get() = kotlinMetadata?.let { + KotlinClassHeader.Kind.getById(it.kind) == KotlinClassHeader.Kind.FILE_FACADE + } ?: false + +/** + * Returns [ClassId.simpleNameWithEnclosingClasses] with '.' replaced with '_' - to use it as a class name. + */ +val ClassId.nameWithEnclosingClassesAsContigousString: String + get() = simpleNameWithEnclosingClasses.replace('.', '_') + val voidClassId = ClassId("void") val booleanClassId = ClassId("boolean") val byteClassId = ClassId("byte") @@ -231,6 +268,8 @@ val primitiveByWrapper = mapOf( val wrapperByPrimitive = primitiveByWrapper.entries.associateBy({ it.value }) { it.key } +fun replaceWithWrapperIfPrimitive(classId: ClassId): ClassId = wrapperByPrimitive[classId] ?: classId + // We consider void primitive here // It is sometimes useful even if void is not technically a primitive type val primitives = setOf( @@ -267,9 +306,29 @@ val atomicIntegerGetAndIncrement = MethodId(atomicIntegerClassId, "getAndIncreme val iterableClassId = java.lang.Iterable::class.id val mapClassId = java.util.Map::class.id +val collectionClassId = java.util.Collection::class.id + +val listClassId = List::class.id +val setClassId = Set::class.id + +val baseStreamClassId = java.util.stream.BaseStream::class.id +val streamClassId = java.util.stream.Stream::class.id +val intStreamClassId = java.util.stream.IntStream::class.id +val longStreamClassId = java.util.stream.LongStream::class.id +val doubleStreamClassId = java.util.stream.DoubleStream::class.id + +val intStreamToArrayMethodId = methodId(intStreamClassId, "toArray", intArrayClassId) +val longStreamToArrayMethodId = methodId(longStreamClassId, "toArray", longArrayClassId) +val doubleStreamToArrayMethodId = methodId(doubleStreamClassId, "toArray", doubleArrayClassId) +val streamToArrayMethodId = methodId(streamClassId, "toArray", objectArrayClassId) val dateClassId = java.util.Date::class.id +fun getArrayClassIdByElementClassId(elementClassId: ClassId): ClassId{ + val elementClass = utContext.classLoader.loadClass(elementClassId.name) + return java.lang.reflect.Array.newInstance(elementClass,0)::class.java.id +} + @Suppress("RemoveRedundantQualifierName") val primitiveToId: Map, ClassId> = mapOf( java.lang.Void.TYPE to voidClassId, @@ -360,9 +419,21 @@ val ClassId.isMap: Boolean val ClassId.isIterableOrMap: Boolean get() = isIterable || isMap +val ClassId.isCollection: Boolean + get() = isSubtypeOf(collectionClassId) + +val ClassId.isCollectionOrMap: Boolean + get() = isCollection || isMap + val ClassId.isEnum: Boolean get() = jClass.isEnum +val ClassId.isData: Boolean + get() = kClass.isData + +val ClassId.enumConstants: List>? + get() = jClass.enumConstants?.filterIsInstance>() + fun ClassId.findFieldByIdOrNull(fieldId: FieldId): Field? { if (isNotSubtypeOf(fieldId.declaringClass)) { return null @@ -384,6 +455,7 @@ fun ClassId.defaultValueModel(): UtModel = when (this) { doubleClassId -> UtPrimitiveModel(0.0) booleanClassId -> UtPrimitiveModel(false) charClassId -> UtPrimitiveModel('\u0000') + voidClassId -> UtVoidModel else -> UtNullModel(this) } @@ -393,6 +465,16 @@ val ClassId.allDeclaredFieldIds: Sequence .flatMap { it.declaredFields.asSequence() } .map { it.fieldId } +val SootField.fieldId: FieldId + get() = FieldId(declaringClass.id, name) + +/** + * For some lambdas class names in byte code and in Soot don't match, so we may fail + * to convert some soot fields to Java fields, in such case `null` is returned. + */ +val SootField.jFieldOrNull: Field? + get() = runCatching { fieldId.jField }.getOrNull() + // FieldId utils val FieldId.safeJField: Field? get() = declaringClass.jClass.declaredFields.firstOrNull { it.name == name } @@ -430,6 +512,12 @@ val MethodId.method: Method ?: error("Can't find method $signature in ${declaringClass.name}") } +/** + * See [KCallable.extensionReceiverParameter] for more details + */ +val MethodId.extensionReceiverParameterIndex: Int? + get() = this.method.kotlinFunction?.extensionReceiverParameter?.index + // TODO: maybe cache it somehow in the future val ConstructorId.constructor: Constructor<*> get() { @@ -472,10 +560,13 @@ val Constructor<*>.executableId: ConstructorId val ExecutableId.humanReadableName: String get() { val executableName = this.name - val parameters = this.parameters.joinToString(separator = ", ") { it.canonicalName } + val parameters = this.parameters.joinToString(separator = ", ") { it.name } return "$executableName($parameters)" } +val ExecutableId.simpleNameWithClass: String + get() = "${classId.simpleName}.${name}" + val Constructor<*>.displayName: String get() = executableId.humanReadableName @@ -484,6 +575,7 @@ val Method.displayName: String val KCallable<*>.declaringClazz: Class<*> get() = when (this) { + is KFunction<*> -> javaMethod?.declaringClass?.kotlin is CallableReference -> owner as? KClass<*> else -> instanceParameter?.type?.classifier as? KClass<*> }?.java ?: tryConstructor(this) ?: error("Can't get parent class for $this") @@ -497,24 +589,22 @@ val ExecutableId.isMethod: Boolean val ExecutableId.isConstructor: Boolean get() = this is ConstructorId -val ExecutableId.isPublic: Boolean - get() = Modifier.isPublic(modifiers) - -val ExecutableId.isProtected: Boolean - get() = Modifier.isProtected(modifiers) - -val ExecutableId.isPrivate: Boolean - get() = Modifier.isPrivate(modifiers) - -val ExecutableId.isStatic: Boolean - get() = Modifier.isStatic(modifiers) - -val ExecutableId.isPackagePrivate: Boolean - get() = !(isPublic || isProtected || isPrivate) - -val ExecutableId.isAbstract: Boolean - get() = Modifier.isAbstract(modifiers) - +fun arrayTypeOf(elementType: ClassId, isNullable: Boolean = false): ClassId { + val arrayIdName = "[${elementType.arrayLikeName}" + return when (elementType) { + is BuiltinClassId -> BuiltinClassId( + canonicalName = "${elementType.canonicalName}[]", + simpleName = "${elementType.simpleName}[]", + elementClassId = elementType, + isNullable = isNullable + ) + else -> ClassId( + name = arrayIdName, + elementClassId = elementType, + isNullable = isNullable + ) + } +} /** * Construct MethodId diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IndentUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IndentUtil.kt new file mode 100644 index 0000000000..5e6bcc57a0 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IndentUtil.kt @@ -0,0 +1,5 @@ +package org.utbot.framework.plugin.api.util + +object IndentUtil { + const val TAB = " " +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/LockFile.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/LockFile.kt new file mode 100644 index 0000000000..3f5fc0a9e4 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/LockFile.kt @@ -0,0 +1,52 @@ +package org.utbot.framework.plugin.api.util + +import java.io.OutputStream +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.text.DateFormat +import kotlin.io.path.deleteIfExists +import kotlin.io.path.outputStream +import mu.KotlinLogging +import org.utbot.framework.utbotHomePath + +private val lockFilePath = "$utbotHomePath/utbot.lock" +private var currentLock : OutputStream? = null +private val logger = KotlinLogging.logger {} + +object LockFile { + @Synchronized + fun isLocked() = currentLock != null + + @Synchronized + fun lock(): Boolean { + if (currentLock != null) return false + return try { + Paths.get(utbotHomePath).toFile().mkdirs() + currentLock = Paths.get(lockFilePath).outputStream(StandardOpenOption.CREATE, StandardOpenOption.DELETE_ON_CLOSE).also { + it.write(DateFormat.getDateTimeInstance().format(System.currentTimeMillis()).toByteArray()) + } + logger.debug("Locked") + true + } catch (e: Exception) { + logger.error("Failed to lock") + false + } + } + + @Synchronized + fun unlock(): Boolean { + try { + val tmp = currentLock + if (tmp != null) { + tmp.close() + Paths.get(lockFilePath).deleteIfExists() + logger.debug("Unlocked") + currentLock = null + return true + } + } catch (ignored: Exception) { + logger.error("Failed to unlock") + } + return false + } +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ModifierUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ModifierUtil.kt new file mode 100644 index 0000000000..bb40f5b7a4 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ModifierUtil.kt @@ -0,0 +1,102 @@ +package org.utbot.framework.plugin.api.util + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MethodId +import java.lang.reflect.Modifier + +class ModifierFactory private constructor( + configure: ModifierFactory.() -> Unit = {} +) { + var public: Boolean = true + var protected: Boolean = false + var private: Boolean = false + var final: Boolean = false + var static: Boolean = false + var abstract: Boolean = false + + init { + this.configure() + } + + private val modifiers: Int = + (Modifier.PUBLIC.takeIf { public } ?: 0) or + (Modifier.PRIVATE.takeIf { private } ?: 0) or + (Modifier.PROTECTED.takeIf { protected } ?: 0) or + (Modifier.FINAL.takeIf { final } ?: 0) or + (Modifier.STATIC.takeIf { static } ?: 0) or + (Modifier.ABSTRACT.takeIf { abstract } ?: 0) + + companion object { + operator fun invoke(configure: ModifierFactory.() -> Unit): Int { + return ModifierFactory(configure).modifiers + } + } +} + +// ClassIds + +val ClassId.isAbstract: Boolean + get() = Modifier.isAbstract(modifiers) + +val ClassId.isPrivate: Boolean + get() = Modifier.isPrivate(modifiers) + +val ClassId.isPackagePrivate: Boolean + get() = !(isPublic || isProtected || isPrivate) + +val ClassId.isStatic: Boolean + get() = Modifier.isStatic(modifiers) + +val ClassId.isFinal: Boolean + get() = Modifier.isFinal(modifiers) + +val ClassId.isProtected: Boolean + get() = Modifier.isProtected(modifiers) + +val ClassId.isPublic: Boolean + get() = Modifier.isPublic(modifiers) + +// ExecutableIds + +val ExecutableId.isPublic: Boolean + get() = Modifier.isPublic(modifiers) + +val ExecutableId.isProtected: Boolean + get() = Modifier.isProtected(modifiers) + +val ExecutableId.isPrivate: Boolean + get() = Modifier.isPrivate(modifiers) + +val ExecutableId.isStatic: Boolean + get() = Modifier.isStatic(modifiers) + +val ExecutableId.isPackagePrivate: Boolean + get() = !(isPublic || isProtected || isPrivate) + +val ExecutableId.isAbstract: Boolean + get() = Modifier.isAbstract(modifiers) + +val ExecutableId.isSynthetic: Boolean + get() = (this is MethodId) && method.isSynthetic + +// FieldIds + +val FieldId.isStatic: Boolean + get() = Modifier.isStatic(modifiers) + +val FieldId.isFinal: Boolean + get() = Modifier.isFinal(modifiers) + +val FieldId.isPackagePrivate: Boolean + get() = !(isPublic || isProtected || isPrivate) + +val FieldId.isProtected: Boolean + get() = Modifier.isProtected(modifiers) + +val FieldId.isPrivate + get() = Modifier.isPrivate(modifiers) + +val FieldId.isPublic: Boolean + get() = Modifier.isPublic(modifiers) \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ReflectionUtils.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ReflectionUtils.kt new file mode 100644 index 0000000000..5cfa8ab8f3 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ReflectionUtils.kt @@ -0,0 +1,63 @@ +package org.utbot.framework.plugin.api.util + +import org.utbot.common.Reflection +import org.utbot.framework.plugin.api.FieldId +import soot.RefType + +/** + * Several fields are inaccessible in runtime even via reflection + */ +val FieldId.isInaccessibleViaReflection: Boolean + get() { + val declaringClassName = declaringClass.name + return declaringClassName in inaccessibleViaReflectionClasses || + (name to declaringClassName) in inaccessibleViaReflectionFields + } + +val RefType.isInaccessibleViaReflection: Boolean + get() { + return className in inaccessibleViaReflectionClasses + } + +private val inaccessibleViaReflectionClasses = setOf( + "jdk.internal.reflect.ReflectionFactory", + "jdk.internal.reflect.Reflection", + "jdk.internal.loader.ClassLoaderValue", + "sun.reflect.Reflection", +) + +private val inaccessibleViaReflectionFields = setOf( + "security" to "java.lang.System", +) + +@Suppress("DEPRECATION") +val Class<*>.anyInstance: Any + get() { +// val defaultCtor = declaredConstructors.singleOrNull { it.parameterCount == 0} +// if (defaultCtor != null) { +// try { +// defaultCtor.isAccessible = true +// return defaultCtor.newInstance() +// } catch (e : Throwable) { +// logger.warn(e) { "Can't create object with default ctor. Fallback to Unsafe." } +// } +// } + return Reflection.unsafe.allocateInstance(this) + +// val constructors = runCatching { +// arrayOf(getDeclaredConstructor()) +// }.getOrElse { declaredConstructors } +// +// return constructors.asSequence().mapNotNull { constructor -> +// runCatching { +// val parameters = constructor.parameterTypes.map { defaultParameterValue(it) } +// val isAccessible = constructor.isAccessible +// try { +// constructor.isAccessible = true +// constructor.newInstance(*parameters.toTypedArray()) +// } finally { +// constructor.isAccessible = isAccessible +// } +// }.getOrNull() +// }.firstOrNull() ?: error("Failed to create instance of $this") + } \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SignatureUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SignatureUtil.kt index a2befaed6b..9a7c1b4cc9 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SignatureUtil.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SignatureUtil.kt @@ -76,7 +76,7 @@ fun Class<*>.singleConstructor(signature: String): Constructor<*> = fun Class<*>.singleMethodOrNull(signature: String): Method? = generateSequence(this) { it.superclass }.mapNotNull { clazz -> - clazz.declaredMethods.firstOrNull { it.signature == signature } + (clazz.methods + clazz.declaredMethods).firstOrNull { it.signature == signature } }.firstOrNull() /** diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SpringModelUtils.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SpringModelUtils.kt new file mode 100644 index 0000000000..2e468b3322 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SpringModelUtils.kt @@ -0,0 +1,982 @@ +package org.utbot.framework.plugin.api.util + +import mu.KotlinLogging +import org.utbot.common.tryLoadClass +import org.utbot.common.withToStringThreadLocalReentrancyGuard +import org.utbot.framework.plugin.api.isNotNull +import org.utbot.framework.plugin.api.ClassId +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.UtModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.UtSpringContextModel +import org.utbot.framework.plugin.api.UtStatementModel +import org.utbot.framework.plugin.api.mapper.UtModelDeepMapper +import org.utbot.framework.plugin.api.mapper.mapModels +import java.util.Optional + +private val logger = KotlinLogging.logger {} + +object SpringModelUtils { + val autowiredClassId = ClassId("org.springframework.beans.factory.annotation.Autowired") + val injectClassIds = getClassIdFromEachAvailablePackage( + packages = listOf("javax", "jakarta"), + classNameFromPackage = "inject.Inject" + ) + val componentClassId = ClassId("org.springframework.stereotype.Component") + + val applicationContextClassId = ClassId("org.springframework.context.ApplicationContext") + val repositoryClassId = ClassId("org.springframework.data.repository.Repository") + + val springBootTestClassId = ClassId("org.springframework.boot.test.context.SpringBootTest") + + val dirtiesContextClassId = ClassId("org.springframework.test.annotation.DirtiesContext") + val dirtiesContextClassModeClassId = ClassId("org.springframework.test.annotation.DirtiesContext\$ClassMode") + val transactionalClassId = ClassId("org.springframework.transaction.annotation.Transactional") + val autoConfigureTestDbClassId = ClassId("org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase") + val autoConfigureMockMvcClassId = ClassId("org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc") + val withMockUserClassId = ClassId("org.springframework.security.test.context.support.WithMockUser") + + val runWithClassId = ClassId("org.junit.runner.RunWith") + val springRunnerClassId = ClassId("org.springframework.test.context.junit4.SpringRunner") + + val extendWithClassId = ClassId("org.junit.jupiter.api.extension.ExtendWith") + val springExtensionClassId = ClassId("org.springframework.test.context.junit.jupiter.SpringExtension") + + val bootstrapWithClassId = ClassId("org.springframework.test.context.BootstrapWith") + val springBootTestContextBootstrapperClassId = + ClassId("org.springframework.boot.test.context.SpringBootTestContextBootstrapper") + + val activeProfilesClassId = ClassId("org.springframework.test.context.ActiveProfiles") + val contextConfigurationClassId = ClassId("org.springframework.test.context.ContextConfiguration") + + private fun getClassIdFromEachAvailablePackage( + packages: List, + classNameFromPackage: String + ): List = packages.map { ClassId("$it.$classNameFromPackage") } + .filter { utContext.classLoader.tryLoadClass(it.name) != null } + + // most likely only one persistent library is on the classpath, but we need to be able to work with either of them + private val persistentLibraries = listOf("javax.persistence", "jakarta.persistence") + private fun persistentClassIds(simpleName: String) = getClassIdFromEachAvailablePackage(persistentLibraries, simpleName) + + // the library in which Cookie is stored depends on the version of Spring + private val cookiesLibraries = listOf("javax.servlet.http", "jakarta.servlet.http") + private val cookieClassIds = getClassIdFromEachAvailablePackage(cookiesLibraries, "Cookie") + + val entityClassIds get() = persistentClassIds("Entity") + val generatedValueClassIds get() = persistentClassIds("GeneratedValue") + val idClassIds get() = persistentClassIds("Id") + val persistenceContextClassIds get() = persistentClassIds("PersistenceContext") + val entityManagerClassIds get() = persistentClassIds("EntityManager") + + val persistMethodIdOrNull: MethodId? + get() { + return MethodId( + classId = entityManagerClassIds.firstOrNull() ?: return null, + name = "persist", + returnType = voidClassId, + parameters = listOf(objectClassId), + bypassesSandbox = true // TODO may be we can use some alternative sandbox that has more permissions + ) + } + + val flushMethodIdOrNull: MethodId? + get() { + return MethodId( + classId = entityManagerClassIds.firstOrNull() ?: return null, + name = "flush", + returnType = voidClassId, + parameters = listOf(), + bypassesSandbox = true // TODO may be we can use some alternative sandbox that has more permissions + ) + } + + val detachMethodIdOrNull: MethodId? + get() { + return MethodId( + classId = entityManagerClassIds.firstOrNull() ?: return null, + name = "detach", + returnType = voidClassId, + parameters = listOf(objectClassId), + bypassesSandbox = true // TODO may be we can use some alternative sandbox that has more permissions + ) + } + + private val validationLibraries = listOf("javax.validation.constraints", "jakarta.validation.constraints") + private fun validationClassIds(simpleName: String) = getClassIdFromEachAvailablePackage(validationLibraries, simpleName) + .filter { utContext.classLoader.tryLoadClass(it.name) != null } + + val notEmptyClassIds get() = validationClassIds("NotEmpty") + val notBlankClassIds get() = validationClassIds("NotBlank") + val emailClassIds get() = validationClassIds("Email") + + + private val getBeanMethodId = MethodId( + classId = applicationContextClassId, + name = "getBean", + returnType = Any::class.id, + parameters = listOf(String::class.id), + bypassesSandbox = true // TODO may be we can use some alternative sandbox that has more permissions + ) + + fun createBeanModel( + beanName: String, + id: Int, + classId: ClassId, + modificationChainProvider: UtAssembleModel.() -> List = { mutableListOf() }, + ) = UtAssembleModel( + id = id, + classId = classId, + modelName = "@Autowired $beanName", + instantiationCall = UtExecutableCallModel( + instance = UtSpringContextModel, + executable = getBeanMethodId, + params = listOf(UtPrimitiveModel(beanName)) + ), + modificationsChainProvider = modificationChainProvider + ) + + fun UtModel.isAutowiredFromContext(): Boolean = + this is UtAssembleModel && this.instantiationCall.instance is UtSpringContextModel + + fun UtModel.getBeanNameOrNull(): String? = if (isAutowiredFromContext()) { + this as UtAssembleModel + val beanNameParam = this.instantiationCall.params.single() + val paramValue = (beanNameParam as? UtPrimitiveModel)?.value + paramValue.toString() + } else { + null + } + + ///region spring-web + private val requestMappingClassId = ClassId("org.springframework.web.bind.annotation.RequestMapping") + private val pathVariableClassId = ClassId("org.springframework.web.bind.annotation.PathVariable") + private val requestHeaderClassId = ClassId("org.springframework.web.bind.annotation.RequestHeader") + private val cookieValueClassId = ClassId("org.springframework.web.bind.annotation.CookieValue") + private val requestAttributesClassId = ClassId("org.springframework.web.bind.annotation.RequestAttribute") + private val sessionAttributesClassId = ClassId("org.springframework.web.bind.annotation.SessionAttribute") + private val modelAttributesClassId = ClassId("org.springframework.web.bind.annotation.ModelAttribute") + private val requestBodyClassId = ClassId("org.springframework.web.bind.annotation.RequestBody") + private val requestParamClassId = ClassId("org.springframework.web.bind.annotation.RequestParam") + private val uriComponentsBuilderClassId = ClassId("org.springframework.web.util.UriComponentsBuilder") + private val mediaTypeClassId = ClassId("org.springframework.http.MediaType") + private val mockHttpServletResponseClassId = ClassId("org.springframework.mock.web.MockHttpServletResponse") + + private val mockMvcRequestBuildersClassId = ClassId("org.springframework.test.web.servlet.request.MockMvcRequestBuilders") + private val requestBuilderClassId = ClassId("org.springframework.test.web.servlet.RequestBuilder") + val resultActionsClassId = ClassId("org.springframework.test.web.servlet.ResultActions") + val mockMvcClassId = ClassId("org.springframework.test.web.servlet.MockMvc") + private val mvcResultClassId = ClassId("org.springframework.test.web.servlet.MvcResult") + private val resultHandlerClassId = ClassId("org.springframework.test.web.servlet.ResultHandler") + val mockMvcResultHandlersClassId = ClassId("org.springframework.test.web.servlet.result.MockMvcResultHandlers") + private val resultMatcherClassId = ClassId("org.springframework.test.web.servlet.ResultMatcher") + val mockMvcResultMatchersClassId = ClassId("org.springframework.test.web.servlet.result.MockMvcResultMatchers") + private val statusResultMatchersClassId = ClassId("org.springframework.test.web.servlet.result.StatusResultMatchers") + private val contentResultMatchersClassId = ClassId("org.springframework.test.web.servlet.result.ContentResultMatchers") + private val viewResultMatchersClassId = ClassId("org.springframework.test.web.servlet.result.ViewResultMatchers") + private val mockHttpServletRequestBuilderClassId = ClassId("org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder") + private val modelAndViewClassId = ClassId("org.springframework.web.servlet.ModelAndView") + private val httpHeaderClassId = ClassId("org.springframework.http.HttpHeaders") + + private val objectMapperClassId = ClassId("com.fasterxml.jackson.databind.ObjectMapper") + + // as of Spring 6.0 `NestedServletException` is deprecated in favor of standard `ServletException` nesting + val nestedServletExceptionClassIds = listOf( + ClassId("org.springframework.web.util.NestedServletException"), + ClassId("jakarta.servlet.ServletException") + ) + + private val requestAttributesMethodId = MethodId( + classId = mockHttpServletRequestBuilderClassId, + name = "requestAttr", + returnType = mockHttpServletRequestBuilderClassId, + parameters = listOf(stringClassId, objectClassId) + ) + + private val sessionAttributesMethodId = MethodId( + classId = mockHttpServletRequestBuilderClassId, + name = "sessionAttr", + returnType = mockHttpServletRequestBuilderClassId, + parameters = listOf(stringClassId, objectClassId) + ) + + private val modelAttributesMethodId = MethodId( + classId = mockHttpServletRequestBuilderClassId, + name = "flashAttr", + returnType = mockHttpServletRequestBuilderClassId, + parameters = listOf(stringClassId, objectClassId) + ) + + private val mockHttpServletHeadersMethodId = MethodId( + classId = mockHttpServletRequestBuilderClassId, + name = "headers", + returnType = mockHttpServletRequestBuilderClassId, + parameters = listOf(httpHeaderClassId) + ) + + private fun mockHttpServletCookieMethodId(cookieClassId: ClassId) = MethodId( + classId = mockHttpServletRequestBuilderClassId, + name = "cookie", + returnType = mockHttpServletRequestBuilderClassId, + parameters = listOf(getArrayClassIdByElementClassId(cookieClassId)) + ) + + private val mockHttpServletContentTypeMethodId = MethodId( + classId = mockHttpServletRequestBuilderClassId, + name = "contentType", + returnType = mockHttpServletRequestBuilderClassId, + parameters = listOf(mediaTypeClassId) + ) + + private val mockHttpServletContentMethodId = MethodId( + classId = mockHttpServletRequestBuilderClassId, + name = "content", + returnType = mockHttpServletRequestBuilderClassId, + parameters = listOf(stringClassId) + ) + + val mockMvcPerformMethodId = MethodId( + classId = mockMvcClassId, + name = "perform", + parameters = listOf(requestBuilderClassId), + returnType = resultActionsClassId + ) + + val resultActionsAndReturnMethodId = MethodId( + classId = resultActionsClassId, + name = "andReturn", + parameters = listOf(), + returnType = mvcResultClassId + ) + + val mvcResultGetResponseMethodId = MethodId( + classId = mvcResultClassId, + name = "getResponse", + parameters = listOf(), + returnType = mockHttpServletResponseClassId + ) + + val responseGetStatusMethodId = MethodId( + classId = mockHttpServletResponseClassId, + name = "getStatus", + parameters = listOf(), + returnType = intClassId + ) + + val responseGetErrorMessageMethodId = MethodId( + classId = mockHttpServletResponseClassId, + name = "getErrorMessage", + parameters = listOf(), + returnType = stringClassId + ) + + val responseGetContentAsStringMethodId = MethodId( + classId = mockHttpServletResponseClassId, + name = "getContentAsString", + parameters = listOf(), + returnType = stringClassId + ) + + val mvcResultGetModelAndViewMethodId = MethodId( + classId = mvcResultClassId, + name = "getModelAndView", + parameters = listOf(), + returnType = modelAndViewClassId + ) + + val modelAndViewGetModelMethodId = MethodId( + classId = modelAndViewClassId, + name = "getModel", + parameters = listOf(), + returnType = mapClassId + ) + + val modelAndViewGetViewNameMethodId = MethodId( + classId = modelAndViewClassId, + name = "getViewName", + parameters = listOf(), + returnType = stringClassId + ) + + val resultActionsAndDoMethodId = MethodId( + classId = resultActionsClassId, + name = "andDo", + parameters = listOf(resultHandlerClassId), + returnType = resultActionsClassId + ) + + val resultHandlersPrintMethodId = MethodId( + classId = mockMvcResultHandlersClassId, + name = "print", + parameters = listOf(), + returnType = resultHandlerClassId + ) + + val resultActionsAndExpectMethodId = MethodId( + classId = resultActionsClassId, + name = "andExpect", + parameters = listOf(resultMatcherClassId), + returnType = resultActionsClassId + ) + + val resultMatchersStatusMethodId = MethodId( + classId = mockMvcResultMatchersClassId, + name = "status", + parameters = listOf(), + returnType = statusResultMatchersClassId + ) + + val statusMatchersIsMethodId = MethodId( + classId = statusResultMatchersClassId, + name = "is", + parameters = listOf(intClassId), + returnType = resultMatcherClassId + ) + + val resultMatchersContentMethodId = MethodId( + classId = mockMvcResultMatchersClassId, + name = "content", + parameters = listOf(), + returnType = contentResultMatchersClassId + ) + + val contentMatchersStringMethodId = MethodId( + classId = contentResultMatchersClassId, + name = "string", + parameters = listOf(stringClassId), + returnType = resultMatcherClassId + ) + + val resultMatchersViewMethodId = MethodId( + classId = mockMvcResultMatchersClassId, + name = "view", + parameters = listOf(), + returnType = viewResultMatchersClassId + ) + + val viewMatchersNameMethodId = MethodId( + classId = viewResultMatchersClassId, + name = "name", + parameters = listOf(stringClassId), + returnType = resultMatcherClassId + ) + + private val supportedControllerParameterAnnotations = setOf( + pathVariableClassId, + requestParamClassId, + requestHeaderClassId, + cookieValueClassId, + requestAttributesClassId, + sessionAttributesClassId, + modelAttributesClassId, + requestBodyClassId, + ) + + /** + * If a controller method has a parameter of one of these types, then we don't fully support conversion + * of direct call of that controller method call to a request that can be done via `mockMvc` even if + * said parameter is annotated with one of [supportedControllerParameterAnnotations]. + */ + private val unsupportedControllerParameterTypes = setOf( + dateClassId, // see #2505 + mapClassId, // e.g. `@RequestParam Map` is not yet properly handled + ) + + /** + * Returns `true` if for every parameter of [methodId] we have a mechanism of registering said + * parameter in `requestBuilder` when calling controller method via `mockMvc.perform(requestBuilder)`. + */ + fun allControllerParametersAreSupported(methodId: MethodId): Boolean = + methodId.parameters.none { it in unsupportedControllerParameterTypes } && + methodId.method.parameters.all { param -> + param.annotations.any { annotation -> + annotation.annotationClass.id in supportedControllerParameterAnnotations + } + } + + fun createMockMvcModel(controller: UtModel?, idGenerator: () -> Int) = + createBeanModel("mockMvc", idGenerator(), mockMvcClassId, modificationChainProvider = { + // we need to keep controller modifications if there are any, so we add them to mockMvc + (controller as? UtAssembleModel)?.let { assembledController -> + val controllerModificationRemover = UtModelDeepMapper { model -> + if (model == assembledController) assembledController.copy(modificationsChain = emptyList()) + else model + } + // modificationsChain may mention controller, causing controller modifications to evaluate twice: + // once for mockMvc and once for controller itself, to avoid that we remove modifications from controller + assembledController.modificationsChain.map { it.mapModels(controllerModificationRemover) } + } ?: mutableListOf() + }) + + fun createRequestBuilderModelOrNull(methodId: MethodId, arguments: List, idGenerator: () -> Int): UtModel? { + check(methodId.parameters.size == arguments.size) + + if (methodId.isStatic) return null + + val requestMappingAnnotation = getRequestMappingAnnotationOrNull(methodId) ?: return null + val requestMethod = getRequestMethodOrNull(requestMappingAnnotation) ?: return null + + @Suppress("UNCHECKED_CAST") + val classRequestMappingAnnotation: Annotation? = + methodId.classId.jClass.getAnnotation(requestMappingClassId.jClass as Class) + val classRequestPath = classRequestMappingAnnotation?.let { getRequestPathOrNull(it) }.orEmpty() + + val requestPath = classRequestPath + (getRequestPathOrNull(requestMappingAnnotation) ?: return null) + + val pathVariablesModel = createPathVariablesModel(methodId, arguments, idGenerator) + + val requestParamsModel = createRequestParamsModel(methodId, arguments, idGenerator) + + val urlTemplateModel = createUrlTemplateModel(requestPath, pathVariablesModel, requestParamsModel, idGenerator) + + var requestBuilderModel = UtAssembleModel( + id = idGenerator(), + classId = mockHttpServletRequestBuilderClassId, + modelName = "requestBuilder", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = requestMethod.requestBuilderMethodId, + params = listOf( + urlTemplateModel, + UtArrayModel( + id = idGenerator(), + classId = objectArrayClassId, + length = 0, + constModel = UtNullModel(objectClassId), + stores = mutableMapOf() + ) + ) + ) + ) + + val headersContentModel = createHeadersContentModel(methodId, arguments, idGenerator) + requestBuilderModel = addHeadersToRequestBuilderModel(headersContentModel, requestBuilderModel, idGenerator) + + cookieClassIds.singleOrNull()?.let { cookieClassId -> + val cookieValuesModel = createCookieValuesModel(cookieClassId, methodId, arguments, idGenerator) + requestBuilderModel = + addCookiesToRequestBuilderModel(cookieClassId, cookieValuesModel, requestBuilderModel, idGenerator) + } ?: logger.warn { "Cookie library not found" } + + val requestAttributes = collectArgumentsWithAnnotationModels(methodId, requestAttributesClassId, arguments) + requestBuilderModel = + addRequestAttributesToRequestModelBuilder(requestAttributes, requestBuilderModel, idGenerator) + + val sessionAttributes = collectArgumentsWithAnnotationModels(methodId, sessionAttributesClassId, arguments) + requestBuilderModel = + addSessionAttributesToRequestModelBuilder(sessionAttributes, requestBuilderModel, idGenerator) + + val modelAttributes = collectArgumentsWithAnnotationModels(methodId, modelAttributesClassId, arguments) + requestBuilderModel = + addModelAttributesToRequestModelBuilder(modelAttributes, requestBuilderModel, idGenerator) + + return addContentToRequestBuilderModel(methodId, arguments, requestBuilderModel, idGenerator) + } + + private fun addRequestAttributesToRequestModelBuilder( + requestAttributes: Map, + requestBuilderModel: UtAssembleModel, + idGenerator: () -> Int + ): UtAssembleModel = addAttributesToRequestBuilderModel( + requestAttributes, + requestAttributesMethodId, + requestBuilderModel, + idGenerator + ) + + + private fun addSessionAttributesToRequestModelBuilder( + sessionAttributes: Map, + requestBuilderModel: UtAssembleModel, + idGenerator: () -> Int + ): UtAssembleModel = addAttributesToRequestBuilderModel( + sessionAttributes, + sessionAttributesMethodId, + requestBuilderModel, + idGenerator + ) + + private fun addModelAttributesToRequestModelBuilder( + modelAttributes: Map, + requestBuilderModel: UtAssembleModel, + idGenerator: () -> Int + ): UtAssembleModel = addAttributesToRequestBuilderModel( + modelAttributes, + modelAttributesMethodId, + requestBuilderModel, + idGenerator + ) + + + private fun addAttributesToRequestBuilderModel( + attributes: Map, + addAttributesMethodId: MethodId, + requestBuilderModel: UtAssembleModel, + idGenerator: () -> Int + ): UtAssembleModel{ + @Suppress("NAME_SHADOWING") + var requestBuilderModel = requestBuilderModel + + attributes.forEach { (name, model) -> + requestBuilderModel = UtAssembleModel( + id = idGenerator(), + classId = mockHttpServletRequestBuilderClassId, + modelName = "requestBuilder", + instantiationCall = UtExecutableCallModel( + instance = requestBuilderModel, + executable = addAttributesMethodId, + params = listOf(UtPrimitiveModel(name), model) + ) + ) + } + + return requestBuilderModel + } + + private fun addCookiesToRequestBuilderModel( + cookieClassId: ClassId, + cookieValuesModel: UtArrayModel, + requestBuilderModel: UtAssembleModel, + idGenerator: () -> Int + ): UtAssembleModel { + @Suppress("NAME_SHADOWING") + var requestBuilderModel = requestBuilderModel + + if(cookieValuesModel.length > 0) { + requestBuilderModel = UtAssembleModel( + id = idGenerator(), + classId = mockHttpServletRequestBuilderClassId, + modelName = "requestBuilder", + instantiationCall = UtExecutableCallModel( + instance = requestBuilderModel, + executable = mockHttpServletCookieMethodId(cookieClassId), + params = listOf(cookieValuesModel) + ) + ) + } + return requestBuilderModel + } + + private fun addHeadersToRequestBuilderModel( + headersContentModel: UtAssembleModel, + requestBuilderModel: UtAssembleModel, + idGenerator: () -> Int + ): UtAssembleModel { + @Suppress("NAME_SHADOWING") + var requestBuilderModel = requestBuilderModel + + if (headersContentModel.modificationsChain.isEmpty()) { + return requestBuilderModel + } + + val headers = UtAssembleModel( + id = idGenerator(), + classId = httpHeaderClassId, + modelName = "headers", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = constructorId(httpHeaderClassId), + params = emptyList(), + ), + modificationsChainProvider = { + listOf( + UtExecutableCallModel( + instance = this, + executable = methodId( + classId = httpHeaderClassId, + name = "setAll", + returnType = voidClassId, + arguments = arrayOf(Map::class.java.id), + ), + params = listOf(headersContentModel) + ) + ) + } + ) + + requestBuilderModel = UtAssembleModel( + id = idGenerator(), + classId = mockHttpServletRequestBuilderClassId, + modelName = "requestBuilder", + instantiationCall = UtExecutableCallModel( + instance = requestBuilderModel, + executable = mockHttpServletHeadersMethodId, + params = listOf(headers) + ) + ) + + return requestBuilderModel + } + + private fun addContentToRequestBuilderModel( + methodId: MethodId, + arguments: List, + requestBuilderModel: UtAssembleModel, + idGenerator: () -> Int + ): UtAssembleModel? { + @Suppress("NAME_SHADOWING") + var requestBuilderModel = requestBuilderModel + methodId.method.parameters.zip(arguments).forEach { (param, arg) -> + @Suppress("UNCHECKED_CAST") + param.getAnnotation(requestBodyClassId.jClass as Class) ?: return@forEach + + val mediaTypeModel = UtAssembleModel( + id = idGenerator(), + classId = mediaTypeClassId, + modelName = "mediaType", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = MethodId( + classId = mediaTypeClassId, + name = "valueOf", + returnType = mediaTypeClassId, + parameters = listOf(stringClassId) + ), + // TODO detect actual media type ("application/json" is very common default) + params = listOf(UtPrimitiveModel("application/json")) + ), + ) + requestBuilderModel = UtAssembleModel( + id = idGenerator(), + classId = mockHttpServletRequestBuilderClassId, + modelName = "requestBuilder", + instantiationCall = UtExecutableCallModel( + instance = requestBuilderModel, + executable = mockHttpServletContentTypeMethodId, + params = listOf( + mediaTypeModel + ) + ) + ) + val content = UtAssembleModel( + id = idGenerator(), + classId = stringClassId, + modelName = "content", + instantiationCall = UtExecutableCallModel( + instance = + // TODO support libraries other than Jackson + if (utContext.classLoader.tryLoadClass(objectMapperClassId.name) == null) + return@addContentToRequestBuilderModel null + // TODO `getBean(ObjectMapper.class)`, name may change depending on Spring version + else createBeanModel("jacksonObjectMapper", idGenerator(), objectMapperClassId), + executable = MethodId( + classId = objectMapperClassId, + name = "writeValueAsString", + returnType = stringClassId, + parameters = listOf(objectClassId) + ), + params = listOf(arg) + ) + ) + requestBuilderModel = UtAssembleModel( + id = idGenerator(), + classId = mockHttpServletRequestBuilderClassId, + modelName = "requestBuilder", + instantiationCall = UtExecutableCallModel( + instance = requestBuilderModel, + executable = mockHttpServletContentMethodId, + params = listOf(content) + ) + ) + } + return requestBuilderModel + } + + private fun createCookieValuesModel( + cookieClassId: ClassId, + methodId: MethodId, + arguments: List, + idGenerator: () -> Int, + ): UtArrayModel { + val cookieValues = collectArgumentsWithAnnotationModels(methodId, cookieValueClassId, arguments) + .mapValues { (_, model) -> convertModelValueToString(model) }.toList() + + // Creating an indexed Map for `UtArrayModel.stores` + val indexedCookieValues = HashMap() + cookieValues.indices.forEach { ind -> + indexedCookieValues[ind] = UtAssembleModel( + id = idGenerator(), + classId = cookieClassId, + modelName = "cookie", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = constructorId(cookieClassId, stringClassId, stringClassId), + params = listOf(UtPrimitiveModel(cookieValues[ind].first), cookieValues[ind].second), + ) + ) + } + + return UtArrayModel( + id = idGenerator(), + classId = getArrayClassIdByElementClassId(cookieClassId), + length = cookieValues.size, + constModel = UtNullModel(cookieClassId), + stores = indexedCookieValues, + ) + } + + private fun createHeadersContentModel( + methodId: MethodId, + arguments: List, + idGenerator: () -> Int, + ): UtAssembleModel { + // Converts Map models values to String because `HttpHeaders.setAll(...)` method takes `Map` + val headersContent = collectArgumentsWithAnnotationModels(methodId, requestHeaderClassId, arguments) + .mapValues { (_, model) -> convertModelValueToString(model) } + + return UtAssembleModel( + id = idGenerator(), + classId = Map::class.java.id, + modelName = "headersContent", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = HashMap::class.java.getConstructor().executableId, + params = emptyList() + ), + modificationsChainProvider = { + headersContent.map { (name, value) -> + UtExecutableCallModel( + instance = this, + // Actually it is a `Map`, but we use `Object::class.java` to avoid concrete failures + executable = Map::class.java.getMethod( + "put", + Object::class.java, + Object::class.java + ).executableId, + params = listOf(UtPrimitiveModel(name), value) + ) + } + } + ) + } + + private fun createPathVariablesModel( + methodId: MethodId, + arguments: List, + idGenerator: () -> Int + ): UtAssembleModel { + val pathVariables = collectArgumentsWithAnnotationModels(methodId, pathVariableClassId, arguments) + + return UtAssembleModel( + id = idGenerator(), + classId = Map::class.java.id, + modelName = "pathVariables", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = HashMap::class.java.getConstructor().executableId, + params = emptyList() + ), + modificationsChainProvider = { + pathVariables.map { (name, value) -> + UtExecutableCallModel( + instance = this, + executable = Map::class.java.getMethod( + "put", + Object::class.java, + Object::class.java + ).executableId, + params = listOf(UtPrimitiveModel(name), value) + ) + } + } + ) + } + + private fun createRequestParamsModel( + methodId: MethodId, + arguments: List, + idGenerator: () -> Int + ): List> { + val requestParams = collectArgumentsWithAnnotationModels(methodId, requestParamClassId, arguments) + + return requestParams.map { (name, value) -> + Pair(UtPrimitiveModel(name), + UtArrayModel( + id = idGenerator(), + classId = getArrayClassIdByElementClassId(objectClassId), + length = 1, + constModel = UtNullModel(objectClassId), + stores = mutableMapOf(0 to value), + ) + ) + } + } + + private fun collectArgumentsWithAnnotationModels( + methodId: MethodId, + annotationClassId: ClassId, + arguments: List + ): MutableMap { + fun UtModel.isEmptyOptional(): Boolean { + return classId == Optional::class.java.id && this is UtAssembleModel && + instantiationCall is UtExecutableCallModel && instantiationCall.executable.name == "empty" + } + + val argumentsModels = mutableMapOf() + methodId.method.parameters.zip(arguments).forEach { (param, arg) -> + @Suppress("UNCHECKED_CAST") val paramAnnotation = + param.getAnnotation(annotationClassId.jClass as Class) ?: return@forEach + val name = (annotationClassId.jClass.getMethod("name").invoke(paramAnnotation) as? String).orEmpty() + .ifEmpty { annotationClassId.jClass.getMethod("value").invoke(paramAnnotation) as? String }.orEmpty() + .ifEmpty { param.name } + + if (arg.isNotNull() && !arg.isEmptyOptional()) { + argumentsModels[name] = arg + } + } + + return argumentsModels + } + + /** + * Converts the model into a form that is understandable for annotations. + * Example: UtArrayModel([UtPrimitiveModel("a"), UtPrimitiveModel("b"), UtPrimitiveModel("c")]) -> UtPrimitiveModel("a, b, c") + * + * There is known issue when using `model.toString()` is not reliable: + * https://github.com/UnitTestBot/UTBotJava/issues/2505 + * This issue may be improved in the future. + */ + private fun convertModelValueToString(model: UtModel): UtModel { + return UtPrimitiveModel( + when(model){ + is UtArrayModel -> withToStringThreadLocalReentrancyGuard { + (0 until model.length).map { model.stores[it] ?: model.constModel }.joinToString(", ") + } + else -> model.toString() + } + ) + } + + private fun createUrlTemplateModel( + requestPath: String, + pathVariablesModel: UtAssembleModel, + requestParamModel: List>, + idGenerator: () -> Int + ): UtModel { + val requestPathModel = UtPrimitiveModel(requestPath) + return if (pathVariablesModel.modificationsChain.isEmpty() && requestParamModel.isEmpty()) requestPathModel + else { + var uriBuilderFromPath = UtAssembleModel( + id = idGenerator(), + classId = uriComponentsBuilderClassId, + modelName = "uriBuilderFromPath", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = MethodId( + classId = uriComponentsBuilderClassId, + name = "fromPath", + parameters = listOf(stringClassId), + returnType = uriComponentsBuilderClassId + ), + params = listOf(requestPathModel), + ) + ) + + if(pathVariablesModel.modificationsChain.isNotEmpty()) { + uriBuilderFromPath = UtAssembleModel( + id = idGenerator(), + classId = uriComponentsBuilderClassId, + modelName = "uriBuilderWithPathVariables", + instantiationCall = UtExecutableCallModel( + instance = uriBuilderFromPath, + executable = MethodId( + classId = uriComponentsBuilderClassId, + name = "uriVariables", + parameters = listOf(Map::class.java.id), + returnType = uriComponentsBuilderClassId + ), + params = listOf(pathVariablesModel), + ) + ) + } + + requestParamModel.forEach { (name, value) -> + uriBuilderFromPath = UtAssembleModel( + id = idGenerator(), + classId = uriComponentsBuilderClassId, + modelName = "uriBuilderWithRequestParam", + instantiationCall = UtExecutableCallModel( + instance = uriBuilderFromPath, + executable = MethodId( + classId = uriComponentsBuilderClassId, + name = "queryParam", + parameters = listOf(stringClassId, getArrayClassIdByElementClassId(objectClassId)), + returnType = uriComponentsBuilderClassId + ), + params = listOf(name, value), + ) + ) + } + + return UtAssembleModel( + id = idGenerator(), + classId = stringClassId, + modelName = "uriString", + instantiationCall = UtExecutableCallModel( + instance = uriBuilderFromPath, + executable = MethodId( + classId = uriComponentsBuilderClassId, + name = "toUriString", + parameters = emptyList(), + returnType = stringClassId + ), + params = emptyList(), + ) + ) + } + } + + // TODO handle multiple annotations on one method + private fun getRequestMappingAnnotationOrNull(methodId: MethodId): Annotation? = + methodId.method.annotations + .firstOrNull { it.annotationClass.id in UtRequestMethod.annotationClassIds } + + // TODO support placeholders (e.g. "/${profile_path}"). + private fun getRequestPathOrNull(requestMappingAnnotation: Annotation): String? = + ((requestMappingAnnotation.annotationClass.java.getMethod("path") + .invoke(requestMappingAnnotation) as Array<*>) + .getOrNull(0) ?: + (requestMappingAnnotation.annotationClass.java.getMethod("value") // TODO separate support for @AliasFor + .invoke(requestMappingAnnotation) as Array<*>) + .getOrNull(0)) as? String // TODO support multiple paths + + private fun getRequestMethodOrNull(requestMappingAnnotation: Annotation): UtRequestMethod? = + UtRequestMethod.values().firstOrNull { requestMappingAnnotation.annotationClass.id == it.annotationClassId } ?: + (requestMappingClassId.jClass.getMethod("method").invoke(requestMappingAnnotation) as Array<*>) + .getOrNull(0)?.let { + UtRequestMethod.valueOf(it.toString()) + } + + private enum class UtRequestMethod { + GET, + HEAD, + POST, + PUT, + PATCH, + DELETE, + OPTIONS, + TRACE; + + val annotationClassId get() = ClassId( + "org.springframework.web.bind.annotation.${name.lowercase().capitalize()}Mapping" + ) + + val requestBuilderMethodId = MethodId( + classId = mockMvcRequestBuildersClassId, + name = name.lowercase(), + returnType = mockHttpServletRequestBuilderClassId, + parameters = listOf(stringClassId, objectArrayClassId) + ) + + companion object { + val annotationClassIds = values().map { it.annotationClassId } + + requestMappingClassId + } + } + + ///endregion +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ThrowableUtils.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ThrowableUtils.kt index 48186cb8f7..30f49c0542 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ThrowableUtils.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/ThrowableUtils.kt @@ -1,10 +1,20 @@ package org.utbot.framework.plugin.api.util +import org.utbot.framework.plugin.api.OverflowDetectionError +import org.utbot.framework.plugin.api.TimeoutException + val Throwable.description get() = message?.replace('\n', '\t') ?: "" val Throwable.isCheckedException get() = !(this is RuntimeException || this is Error) +val Throwable.prettyName + get() = when (this) { + is OverflowDetectionError -> "Overflow" + is TimeoutException -> "Timeout" + else -> this::class.simpleName + } + val Class<*>.isCheckedException get() = !(RuntimeException::class.java.isAssignableFrom(this) || Error::class.java.isAssignableFrom(this)) \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/UtSettingsUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/UtSettingsUtil.kt index 2c3117d81d..a0585ca383 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/UtSettingsUtil.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/UtSettingsUtil.kt @@ -14,4 +14,4 @@ inline fun withStaticsSubstitutionRequired(condition: Boolean, block: () -> } finally { UtSettings.substituteStaticsWithSymbolicVariable = standardSubstitutionSetting } -} \ No newline at end of file +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/util/lambda/LambdaConstructionUtils.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/constructor/LambdaConstructionUtils.kt similarity index 91% rename from utbot-framework/src/main/kotlin/org/utbot/engine/util/lambda/LambdaConstructionUtils.kt rename to utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/constructor/LambdaConstructionUtils.kt index 1f26451bb7..f2e16807f8 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/util/lambda/LambdaConstructionUtils.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/constructor/LambdaConstructionUtils.kt @@ -1,10 +1,13 @@ -package org.utbot.engine.util.lambda +package org.utbot.framework.plugin.api.util.constructor import java.lang.invoke.LambdaMetafactory import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodHandles.Lookup import java.lang.invoke.MethodType +import java.lang.reflect.Field import java.lang.reflect.Method import java.lang.reflect.Modifier +import org.utbot.common.Reflection /** * This class represents the `type` and `value` of a value captured by lambda. @@ -18,17 +21,18 @@ data class CapturedArgument(val type: Class<*>, val value: Any?) * @return [MethodHandles.Lookup] instance for the given [clazz]. * It can be used, for example, to search methods of this [clazz], even the `private` ones. */ -private fun getLookupIn(clazz: Class<*>): MethodHandles.Lookup { +@Suppress("UNCHECKED_CAST") +private fun getLookupIn(clazz: Class<*>): Lookup { val lookup = MethodHandles.lookup().`in`(clazz) // Allow lookup to access all members of declaringClass, including the private ones. // For example, it is useful to access private synthetic methods representing lambdas. - val allowedModes = MethodHandles.Lookup::class.java.getDeclaredField("allowedModes") + val fields = Reflection.getDeclaredFields0Method.invoke(Lookup::class.java, false) as Array + val allowedModes = fields.single { it.name == "allowedModes" } + val allModesField = fields.single { it.name == "ALL_MODES" } allowedModes.isAccessible = true - allowedModes.setInt( - lookup, - Modifier.PUBLIC or Modifier.PROTECTED or Modifier.PRIVATE or Modifier.STATIC - ) + allModesField.isAccessible = true + allowedModes.setInt(lookup, allModesField.get(null) as Int) return lookup } @@ -66,7 +70,7 @@ private fun getLambdaMethod(declaringClass: Class<*>, lambdaName: String): Metho * We obtain this info in [prepareLambdaInfo] to avoid duplicated code in [constructLambda] and [constructStaticLambda]. */ private data class LambdaMetafactoryInfo( - val caller: MethodHandles.Lookup, + val caller: Lookup, val invokedName: String, val samMethodType: MethodType, val lambdaMethod: Method, @@ -115,7 +119,7 @@ private fun getSingleAbstractMethod(clazz: Class<*>): Method { * @return an [Any] that represents an instance of the given functional interface `samType` * and implements its single abstract method with the behavior of the given lambda. */ -internal fun constructStaticLambda( +fun constructStaticLambda( samType: Class<*>, declaringClass: Class<*>, lambdaName: String, @@ -151,7 +155,7 @@ internal fun constructStaticLambda( * @return an [Any] that represents an instance of the given functional interface `samType` * and implements its single abstract method with the behavior of the given lambda. */ -internal fun constructLambda( +fun constructLambda( samType: Class<*>, declaringClass: Class<*>, lambdaName: String, diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/constructor/ValueConstructor.kt similarity index 89% rename from utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt rename to utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/constructor/ValueConstructor.kt index ff436d10ff..c2fa773df7 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/constructor/ValueConstructor.kt @@ -1,11 +1,11 @@ -package org.utbot.engine +package org.utbot.framework.plugin.api.util.constructor +import org.utbot.common.Reflection import org.utbot.common.invokeCatching +import org.utbot.common.withAccessibility import org.utbot.framework.plugin.api.ClassId -import org.utbot.engine.util.lambda.CapturedArgument -import org.utbot.engine.util.lambda.constructLambda -import org.utbot.engine.util.lambda.constructStaticLambda import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.DirectFieldAccessId import org.utbot.framework.plugin.api.EnvironmentModels import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.FieldMockTarget @@ -20,7 +20,9 @@ import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtClassRefModel import org.utbot.framework.plugin.api.UtCompositeModel import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.UtCustomModel import org.utbot.framework.plugin.api.UtDirectSetFieldModel +import org.utbot.framework.plugin.api.UtDirectGetFieldModel import org.utbot.framework.plugin.api.UtEnumConstantModel import org.utbot.framework.plugin.api.UtExecutableCallModel import org.utbot.framework.plugin.api.UtExecution @@ -33,20 +35,19 @@ 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.UtReferenceModel +import org.utbot.framework.plugin.api.UtStatementCallModel import org.utbot.framework.plugin.api.UtSymbolicExecution import org.utbot.framework.plugin.api.UtValueExecution import org.utbot.framework.plugin.api.UtValueExecutionState import org.utbot.framework.plugin.api.UtVoidModel import org.utbot.framework.plugin.api.isMockModel +import org.utbot.framework.plugin.api.util.anyInstance import org.utbot.framework.plugin.api.util.constructor import org.utbot.framework.plugin.api.util.isStatic -import org.utbot.framework.plugin.api.util.jField import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.jField import org.utbot.framework.plugin.api.util.method import org.utbot.framework.plugin.api.util.utContext -import org.utbot.framework.util.anyInstance -import org.utbot.instrumentation.process.runSandbox -import java.lang.reflect.Field import java.lang.reflect.Modifier import kotlin.reflect.KClass @@ -187,12 +188,18 @@ class ValueConstructor { is UtNullModel -> UtConcreteValue(null, model.classId.jClass) is UtPrimitiveModel -> UtConcreteValue(model.value, model.classId.jClass) is UtEnumConstantModel -> UtConcreteValue(model.value) - is UtClassRefModel -> UtConcreteValue(model.value) + is UtClassRefModel -> UtConcreteValue(model.value.jClass) is UtCompositeModel -> UtConcreteValue(constructObject(model), model.classId.jClass) is UtArrayModel -> UtConcreteValue(constructArray(model)) is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model)) is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) is UtVoidModel -> UtConcreteValue(Unit) + is UtCustomModel -> UtConcreteValue( + constructObject(model.origin ?: error("Can't construct value for custom model without origin [$model]")), + model.classId.jClass + ) + // Python, JavaScript are supposed to be here as well + else -> throw UnsupportedOperationException("UtModel $model cannot construct UtConcreteValue") } } @@ -237,8 +244,7 @@ class ValueConstructor { try { declaredField.isAccessible = true - val modifiersField = Field::class.java.getDeclaredField("modifiers") - modifiersField.isAccessible = true + check(Reflection.isModifiersAccessible()) val target = mockTarget(fieldModel) { FieldMockTarget( @@ -329,15 +335,15 @@ class ValueConstructor { constructedObjects[assembleModel]?.let { return it } val instantiationExecutableCall = assembleModel.instantiationCall - val result = updateWithExecutableCallModel(instantiationExecutableCall) + val result = updateWithStatementCallModel(instantiationExecutableCall) checkNotNull(result) { - "Tracked instance can't be null for call ${instantiationExecutableCall.executable} in model $assembleModel" + "Tracked instance can't be null for call ${instantiationExecutableCall.statement} in model $assembleModel" } constructedObjects[assembleModel] = result assembleModel.modificationsChain.forEach { statementModel -> when (statementModel) { - is UtExecutableCallModel -> updateWithExecutableCallModel(statementModel) + is UtStatementCallModel -> updateWithStatementCallModel(statementModel) is UtDirectSetFieldModel -> updateWithDirectSetFieldModel(statementModel) } } @@ -376,19 +382,25 @@ class ValueConstructor { * * @return the result of [callModel] invocation */ - private fun updateWithExecutableCallModel( - callModel: UtExecutableCallModel, - ): Any? { - val executable = callModel.executable - val instanceValue = callModel.instance?.let { value(it) } - val params = callModel.params.map { value(it) } - - val result = when (executable) { - is MethodId -> executable.call(params, instanceValue) - is ConstructorId -> executable.call(params) - } + private fun updateWithStatementCallModel(callModel: UtStatementCallModel, ): Any? { + when (callModel) { + is UtExecutableCallModel -> { + val executable = callModel.executable + val instanceValue = callModel.instance?.let { value(it) } + val params = callModel.params.map { value(it) } + + return when (executable) { + is MethodId -> executable.call(params, instanceValue) + is ConstructorId -> executable.call(params) + } + } + is UtDirectGetFieldModel -> { + val fieldAccess = callModel.fieldAccess + val instanceValue = value(callModel.instance) - return result + return fieldAccess.get(instanceValue) + } + } } /** @@ -433,15 +445,22 @@ class ValueConstructor { private fun value(model: UtModel) = construct(model, null).value private fun MethodId.call(args: List, instance: Any?): Any? = - method.runSandbox { - invokeCatching(obj = instance, args = args).getOrThrow() + method.withAccessibility { + method.invokeCatching(obj = instance, args = args).getOrThrow() } private fun ConstructorId.call(args: List): Any? = - constructor.runSandbox { + constructor.withAccessibility { newInstance(*args.toTypedArray()) } + private fun DirectFieldAccessId.get(instance: Any?): Any { + val field = fieldId.jField + return field.withAccessibility { + field.get(instance) + } + } + /** * Fetches primitive value from NutsModel to create array of primitives. */ diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/visible/UtStreamConsumingException.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/visible/UtStreamConsumingException.kt new file mode 100644 index 0000000000..522a73b88f --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/visible/UtStreamConsumingException.kt @@ -0,0 +1,18 @@ +package org.utbot.framework.plugin.api.visible + +/** + * An artificial exception that stores an exception that would be thrown in case of consuming stream by invoking terminal operations. + * [innerException] stores this possible exception (null if [UtStreamConsumingException] was constructed by the engine). + * + * NOTE: this class should be visible in almost all parts of the tool - Soot, engine, concrete execution and code generation, + * that's the reason why this class is placed in this module and this package is called `visible`. + */ +data class UtStreamConsumingException(private val innerException: Exception?) : RuntimeException() { + /** + * Returns the original exception [innerException] if possible, and any [RuntimeException] otherwise. + */ + val innerExceptionOrAny: Throwable + get() = innerException ?: RuntimeException("Unknown runtime exception during consuming stream") + + override fun toString(): String = innerExceptionOrAny.toString() +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/services/JdkInfoService.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/services/JdkInfoService.kt index fd4d27bd9b..08d18c1034 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/services/JdkInfoService.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/services/JdkInfoService.kt @@ -11,7 +11,7 @@ data class JdkInfo( /** * Singleton to enable abstract access to path to JDK. - * Used in [org.utbot.instrumentation.process.ChildProcessRunner]. + * Used in [org.utbot.framework.process.AbstractRDProcessCompanion]. * The purpose is to use the same JDK in [org.utbot.instrumentation.ConcreteExecutor] and in the test runs. * This is necessary because the engine can be run from the various starting points, like IDEA plugin, CLI, etc. */ diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/services/WorkingDirService.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/services/WorkingDirService.kt index a714fa64b4..e448a21a76 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/services/WorkingDirService.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/services/WorkingDirService.kt @@ -6,7 +6,7 @@ import java.nio.file.Paths /** * Singleton to enable abstract access to the working directory. * - * Used in [org.utbot.instrumentation.process.ChildProcessRunner]. + * Used in [org.utbot.instrumentation.rd.InstrumentedProcess]. * The purpose is to use the same working directory in [org.utbot.instrumentation.ConcreteExecutor] * and in the test runs. */ diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/AbstractRDProcessCompanion.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/AbstractRDProcessCompanion.kt new file mode 100644 index 0000000000..41b0499cae --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/AbstractRDProcessCompanion.kt @@ -0,0 +1,37 @@ +package org.utbot.framework.process + +import org.utbot.common.osSpecificJavaExecutable +import org.utbot.framework.plugin.services.JdkInfoService +import org.utbot.rd.rdPortArgument +import java.io.File +import kotlin.io.path.pathString + +abstract class AbstractRDProcessCompanion( + private val debugPort: Int, + private val runWithDebug: Boolean, + private val suspendExecutionInDebugMode: Boolean, + private val processSpecificCommandLineArgs: () -> List +) { + private val javaExecutablePathString get() = + JdkInfoService.provide().path.resolve("bin${File.separatorChar}${osSpecificJavaExecutable()}") + + protected fun obtainProcessCommandLine(port: Int): List = buildList { + addAll(obtainCommonProcessCommandLineArgs()) + addAll(processSpecificCommandLineArgs()) + add(rdPortArgument(port)) + } + + private fun obtainCommonProcessCommandLineArgs(): List = buildList { + val suspendValue = if (suspendExecutionInDebugMode) "y" else "n" + val debugArgument = + "-agentlib:jdwp=transport=dt_socket,server=n,suspend=${suspendValue},quiet=y,address=$debugPort" + .takeIf { runWithDebug } + + add(javaExecutablePathString.pathString) + val javaVersionSpecificArgs = OpenModulesContainer.javaVersionSpecificArguments + if (javaVersionSpecificArgs.isNotEmpty()) { + addAll(javaVersionSpecificArgs) + } + debugArgument?.let { add(it) } + } +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/OpenModulesContainer.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/OpenModulesContainer.kt index fb53e2fd10..2d95c4669c 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/OpenModulesContainer.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/OpenModulesContainer.kt @@ -6,15 +6,61 @@ object OpenModulesContainer { private val modulesContainer: List val javaVersionSpecificArguments: List get() = modulesContainer - .takeIf { JdkInfoService.provide().version > 8 } - ?: emptyList() + .takeIf { JdkInfoService.provide().version > 8 } ?: emptyList() init { modulesContainer = buildList { + openPackage("java.base", "java.util.concurrent.atomic") + openPackage("java.base", "sun.security.util") + openPackage("java.base", "sun.reflect.annotation") + openPackage("java.base", "java.time") + openPackage("java.base", "java.text") + openPackage("java.base", "java.lang.invoke") openPackage("java.base", "jdk.internal.misc") + openPackage("java.base", "sun.reflect.generics.repository") + openPackage("java.base", "java.io") + openPackage("java.base", "java.nio") + openPackage("java.base", "java.nio.file") + openPackage("java.base", "java.net") openPackage("java.base", "java.lang") + openPackage("java.base", "java.security") + openPackage("java.base", "java.util") + openPackage("java.base", "java.util.stream") + openPackage("java.base", "java.util.concurrent") + openPackage("java.base", "java.util.concurrent.locks") + openPackage("java.base", "java.math") + openPackage("java.base", "java.lang.ref") openPackage("java.base", "java.lang.reflect") openPackage("java.base", "sun.security.provider") + openPackage("java.base", "sun.net.util") + openPackage("java.base", "sun.net.fs") + openPackage("java.base", "jdk.internal.event") + openPackage("java.base", "jdk.internal.jimage") + openPackage("java.base", "jdk.internal.jimage.decompressor") + openPackage("java.base", "jdk.internal.jmod") + openPackage("java.base", "jdk.internal.jtrfs") + openPackage("java.base", "jdk.internal.loader") + openPackage("java.base", "jdk.internal.logger") + openPackage("java.base", "jdk.internal.math") + openPackage("java.base", "jdk.internal.misc") + openPackage("java.base", "jdk.internal.module") + openPackage("java.base", "jdk.internal.org.objectweb.asm.commons") + openPackage("java.base", "jdk.internal.org.objectweb.asm.signature") + openPackage("java.base", "jdk.internal.org.objectweb.asm.tree") + openPackage("java.base", "jdk.internal.org.objectweb.asm.tree.analysis") + openPackage("java.base", "jdk.internal.org.objectweb.asm.util") + openPackage("java.base", "jdk.internal.org.xml.sax") + openPackage("java.base", "jdk.internal.org.xml.sax.helpers") + openPackage("java.base", "jdk.internal.perf") + openPackage("java.base", "jdk.internal.platform") + openPackage("java.base", "jdk.internal.ref") + openPackage("java.base", "jdk.internal.reflect") + openPackage("java.base", "jdk.internal.util") + openPackage("java.base", "jdk.internal.util.jar") + openPackage("java.base", "jdk.internal.util.xml") + openPackage("java.base", "jdk.internal.util.xml.impl") + openPackage("java.base", "jdk.internal.vm") + openPackage("java.base", "jdk.internal.vm.annotation") add("--illegal-access=warn") } } diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/KryoHelper.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/KryoHelper.kt new file mode 100644 index 0000000000..3fb47e2558 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/KryoHelper.kt @@ -0,0 +1,121 @@ +package org.utbot.framework.process.kryo + +import com.esotericsoftware.kryo.kryo5.Kryo +import com.esotericsoftware.kryo.kryo5.SerializerFactory +import com.esotericsoftware.kryo.kryo5.io.Input +import com.esotericsoftware.kryo.kryo5.io.Output +import com.esotericsoftware.kryo.kryo5.objenesis.instantiator.ObjectInstantiator +import com.esotericsoftware.kryo.kryo5.objenesis.strategy.StdInstantiatorStrategy +import com.esotericsoftware.kryo.kryo5.serializers.JavaSerializer +import com.esotericsoftware.kryo.kryo5.util.DefaultInstantiatorStrategy +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.throwIfNotAlive +import java.io.ByteArrayOutputStream +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * Helpful class for working with the kryo. + */ +class KryoHelper constructor( + private val lifetime: Lifetime +) { + private val outputBuffer = ByteArrayOutputStream() + private val kryoOutput = Output(outputBuffer) + private val kryoInput= Input() + private val sendKryo: Kryo = TunedKryo() + private val receiveKryo: Kryo = TunedKryo() + private val myLockObject = ReentrantLock() + + init { + sendKryo.setAutoReset(true) + receiveKryo.setAutoReset(true) + lifetime.onTermination { + kryoInput.close() + kryoOutput.close() + } + } + + fun setKryoClassLoader(classLoader: ClassLoader) { + sendKryo.classLoader = classLoader + receiveKryo.classLoader = classLoader + } + + /** + * Serializes object to ByteArray + * + * @throws WritingToKryoException wraps all exceptions + */ + fun writeObject(obj: T): ByteArray = myLockObject.withLock { + lifetime.throwIfNotAlive() + try { + sendKryo.writeClassAndObject(kryoOutput, obj) + kryoOutput.flush() + + return outputBuffer.toByteArray() + } catch (e: Exception) { + throw WritingToKryoException(e) + } finally { + kryoOutput.reset() + outputBuffer.reset() + } + } + + /** + * Deserializes object form ByteArray + * + * @throws ReadingFromKryoException wraps all exceptions + */ + fun readObject(byteArray: ByteArray): T = myLockObject.withLock { + lifetime.throwIfNotAlive() + return try { + kryoInput.buffer = byteArray + receiveKryo.readClassAndObject(kryoInput) as T + } catch (e: Exception) { + throw ReadingFromKryoException(e) + } + finally { + receiveKryo.reset() + } + } +} + +// This kryo is used to initialize collections properly. +internal class TunedKryo : Kryo() { + init { + this.references = true + this.isRegistrationRequired = false + + this.instantiatorStrategy = object : StdInstantiatorStrategy() { + // workaround for Collections as they cannot be correctly deserialized without calling constructor + val default = DefaultInstantiatorStrategy() + val classesBadlyDeserialized = listOf( + java.util.Queue::class.java, + java.util.HashSet::class.java + ) + + override fun newInstantiatorOf(type: Class): ObjectInstantiator { + return if (classesBadlyDeserialized.any { it.isAssignableFrom(type) }) { + @Suppress("UNCHECKED_CAST") + default.newInstantiatorOf(type) as ObjectInstantiator + } else { + super.newInstantiatorOf(type) + } + } + } + + this.setOptimizedGenerics(false) + + // Kryo cannot (at least, the current used version) deserialize stacktraces that are required for SARIF reports. + // TODO: JIRA:1492 + addDefaultSerializer(java.lang.Throwable::class.java, ThrowableSerializer()) + + addDefaultSerializer(java.lang.StackTraceElement::class.java, JavaSerializer()) + + val factory = object : SerializerFactory.FieldSerializerFactory() {} + factory.config.ignoreSyntheticFields = true + factory.config.serializeTransient = false + factory.config.fieldsCanBeNull = true + this.setDefaultSerializer(factory) + } +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ProcessCommunicationException.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ProcessCommunicationException.kt new file mode 100644 index 0000000000..53dc6bd623 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ProcessCommunicationException.kt @@ -0,0 +1,9 @@ +package org.utbot.framework.process.kryo + +open class ProcessCommunicationException(msg: String?, cause: Throwable? = null) : Exception(msg, cause) + +class ReadingFromKryoException(e: Throwable) : + ProcessCommunicationException("Reading from Kryo exception |> ${e.stackTraceToString()}", e) + +class WritingToKryoException(e: Throwable) : + ProcessCommunicationException("Writing to Kryo exception |> ${e.stackTraceToString()}", e) \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ThrowableSerializer.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ThrowableSerializer.kt new file mode 100644 index 0000000000..821ed28563 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ThrowableSerializer.kt @@ -0,0 +1,144 @@ +package org.utbot.framework.process.kryo + +import com.esotericsoftware.kryo.kryo5.Kryo +import com.esotericsoftware.kryo.kryo5.KryoException +import com.esotericsoftware.kryo.kryo5.Serializer +import com.esotericsoftware.kryo.kryo5.io.Input +import com.esotericsoftware.kryo.kryo5.io.Output +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.warn +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jClass +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.ObjectStreamClass + +class ThrowableSerializer : Serializer() { + companion object { + private val loggedUnserializableExceptionClassIds = mutableSetOf() + private val logger = getLogger() + } + + private class ThrowableModel( + val classId: ClassId, + val message: String?, + val stackTrace: Array, + val cause: ThrowableModel?, + val serializedExceptionBytes: ByteArray?, + ) + + override fun write(kryo: Kryo, output: Output, throwable: Throwable?) { + fun Throwable.toModel(): ThrowableModel = ThrowableModel( + classId = this::class.java.id, + message = message, + stackTrace = stackTrace, + cause = cause?.toModel(), + serializedExceptionBytes = try { + ByteArrayOutputStream().use { byteOutputStream -> + val objectOutputStream = ObjectOutputStream(byteOutputStream) + objectOutputStream.writeObject(this) + objectOutputStream.flush() + byteOutputStream.toByteArray() + } + } catch (e: Throwable) { + if (loggedUnserializableExceptionClassIds.add(this::class.java.id)) { + logger.warn { "Failed to serialize ${this::class.java.id} to bytes, cause: $e" } + logger.warn { "Constructing ThrowableModel with serializedExceptionBytes = null" } + } + null + } + ) + kryo.writeObject(output, throwable?.toModel()) + } + + override fun read(kryo: Kryo, input: Input, type: Class): Throwable? { + fun ThrowableModel.toThrowable(): Throwable { + this.serializedExceptionBytes?.let { bytes -> + try { + return@toThrowable ByteArrayInputStream(bytes).use { byteInputStream -> + val objectInputStream = IgnoringUidWrappingObjectInputStream(byteInputStream, kryo.classLoader) + objectInputStream.readObject() as Throwable + } + } catch (e: Throwable) { + if (loggedUnserializableExceptionClassIds.add(this.classId)) { + logger.warn { "Failed to deserialize ${this.classId} from bytes, cause: $e" } + logger.warn { "Falling back to constructing throwable instance from ThrowableModel" } + } + } + } + + val cause = cause?.toThrowable() + + val messageCauseConstructor = runCatching { classId.jClass.getConstructor(String::class.java, Throwable::class.java) }.getOrNull() + val causeOnlyConstructor = runCatching { classId.jClass.getConstructor(Throwable::class.java) }.getOrNull() + val messageOnlyConstructor = runCatching { classId.jClass.getConstructor(String::class.java) }.getOrNull() + + val throwableFromConstructor = runCatching { + when { + messageCauseConstructor != null && message != null && cause != null -> + messageCauseConstructor.newInstance(message, cause) + + causeOnlyConstructor != null && cause != null -> causeOnlyConstructor.newInstance(cause) + messageOnlyConstructor != null && message != null -> messageOnlyConstructor.newInstance(message) + else -> null + } + }.getOrNull() as Throwable? + + return (throwableFromConstructor ?: when { + RuntimeException::class.java.isAssignableFrom(classId.jClass) -> RuntimeException(message, cause) + Error::class.java.isAssignableFrom(classId.jClass) -> Error(message, cause) + else -> Exception(message, cause) + }).also { + it.stackTrace = stackTrace + } + } + + return kryo.readObject(input, ThrowableModel::class.java)?.toThrowable() + } +} + +class IgnoringUidWrappingObjectInputStream(iss : InputStream?, private val classLoader: ClassLoader) : ObjectInputStream(iss) { + override fun resolveClass(type: ObjectStreamClass): Class<*>? { + return try { + Class.forName(type.name, false, classLoader) + } catch (ex: ClassNotFoundException) { + try { + return Kryo::class.java.classLoader.loadClass(type.name) + } catch (e: ClassNotFoundException) { + try { + return super.resolveClass(type); + } catch (e: ClassNotFoundException) { + throw KryoException("Class not found: " + type.name, e) + } + } + } + } + + // This overriding allows to ignore serialVersionUID during deserialization. + // For more info, see https://stackoverflow.com/a/1816711 + override fun readClassDescriptor(): ObjectStreamClass { + var resultClassDescriptor = super.readClassDescriptor() // initially streams descriptor + + // the class in the local JVM that this descriptor represents. + val localClass: Class<*> = try { + classLoader.loadClass(resultClassDescriptor.name) + } catch (e: ClassNotFoundException) { + return resultClassDescriptor + } + + val localClassDescriptor = ObjectStreamClass.lookup(localClass) ?: return resultClassDescriptor + + // only if class implements serializable + val localSUID = localClassDescriptor.serialVersionUID + val streamSUID = resultClassDescriptor.serialVersionUID + if (streamSUID != localSUID) { // check for serialVersionUID mismatch. + resultClassDescriptor = localClassDescriptor // Use local class descriptor for deserialization + } + + return resultClassDescriptor + } +} diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/util/SootUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/util/SootUtil.kt new file mode 100644 index 0000000000..9817e5f921 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/util/SootUtil.kt @@ -0,0 +1,25 @@ +package org.utbot.framework.util + +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.classId +import org.utbot.framework.plugin.api.id +import org.utbot.framework.plugin.api.util.constructorId +import org.utbot.framework.plugin.api.util.methodId +import soot.SootMethod + +/** + * Gets method or constructor id of SootMethod. + */ +val SootMethod.executableId: ExecutableId + get() = when { + isConstructor -> constructorId( + classId = declaringClass.id, + arguments = parameterTypes.map { it.classId }.toTypedArray() + ) + else -> methodId( + classId = declaringClass.id, + name = name, + returnType = returnType.classId, + arguments = parameterTypes.map { it.classId }.toTypedArray() + ) + } diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/utils/UtilMethodProvider.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/utils/UtilMethodProvider.kt new file mode 100644 index 0000000000..dbe881be8f --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/utils/UtilMethodProvider.kt @@ -0,0 +1,258 @@ +package org.utbot.framework.utils + +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.BuiltinConstructorId +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.CgClassId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.util.arrayTypeOf +import org.utbot.framework.plugin.api.util.baseStreamClassId +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.builtinConstructorId +import org.utbot.framework.plugin.api.util.classClassId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.objectArrayClassId +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.invoke.MethodHandles +import java.lang.invoke.MethodType +import java.lang.reflect.Method + +/** + * Set of ids of all possible util methods for a given class. + * + * The class may actually not have some of these methods if they + * are not required in the process of code generation (this is the case for [TestClassUtilMethodProvider]). + */ +abstract class UtilMethodProvider(val utilClassId: ClassId) { + val utilMethodIds: Set + get() = setOf( + getUnsafeInstanceMethodId, + createInstanceMethodId, + createArrayMethodId, + setFieldMethodId, + setStaticFieldMethodId, + getFieldValueMethodId, + getStaticFieldValueMethodId, + getEnumConstantByNameMethodId, + deepEqualsMethodId, + arraysDeepEqualsMethodId, + iterablesDeepEqualsMethodId, + streamsDeepEqualsMethodId, + mapsDeepEqualsMethodId, + hasCustomEqualsMethodId, + getArrayLengthMethodId, + consumeBaseStreamMethodId, + buildStaticLambdaMethodId, + buildLambdaMethodId, + getLookupInMethodId, + getLambdaCapturedArgumentTypesMethodId, + getLambdaCapturedArgumentValuesMethodId, + getInstantiatedMethodTypeMethodId, + getLambdaMethodMethodId, + getSingleAbstractMethodMethodId + ) + + val getUnsafeInstanceMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getUnsafeInstance", + returnType = Unsafe::class.id, + ) + + /** + * Method that creates instance using Unsafe + */ + val createInstanceMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "createInstance", + returnType = CgClassId(objectClassId, isNullable = true), + arguments = arrayOf(stringClassId) + ) + + val createArrayMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "createArray", + returnType = Array::class.id, + arguments = arrayOf(stringClassId, intClassId, Array::class.id) + ) + + val setFieldMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "setField", + returnType = voidClassId, + arguments = arrayOf(objectClassId, stringClassId, stringClassId, objectClassId) + ) + + val setStaticFieldMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "setStaticField", + returnType = voidClassId, + arguments = arrayOf(Class::class.id, stringClassId, objectClassId) + ) + + val getFieldValueMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getFieldValue", + returnType = objectClassId, + arguments = arrayOf(objectClassId, stringClassId, stringClassId) + ) + + val getStaticFieldValueMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getStaticFieldValue", + returnType = objectClassId, + arguments = arrayOf(Class::class.id, stringClassId) + ) + + val getEnumConstantByNameMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getEnumConstantByName", + returnType = objectClassId, + arguments = arrayOf(Class::class.id, stringClassId) + ) + + val deepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "deepEquals", + returnType = booleanClassId, + arguments = arrayOf(objectClassId, objectClassId) + ) + + val arraysDeepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "arraysDeepEquals", + returnType = booleanClassId, + arguments = arrayOf(objectClassId, objectClassId) + ) + + val iterablesDeepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "iterablesDeepEquals", + returnType = booleanClassId, + arguments = arrayOf(java.lang.Iterable::class.id, java.lang.Iterable::class.id) + ) + + val streamsDeepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "streamsDeepEquals", + returnType = booleanClassId, + arguments = arrayOf(java.util.stream.BaseStream::class.id, java.util.stream.BaseStream::class.id) + ) + + val mapsDeepEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "mapsDeepEquals", + returnType = booleanClassId, + arguments = arrayOf(java.util.Map::class.id, java.util.Map::class.id) + ) + + val hasCustomEqualsMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "hasCustomEquals", + returnType = booleanClassId, + arguments = arrayOf(Class::class.id) + ) + + val getArrayLengthMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getArrayLength", + returnType = intClassId, + arguments = arrayOf(objectClassId) + ) + + val consumeBaseStreamMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "consumeBaseStream", + returnType = voidClassId, + arguments = arrayOf(baseStreamClassId) + ) + + val buildStaticLambdaMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "buildStaticLambda", + returnType = objectClassId, + arguments = arrayOf( + classClassId, + classClassId, + stringClassId, + arrayTypeOf(capturedArgumentClassId) + ) + ) + + val buildLambdaMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "buildLambda", + returnType = objectClassId, + arguments = arrayOf( + classClassId, + classClassId, + stringClassId, + objectClassId, + arrayTypeOf(capturedArgumentClassId) + ) + ) + + val getLookupInMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getLookupIn", + returnType = MethodHandles.Lookup::class.id, + arguments = arrayOf(classClassId) + ) + + val getLambdaCapturedArgumentTypesMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getLambdaCapturedArgumentTypes", + returnType = arrayTypeOf(classClassId), + arguments = arrayOf(arrayTypeOf(capturedArgumentClassId)) + ) + + val getLambdaCapturedArgumentValuesMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getLambdaCapturedArgumentValues", + returnType = objectArrayClassId, + arguments = arrayOf(arrayTypeOf(capturedArgumentClassId)) + ) + + val getInstantiatedMethodTypeMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getInstantiatedMethodType", + returnType = MethodType::class.id, + arguments = arrayOf(Method::class.id, arrayTypeOf(classClassId)) + ) + + val getLambdaMethodMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getLambdaMethod", + returnType = Method::class.id, + arguments = arrayOf(classClassId, stringClassId) + ) + + val getSingleAbstractMethodMethodId: MethodId + get() = utilClassId.utilMethodId( + name = "getSingleAbstractMethod", + returnType = java.lang.reflect.Method::class.id, + arguments = arrayOf(classClassId) + ) + + val capturedArgumentClassId: BuiltinClassId + get() = BuiltinClassId( + canonicalName = "${utilClassId.name}.CapturedArgument", + simpleName = "CapturedArgument" + ) + + val capturedArgumentConstructorId: BuiltinConstructorId + get() = builtinConstructorId(capturedArgumentClassId, classClassId, objectClassId) +} + +internal fun ClassId.utilMethodId( + name: String, + returnType: ClassId, + vararg arguments: ClassId, + // usually util methods are static, so this argument is true by default + isStatic: Boolean = true +): MethodId = + BuiltinMethodId(this, name, returnType, arguments.toList(), isStatic = isStatic) diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/testcheckers/SettingsModificators.kt b/utbot-framework-api/src/main/kotlin/org/utbot/testcheckers/SettingsModificators.kt index 7b2223947b..ae571e273b 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/testcheckers/SettingsModificators.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/testcheckers/SettingsModificators.kt @@ -59,6 +59,16 @@ inline fun withTreatingOverflowAsError(block: () -> T): T { } } +inline fun withoutThrowTaintErrorForEachMarkSeparately(block: () -> T): T { + val prev = UtSettings.throwTaintErrorForEachMarkSeparately + UtSettings.throwTaintErrorForEachMarkSeparately = false + try { + return block() + } finally { + UtSettings.throwTaintErrorForEachMarkSeparately = prev + } +} + inline fun withPushingStateFromPathSelectorForConcrete(block: () -> T): T { val prev = UtSettings.saveRemainingStatesForConcreteExecution UtSettings.saveRemainingStatesForConcreteExecution = true @@ -109,6 +119,27 @@ inline fun withoutConcrete(block: () -> T): T { } } +inline fun withProcessingClinitSections(value: Boolean, block: () -> T): T { + val prev = UtSettings.enableClinitSectionsAnalysis + UtSettings.enableClinitSectionsAnalysis = value + try { + return block() + } finally { + UtSettings.enableClinitSectionsAnalysis = prev + } +} + +inline fun withProcessingAllClinitSectionsConcretely(value: Boolean, block: () -> T): T { + val prev = UtSettings.processAllClinitSectionsConcretely + UtSettings.processAllClinitSectionsConcretely = value + try { + return block() + } finally { + UtSettings.processAllClinitSectionsConcretely = prev + } +} + + /** * Run [block] with disabled sandbox in the concrete executor */ diff --git a/utbot-framework-api/src/test/kotlin/org/utbot/framework/UtSettingsTest.kt b/utbot-framework-api/src/test/kotlin/org/utbot/framework/UtSettingsTest.kt new file mode 100644 index 0000000000..a05c3cab21 --- /dev/null +++ b/utbot-framework-api/src/test/kotlin/org/utbot/framework/UtSettingsTest.kt @@ -0,0 +1,24 @@ +package org.utbot.framework + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals + +internal class UtSettingsTest { + @Test + fun testMaxTestFileSize() { + assertEquals(1, UtSettings.parseFileSize("1")) + assertEquals(12, UtSettings.parseFileSize("12")) + assertEquals(123, UtSettings.parseFileSize("123")) + assertEquals(30000, UtSettings.parseFileSize("30K")) + assertEquals(30000, UtSettings.parseFileSize("30Kb")) + assertEquals(30000, UtSettings.parseFileSize("30kB")) + assertEquals(30000, UtSettings.parseFileSize("30kb")) + assertEquals(7000000, UtSettings.parseFileSize("7MB")) + assertEquals(7000000, UtSettings.parseFileSize("7Mb")) + assertEquals(7000000, UtSettings.parseFileSize("7M")) + assertEquals(7, UtSettings.parseFileSize("7abc")) + assertEquals(UtSettings.DEFAULT_MAX_FILE_SIZE, UtSettings.parseFileSize("qwerty")) + assertEquals(UtSettings.DEFAULT_MAX_FILE_SIZE, UtSettings.parseFileSize("MB")) + assertEquals(UtSettings.DEFAULT_MAX_FILE_SIZE, UtSettings.parseFileSize("1000000")) + } +} \ No newline at end of file diff --git a/utbot-framework-test/build.gradle b/utbot-framework-test/build.gradle index 23676e0c38..5c4bc02926 100644 --- a/utbot-framework-test/build.gradle +++ b/utbot-framework-test/build.gradle @@ -1,29 +1,8 @@ -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11 - freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] - } -} - -tasks.withType(JavaCompile) { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_11 -} - -repositories { - flatDir { - dirs 'dist' - } -} - -configurations { - z3native -} - dependencies { api project(':utbot-framework-api') + testImplementation project(':utbot-testing') - api project(':utbot-fuzzers') + api project(':utbot-java-fuzzing') api project(':utbot-instrumentation') api project(':utbot-summary') @@ -32,17 +11,19 @@ dependencies { testImplementation project(":utbot-framework").sourceSets.test.output testImplementation project(":utbot-core").sourceSets.test.output - implementation "com.github.UnitTestBot:soot:${sootCommitHash}" + implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { + exclude group:'com.google.guava', module:'guava' + } + implementation group: 'com.charleskorn.kaml', name: 'kaml', version: kamlVersion implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: jacksonVersion - implementation group: 'org.sosy-lab', name: 'javasmt-solver-z3', version: javasmtSolverZ3Version implementation group: 'com.github.curious-odd-man', name: 'rgxgen', version: rgxgenVersion implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: log4j2Version implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion implementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion implementation group: 'org.apache.commons', name: 'commons-text', version: apacheCommonsTextVersion // we need this for construction mocks from composite models - implementation group: 'org.mockito', name: 'mockito-core', version: '4.2.0' + implementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion // 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' @@ -55,32 +36,21 @@ dependencies { testImplementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4PlatformVersion testImplementation group: 'org.antlr', name: 'antlr4', version: antlrVersion testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion - testImplementation group: 'org.testng', name: 'testng', version: testNgVersion testImplementation group: 'org.mockito', name: 'mockito-inline', version: mockitoInlineVersion testImplementation group: 'com.google.guava', name: 'guava', version: guavaVersion testImplementation group: 'org.mockito', name: 'mockito-inline', version: mockitoInlineVersion testImplementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version - z3native group: 'com.microsoft.z3', name: 'z3-native-win64', version: z3Version, ext: 'zip' - z3native group: 'com.microsoft.z3', name: 'z3-native-linux64', version: z3Version, ext: 'zip' - z3native group: 'com.microsoft.z3', name: 'z3-native-osx', version: z3Version, ext: 'zip' + // TODO sbft-usvm-merge: UtBot engine expects `com.github.UnitTestBot.ksmt` here + implementation group: 'io.ksmt', name: 'ksmt-core', version: ksmtVersion + implementation group: 'io.ksmt', name: 'ksmt-z3', version: ksmtVersion } - +// This is required to avoid conflict between SpringBoot standard logger and the logger of our project. +// See https://stackoverflow.com/a/28735604 for more details. test { - - minHeapSize = "128m" - maxHeapSize = "2048m" - - jvmArgs '-XX:MaxHeapSize=2048m' - - // To use JUnit4, comment out useJUnitPlatform and uncomment useJUnit. Please also check "dependencies" section - //useJUnit() - useJUnitPlatform() { - excludeTags 'slow', 'IntegrationTest' - } if (System.getProperty('DEBUG', 'false') == 'true') { - jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9009' + jvmArgs '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9009' } } diff --git a/utbot-framework-test/src/main/java/org/utbot/examples/manual/examples/Trivial.java b/utbot-framework-test/src/main/java/org/utbot/examples/manual/examples/Trivial.java index 7bc2779d50..d5aa823325 100644 --- a/utbot-framework-test/src/main/java/org/utbot/examples/manual/examples/Trivial.java +++ b/utbot-framework-test/src/main/java/org/utbot/examples/manual/examples/Trivial.java @@ -2,6 +2,10 @@ public class Trivial { public int aMethod(int a) { - return a; + String s = "a"; + if (a > 1) { + return s.length(); + } + return s.length() + 1; } } diff --git a/utbot-framework-test/src/main/java/org/utbot/examples/strings11/StringConcat.java b/utbot-framework-test/src/main/java/org/utbot/examples/strings11/StringConcat.java new file mode 100644 index 0000000000..78552f701d --- /dev/null +++ b/utbot-framework-test/src/main/java/org/utbot/examples/strings11/StringConcat.java @@ -0,0 +1,80 @@ +package org.utbot.examples.strings11; + +import org.utbot.api.mock.UtMock; + + +public class StringConcat { + public static class Test { + public int x; + + @Override + public String toString() { + if (x == 42) { + throw new IllegalArgumentException(); + } + return "x = " + x; + } + } + + String str; + public String concatArguments(String a, String b, String c) { + return a + b + c; + } + + public int concatWithConstants(String a) { + String res = '<' + a + '>'; + + if (res.equals("")) { + return 1; + } + + if (res.equals("")) { + return 2; + } + + if (a == null) { + return 3; + } + + return 4; + } + + public String concatWithPrimitives(String a) { + return a + '#' + 42 + 53.0; + } + + public String exceptionInToString(Test t) { + return "Test: " + t + "!"; + } + + public String concatWithField(String a) { + return a + str + '#'; + } + + public int concatWithPrimitiveWrappers(Integer b, char c) { + String res = "" + b + c; + + if (res.endsWith("42")) { + return 1; + } + return 2; + } + + public int sameConcat(String a, String b) { + UtMock.assume(a != null && b != null); + + String res1 = '!' + a + '#'; + String res2 = '!' + b + '#'; + + if (res1.equals(res2)) { + return 0; + } else { + return 1; + } + } + + public String concatStrangeSymbols() { + return "\u0000" + '#' + '\u0001' + "!\u0002" + "@\u0012\t"; + } +} + diff --git a/utbot-framework-test/src/test/java/org/utbot/examples/manual/UtBotJavaApiTest.java b/utbot-framework-test/src/test/java/org/utbot/examples/manual/UtBotJavaApiTest.java index b04512aafc..c8e0213371 100644 --- a/utbot-framework-test/src/test/java/org/utbot/examples/manual/UtBotJavaApiTest.java +++ b/utbot-framework-test/src/test/java/org/utbot/examples/manual/UtBotJavaApiTest.java @@ -1,12 +1,9 @@ package org.utbot.examples.manual; import kotlin.Pair; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.utbot.common.PathUtil; +import org.utbot.api.java.AbstractUtBotJavaApiTest; import org.utbot.examples.assemble.DirectAccess; import org.utbot.examples.assemble.PrimitiveFields; import org.utbot.examples.assemble.ArrayOfComplexArrays; @@ -18,69 +15,44 @@ import org.utbot.examples.manual.examples.customer.C; import org.utbot.examples.manual.examples.customer.Demo9; import org.utbot.external.api.TestMethodInfo; +import org.utbot.external.api.UnitTestBotLight; import org.utbot.external.api.UtBotJavaApi; -import org.utbot.external.api.UtModelFactory; -import org.utbot.framework.codegen.ForceStaticMocking; -import org.utbot.framework.codegen.Junit4; -import org.utbot.framework.codegen.MockitoStaticMocking; +import org.utbot.framework.codegen.domain.*; +import org.utbot.framework.context.simple.SimpleApplicationContext; import org.utbot.framework.plugin.api.*; -import org.utbot.framework.plugin.api.util.UtContext; import org.utbot.framework.plugin.services.JdkInfoDefaultProvider; import org.utbot.framework.util.Snippet; import org.utbot.framework.util.SootUtils; -import java.io.File; import java.lang.reflect.Method; -import java.net.URISyntaxException; -import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import static java.util.Collections.*; import static org.utbot.external.api.UtModelFactoryKt.classIdForType; import static org.utbot.framework.plugin.api.MockFramework.MOCKITO; -import static org.utbot.framework.plugin.api.util.IdUtilKt.getIntArrayClassId; +import static org.utbot.framework.plugin.api.util.IdUtilKt.*; import static org.utbot.framework.util.TestUtilsKt.compileClassAndGetClassPath; import static org.utbot.framework.util.TestUtilsKt.compileClassFile; -class PredefinedGeneratorParameters { +/** + * Tests for UnitTestBot Java API (Examples at the same time) + */ +public class UtBotJavaApiTest extends AbstractUtBotJavaApiTest { - static String destinationClassName = "GeneratedTest"; - - static Method getMethodByName(Class clazz, String name, Class... parameters) { - try { - return clazz.getDeclaredMethod(name, parameters); - } catch (NoSuchMethodException ignored) { - Assertions.fail(); - } - throw new RuntimeException(); - } -} - -public class UtBotJavaApiTest { - private AutoCloseable context; - private UtModelFactory modelFactory; - - @BeforeEach - public void setUp() { - context = UtContext.Companion.setUtContext(new UtContext(PrimitiveFields.class.getClassLoader())); - modelFactory = new UtModelFactory(); - } - - @AfterEach - public void tearDown() { - try { - context.close(); - modelFactory = null; - } catch (Exception e) { - Assertions.fail(); - } - } + /** Uses {@link MultiMethodExample} as a class under test. Demonstrates how to gather information for multiple + * methods analysis and pass it to {@link UtBotJavaApi#generateTestSetsForMethods} in order to produce set + * of information needed for the test generation. After that shows how to use {@link UtBotJavaApi#generateTestCode} + * in order to generate tests code. + *
+ * The tests are generated with and without concrete execution. + *
+ * Note, that you can use the {@code Snippet} instance in order to evaluate the test generation result. + */ @Test public void testMultiMethodClass() { @@ -93,91 +65,99 @@ public void testMultiMethodClass() { classIdForType(MultiMethodExample.class) ); - EnvironmentModels initialState = new EnvironmentModels( - classUnderTestModel, - Collections.emptyList(), - Collections.emptyMap() - ); - - EnvironmentModels thirdMethodState = new EnvironmentModels( - classUnderTestModel, - Collections.singletonList(new UtPrimitiveModel("some")), - Collections.emptyMap() - ); - - - Method firstMethodUnderTest = PredefinedGeneratorParameters.getMethodByName( - MultiMethodExample.class, - "firstMethod" - ); - - Method secondMethodUnderTest = PredefinedGeneratorParameters.getMethodByName( - MultiMethodExample.class, - "secondMethod" - ); - - Method thirdMethodUnderTest = PredefinedGeneratorParameters.getMethodByName( - MultiMethodExample.class, - "thirdMethod", - String.class - ); + Method firstMethodUnderTest = getMethodByName(MultiMethodExample.class, "firstMethod"); + Method secondMethodUnderTest = getMethodByName(MultiMethodExample.class, "secondMethod"); + Method thirdMethodUnderTest = getMethodByName(MultiMethodExample.class, "thirdMethod", String.class); - TestMethodInfo firstTestMethodInfo = new TestMethodInfo( - firstMethodUnderTest, - initialState); - TestMethodInfo secondTestMethodInfo = new TestMethodInfo( - secondMethodUnderTest, - initialState); - TestMethodInfo thirdTestMethodInfo = new TestMethodInfo( - thirdMethodUnderTest, - thirdMethodState); + // To find test sets, you need only {@link Method} instances + // and some configuration - List testSets = UtBotJavaApi.generateTestSets( - Arrays.asList(firstTestMethodInfo, secondTestMethodInfo, thirdTestMethodInfo), + List testSets = UtBotJavaApi.generateTestSetsForMethods( + Arrays.asList( + firstMethodUnderTest, + secondMethodUnderTest, + thirdMethodUnderTest + ), MultiMethodExample.class, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResult = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, MultiMethodExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, ForceStaticMocking.DO_NOT_FORCE, - MultiMethodExample.class.getPackage().getName() + MultiMethodExample.class.getPackage().getName(), + new SimpleApplicationContext() + ); + + TestMethodInfo firstTestMethodInfo = buildTestMethodInfo( + firstMethodUnderTest, + classUnderTestModel, + emptyList(), + Collections.emptyMap() + ); + + TestMethodInfo secondTestMethodInfo = buildTestMethodInfo( + firstMethodUnderTest, + classUnderTestModel, + emptyList(), + Collections.emptyMap() + ); + + TestMethodInfo thirdTestMethodInfo = buildTestMethodInfo( + thirdMethodUnderTest, + classUnderTestModel, + Collections.singletonList(new UtPrimitiveModel("some")), + Collections.emptyMap() ); Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Arrays.asList(firstTestMethodInfo, secondTestMethodInfo, thirdTestMethodInfo), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet1); + String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generateTestCode( + Arrays.asList( + firstTestMethodInfo, + secondTestMethodInfo, + thirdTestMethodInfo + ), + emptyList(), + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, - MultiMethodExample.class + MultiMethodExample.class, + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } + /** Uses {@link ClassRefExample} as a class under test. Demonstrates how to specify custom package name for the + * generate tests. + *
+ * The tests are generated with and without concrete execution. + *
+ * Note, that you can use the {@code Snippet} instance in order to evaluate the test generation result. + */ @Test public void testCustomPackage() { UtBotJavaApi.setStopConcreteExecutorOnExit(false); - String classpath = getClassPath(DirectAccess.class); + String classpath = getClassPath(ClassRefExample.class); String dependencyClassPath = getDependencyClassPath(); HashMap fields = new HashMap<>(); @@ -190,80 +170,84 @@ public void testCustomPackage() { UtClassRefModel classRefModel = modelFactory.produceClassRefModel(Class.class); - EnvironmentModels initialState = new EnvironmentModels( - classUnderTestModel, - Collections.singletonList(classRefModel), - Collections.emptyMap() - ); - - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( + Method methodUnderTest = getMethodByName( ClassRefExample.class, "assertInstance", - Class.class - ); + Class.class); - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), + List testSets = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(methodUnderTest), ClassRefExample.class, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResult = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, ClassRefExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, ForceStaticMocking.DO_NOT_FORCE, - "some.custom.name" + "some.custom.name", + new SimpleApplicationContext() ); Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet1); - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, + TestMethodInfo testMethodInfo = buildTestMethodInfo( + methodUnderTest, + classUnderTestModel, + Collections.singletonList(classRefModel), + Collections.emptyMap() + ); + + String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generateTestCode( + Collections.singletonList(testMethodInfo), + emptyList(), + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, ClassRefExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, ForceStaticMocking.DO_NOT_FORCE, - "some.custom.name" + "some.custom.name", + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } + /** Uses {@link AssignedArrayExample} as a class under test. Demonstrates how to specify custom package name for the + * generate tests. + *
+ * The tests are generated with and without concrete execution. + *
+ * Note, that you can use the {@code Snippet} instance in order to evaluate the test generation result. + */ @Test public void testOnObjectWithAssignedArrayField() { UtBotJavaApi.setStopConcreteExecutorOnExit(false); - String classpath = getClassPath(DirectAccess.class); + String classpath = getClassPath(AssignedArray.class); String dependencyClassPath = getDependencyClassPath(); ClassId classIdAssignedArray = classIdForType(AssignedArray.class); @@ -288,164 +272,82 @@ public void testOnObjectWithAssignedArrayField() { classIdForType(AssignedArrayExample.class) ); - EnvironmentModels initialState = new EnvironmentModels( - classUnderTestModel, - Collections.singletonList(compositeModel), - Collections.emptyMap() - ); - - - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( + Method methodUnderTest = getMethodByName( AssignedArrayExample.class, "foo", AssignedArray.class ); - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), + List testSets = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(methodUnderTest), AssignedArrayExample.class, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResult = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, AssignedArrayExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); - - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, - classpath, - dependencyClassPath, - AssignedArrayExample.class, - Junit4.INSTANCE, - MOCKITO, - CodegenLanguage.JAVA, - MockitoStaticMocking.INSTANCE, - false, - ForceStaticMocking.DO_NOT_FORCE - ); - - Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); - } - - @Test - public void testClassRef() { - - UtBotJavaApi.setStopConcreteExecutorOnExit(false); - - String classpath = getClassPath(DirectAccess.class); - String dependencyClassPath = getDependencyClassPath(); - - HashMap fields = new HashMap<>(); - fields.put("stringClass", modelFactory.produceClassRefModel(String.class)); - - UtCompositeModel classUnderTestModel = modelFactory.produceCompositeModel( - classIdForType(ClassRefExample.class), - fields - ); - - UtClassRefModel classRefModel = modelFactory.produceClassRefModel(Class.class); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet1); - EnvironmentModels initialState = new EnvironmentModels( + TestMethodInfo methodInfo = buildTestMethodInfo( + methodUnderTest, classUnderTestModel, - Collections.singletonList(classRefModel), + Collections.singletonList(compositeModel), Collections.emptyMap() ); - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( - ClassRefExample.class, - "assertInstance", - Class.class - ); - - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), - ClassRefExample.class, - classpath, - dependencyClassPath, - MockStrategyApi.OTHER_PACKAGES, - 3000L - ); - - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), - testSets, - PredefinedGeneratorParameters.destinationClassName, - classpath, - dependencyClassPath, - ClassRefExample.class, - Junit4.INSTANCE, - MOCKITO, - CodegenLanguage.JAVA, - MockitoStaticMocking.INSTANCE, - false, - ForceStaticMocking.DO_NOT_FORCE - ); - - - Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); - - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, + String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generateTestCode( + Collections.singletonList(methodInfo), + emptyList(), + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, - ClassRefExample.class, + AssignedArrayExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } + /** Uses {@link DirectAccessExample} as a class under test. Demonstrates how to test objects with public fields. + *
+ * The tests are generated with and without concrete execution. + *
+ * Note, that you can use the {@code Snippet} instance in order to evaluate the test generation result. + */ @Test public void testObjectWithPublicFields() { UtBotJavaApi.setStopConcreteExecutorOnExit(false); - String classpath = getClassPath(DirectAccess.class); + String classpath = getClassPath(DirectAccessExample.class); String dependencyClassPath = getDependencyClassPath(); ClassId testClassId = classIdForType(DirectAccess.class); @@ -477,155 +379,77 @@ public void testObjectWithPublicFields() { classIdForType(DirectAccessExample.class) ); - EnvironmentModels initialState = new EnvironmentModels( - classUnderTestModel, - Collections.singletonList(compositeModel), - Collections.emptyMap() - ); - - - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( + Method methodUnderTest = getMethodByName( DirectAccessExample.class, "foo", DirectAccess.class ); - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), + List testSets = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(methodUnderTest), DirectAccessExample.class, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResult = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, DirectAccessExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); - - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, - classpath, - dependencyClassPath, - DirectAccessExample.class, - Junit4.INSTANCE, - MOCKITO, - CodegenLanguage.JAVA, - MockitoStaticMocking.INSTANCE, - false, - ForceStaticMocking.DO_NOT_FORCE - ); - - Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); - } - - @Test - public void testObjectWithPublicFieldsWithAssembleModel() { + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet1); - UtBotJavaApi.setStopConcreteExecutorOnExit(false); - - String classpath = getClassPath(DirectAccess.class); - String dependencyClassPath = getDependencyClassPath(); - - ClassId testClassId = classIdForType(DirectAccess.class); - ClassId innerClassId = classIdForType(PrimitiveFields.class); - - HashMap primitiveFields = new HashMap<>(); - primitiveFields.put("a", new UtPrimitiveModel(2)); - primitiveFields.put("b", new UtPrimitiveModel(4)); - - HashMap fields = new HashMap<>(); - fields.put("a", new UtPrimitiveModel(2)); - fields.put("b", new UtPrimitiveModel(4)); - fields.put("s", - modelFactory.produceCompositeModel( - innerClassId, - primitiveFields, - Collections.emptyMap() - ) - ); - - UtCompositeModel compositeModel = modelFactory.produceCompositeModel( - testClassId, - fields, - Collections.emptyMap() - ); - - // This class does not contain any fields. Using overloads - UtCompositeModel classUnderTestModel = modelFactory.produceCompositeModel( - classIdForType(DirectAccessExample.class) - ); - - EnvironmentModels initialState = new EnvironmentModels( + TestMethodInfo methodInfo = buildTestMethodInfo( + methodUnderTest, classUnderTestModel, Collections.singletonList(compositeModel), Collections.emptyMap() ); - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( - DirectAccessExample.class, - "foo", - DirectAccess.class - ); - - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), - DirectAccessExample.class, - classpath, - dependencyClassPath, - MockStrategyApi.OTHER_PACKAGES, - 3000L - ); - - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), - testSets, - PredefinedGeneratorParameters.destinationClassName, + String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generateTestCode( + Collections.singletonList(methodInfo), + emptyList(), + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, DirectAccessExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); - Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); + Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } - + /** Uses {@link ArrayOfPrimitiveArraysExample} as a class under test. Demonstrates how to test code that uses arrays of primitives + * inside arrays. + *
+ * The tests are generated with and without concrete execution. + *
+ * Note, that you can use the {@code Snippet} instance in order to evaluate the test generation result. + */ @Test public void testOnObjectWithArrayOfPrimitiveArrays() { @@ -661,83 +485,84 @@ public void testOnObjectWithArrayOfPrimitiveArrays() { enclosingArrayParameters ); - UtCompositeModel cmArrayOfPrimitiveArrays = modelFactory.produceCompositeModel( + UtCompositeModel compositeModelArrayOfPrimitiveArrays = modelFactory.produceCompositeModel( classIdArrayOfPrimitiveArraysClass, Collections.singletonMap("array", enclosingArrayOfPrimitiveArrayModel) ); - UtCompositeModel testClassCompositeModel = modelFactory.produceCompositeModel( + UtCompositeModel classUnderTestModel = modelFactory.produceCompositeModel( cidArrayOfPrimitiveArraysTest ); - EnvironmentModels initialState = new EnvironmentModels( - testClassCompositeModel, - Collections.singletonList(cmArrayOfPrimitiveArrays), - Collections.emptyMap() - ); - - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( + Method methodUnderTest = getMethodByName( ArrayOfPrimitiveArraysExample.class, "assign10", ArrayOfPrimitiveArrays.class ); - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), + List testSets = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(methodUnderTest), ArrayOfPrimitiveArraysExample.class, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResult = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, ArrayOfPrimitiveArraysExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet); - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState) - ), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, + TestMethodInfo methodInfo = buildTestMethodInfo( + methodUnderTest, + classUnderTestModel, + Collections.singletonList(compositeModelArrayOfPrimitiveArrays), + Collections.emptyMap() + ); + + String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generateTestCode( + Collections.singletonList(methodInfo), + emptyList(), + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, ArrayOfPrimitiveArraysExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } - /** - * The test is inspired by the API customers + /** Example provided by customers. + *
+ * The tests are generated with and without concrete execution. + *
+ * Note, that you can use the {@code Snippet} instance in order to evaluate the test generation result. */ @Test public void testProvided3() { @@ -765,144 +590,144 @@ public void testProvided3() { Collections.singletonMap("b0", bClassModel) ); - EnvironmentModels environmentModels = new EnvironmentModels( - demo9Model, - Collections.singletonList(bClassModel), - Collections.emptyMap() - ); - - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( + Method methodUnderTest = getMethodByName( Demo9.class, "test", B.class ); - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - environmentModels - ) - ), + + List testSets = UtBotJavaApi.generateTestSetsForMethods( + singletonList(methodUnderTest), Demo9.class, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResult = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, Demo9.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); - - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - environmentModels - ) - ), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet1); + + TestMethodInfo methodInfo = buildTestMethodInfo( + methodUnderTest, + demo9Model, + Collections.singletonList(bClassModel), + Collections.emptyMap() + ); + + + String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generateTestCode( + Collections.singletonList(methodInfo), + emptyList(), + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, - Demo9.class + Demo9.class, + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } + /** Trivial example. + *
+ * The tests are generated with and without concrete execution. + *
+ * Note, that you can use the {@code Snippet} instance in order to evaluate the test generation result. + */ @Test public void testCustomAssertion() { + String classpath = getClassPath(Trivial.class); String dependencyClassPath = getDependencyClassPath(); - UtCompositeModel model = modelFactory. + UtCompositeModel trivialModel = modelFactory. produceCompositeModel( classIdForType(Trivial.class) ); - EnvironmentModels environmentModels = new EnvironmentModels( - model, - Collections.singletonList(new UtPrimitiveModel(2)), - Collections.emptyMap() - ); - - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( + Method methodUnderTest = getMethodByName( Trivial.class, "aMethod", int.class ); - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - environmentModels - ) - ), + List testSets = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(methodUnderTest), Trivial.class, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResult = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, Trivial.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); - - String generationResult2 = UtBotJavaApi.generate( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - environmentModels - ) - ), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet1); + + TestMethodInfo methodInfo = buildTestMethodInfo( + methodUnderTest, + trivialModel, + Collections.singletonList(new UtPrimitiveModel(2)), + Collections.emptyMap() + ); + + String generationResult2 = UtBotJavaApi.generateTestCode( + Collections.singletonList(methodInfo), + emptyList(), + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, Trivial.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResult2); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } /** @@ -943,55 +768,41 @@ public void testProvided3Reused() { Assertions.fail("Failed to load the class"); } - UtCompositeModel demo9Model = modelFactory. - produceCompositeModel( - classIdForType(compiledClass), - Collections.emptyMap() - ); - - EnvironmentModels environmentModels = new EnvironmentModels( - demo9Model, - Collections.singletonList(new UtPrimitiveModel(3)), - Collections.emptyMap() - ); - - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( + Method methodUderTest = getMethodByName( compiledClass, "test", int.class ); - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - environmentModels - ) - ), + List testSets = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(methodUderTest), compiledClass, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, compiledClass, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet); // The test compiles and everything goes well. // Let's recompile the initial clas file @@ -1021,52 +832,49 @@ public void testProvided3Reused() { Assertions.fail("Failed to load the class after recompilation"); } - EnvironmentModels environmentModels2 = new EnvironmentModels( - demo9Model, - Arrays.asList(new UtPrimitiveModel(4), new UtPrimitiveModel("Some String")), - Collections.emptyMap() - ); - - Method methodUnderTest2 = PredefinedGeneratorParameters.getMethodByName( + Method recompiledMethod = getMethodByName( recompiledClass, "test", - int.class, - String.class + int.class, String.class ); - List testSets1 = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest2, - environmentModels - ) - ), + List testSets1 = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(recompiledMethod), recompiledClass, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResultWithConcreteExecutionOnly2 = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResultWithConcreteExecutionOnly2 = UtBotJavaApi.generateTestCode( + emptyList(), testSets1, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, recompiledClass, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly2); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } + /** A test provided by our customer. Demonstrates how to use the API in + *
+ * The tests are generated with and without concrete execution. + *
+ * Note, that you can use the {@code Snippet} instance in order to evaluate the test generation result. + */ @Test public void testProvided1() { @@ -1087,66 +895,61 @@ public void testProvided1() { new UtPrimitiveModel("Some Text") ); - EnvironmentModels initialState = new EnvironmentModels( - providedTestModel, - parameters, - Collections.emptyMap() - ); - - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( + Method methodUnderTest = getMethodByName( ProvidedExample.class, "test0", int.class, int.class, String.class ); - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState - ) - ), + List testSets = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(methodUnderTest), ProvidedExample.class, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResult = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, ProvidedExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); - - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState - ) - ), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet1); + + TestMethodInfo methodInfo = buildTestMethodInfo( + methodUnderTest, + providedTestModel, + parameters, + Collections.emptyMap() + ); + + String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generateTestCode( + Collections.singletonList(methodInfo), + emptyList(), + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, - ProvidedExample.class + ProvidedExample.class, + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } @Test @@ -1163,66 +966,62 @@ public void testOnObjectWithArrayOfComplexArrays() { classIdForType(ArrayOfComplexArraysExample.class) ); - EnvironmentModels initialState = new EnvironmentModels( - testClassCompositeModel, - Collections.singletonList(cmArrayOfComplexArrays), - Collections.emptyMap() - ); - - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName( + Method methodUnderTest = getMethodByName( ArrayOfComplexArraysExample.class, "getValue", ArrayOfComplexArrays.class ); - List testSets = UtBotJavaApi.generateTestSets( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState - ) - ), + List testSets = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(methodUnderTest), ArrayOfComplexArraysExample.class, classpath, dependencyClassPath, MockStrategyApi.OTHER_PACKAGES, - 3000L + 3000L, + new SimpleApplicationContext() ); - String generationResult = UtBotJavaApi.generate( - Collections.emptyList(), + String generationResult = UtBotJavaApi.generateTestCode( + emptyList(), testSets, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, ArrayOfComplexArraysExample.class, + ProjectType.PureJvm, Junit4.INSTANCE, MOCKITO, CodegenLanguage.JAVA, MockitoStaticMocking.INSTANCE, false, - ForceStaticMocking.DO_NOT_FORCE + ForceStaticMocking.DO_NOT_FORCE, + new SimpleApplicationContext() ); Snippet snippet1 = new Snippet(CodegenLanguage.JAVA, generationResult); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet1); - - String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generate( - Collections.singletonList( - new TestMethodInfo( - methodUnderTest, - initialState - ) - ), - Collections.emptyList(), - PredefinedGeneratorParameters.destinationClassName, + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet1); + + + TestMethodInfo methodInfo = buildTestMethodInfo( + methodUnderTest, + testClassCompositeModel, + Collections.singletonList(cmArrayOfComplexArrays), + Collections.emptyMap() + ); + + String generationResultWithConcreteExecutionOnly = UtBotJavaApi.generateTestCode( + Collections.singletonList(methodInfo), + emptyList(), + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, - ArrayOfComplexArraysExample.class + ArrayOfComplexArraysExample.class, + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generationResultWithConcreteExecutionOnly); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } @Test @@ -1237,27 +1036,14 @@ public void testFuzzingSimple() { classIdForType(StringSwitchExample.class) ); - Method methodUnderTest = PredefinedGeneratorParameters.getMethodByName(StringSwitchExample.class, "validate", String.class, int.class, int.class); - - IdentityHashMap models = modelFactory.produceAssembleModel( - methodUnderTest, + Method methodUnderTest = getMethodByName( StringSwitchExample.class, - Collections.singletonList(classUnderTestModel) + "validate", + String.class, int.class, int.class ); - 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 testSets1 = UtBotJavaApi.fuzzingTestSets( - Collections.singletonList( - methodInfo - ), + Collections.singletonList(methodUnderTest), StringSwitchExample.class, classpath, dependencyClassPath, @@ -1268,44 +1054,31 @@ public void testFuzzingSimple() { return Arrays.asList(0, Integer.MIN_VALUE, Integer.MAX_VALUE); } return null; - } + }, + new SimpleApplicationContext() + ); + + TestMethodInfo methodInfo = buildTestMethodInfo( + methodUnderTest, + classUnderTestModel, + Arrays.asList(new UtPrimitiveModel("Some"), new UtPrimitiveModel(-10), new UtPrimitiveModel(0)), + Collections.emptyMap() ); - String generate = UtBotJavaApi.generate( + String generate = UtBotJavaApi.generateTestCode( Collections.singletonList(methodInfo), testSets1, - PredefinedGeneratorParameters.destinationClassName, + GENERATED_TEST_CLASS_NAME, classpath, dependencyClassPath, - StringSwitchExample.class + StringSwitchExample.class, + new SimpleApplicationContext() ); Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generate); - compileClassFile(PredefinedGeneratorParameters.destinationClassName, snippet2); - } - - @NotNull - private String getClassPath(Class clazz) { - return clazz.getProtectionDomain().getCodeSource().getLocation().getPath(); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet2); } - @NotNull - private String getDependencyClassPath() { - - ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); - URL[] urls = PathUtil.getUrlsFromClassLoader(contextClassLoader); - - - return Arrays.stream(urls).map(url -> - { - try { - return new File(url.toURI()).toString(); - } catch (URISyntaxException e) { - Assertions.fail(e); - } - throw new RuntimeException(); - }).collect(Collectors.joining(File.pathSeparator)); - } public UtCompositeModel createArrayOfComplexArraysModel() { ClassId classIdOfArrayOfComplexArraysClass = classIdForType(ArrayOfComplexArrays.class); ClassId classIdOfComplexArray = classIdForType(ComplexArray.class); @@ -1356,4 +1129,35 @@ public UtCompositeModel createArrayOfComplexArraysModel() { classIdOfArrayOfComplexArraysClass, Collections.singletonMap("array", arrayOfComplexArrayClasses)); } + + @Test + public void testUnitTestBotLight() { + String classpath = getClassPath(Trivial.class); + String dependencyClassPath = getDependencyClassPath(); + + UtCompositeModel model = modelFactory. + produceCompositeModel( + classIdForType(Trivial.class) + ); + + Method methodUnderTest = getMethodByName( + Trivial.class, + "aMethod", + int.class + ); + + TestMethodInfo methodInfo = buildTestMethodInfo( + methodUnderTest, + model, + Collections.singletonList(new UtPrimitiveModel(2)), + Collections.emptyMap() + ); + + UnitTestBotLight.run( + (engine, state) -> System.err.println("Got a call:" + state.getStmt()), + methodInfo, + classpath, + dependencyClassPath + ); + } } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/BinarySearchTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/BinarySearchTest.kt index d197e8a838..b2a0a82c2b 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/BinarySearchTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/BinarySearchTest.kt @@ -1,35 +1,13 @@ package org.utbot.examples.algorithms -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement import org.junit.jupiter.api.Test +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException class BinarySearchTest : UtValueTestCaseChecker(testClass = BinarySearch::class,) { @Test fun testLeftBinarySearch() { - val fullSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("does not iterate "), - DocCodeStmt("while(left < right - 1)"), - DocRegularStmt(", "), - DocRegularStmt("executes conditions:\n"), - DocRegularStmt(" "), - DocCodeStmt("(found): False"), - DocRegularStmt("\n"), - DocRegularStmt("returns from: "), - DocCodeStmt("return -1;"), - DocRegularStmt("\n") - - ) - ) - ) checkWithException( BinarySearch::leftBinSearch, ignoreExecutionsNumber, @@ -37,35 +15,7 @@ class BinarySearchTest : UtValueTestCaseChecker(testClass = BinarySearch::class, { a, _, r -> a.size >= 2 && a[0] > a[1] && r.isException() }, { a, _, r -> a.isEmpty() && r.getOrNull() == -1 }, { a, key, r -> a.isNotEmpty() && key >= a[(a.size - 1) / 2] && key !in a && r.getOrNull() == -1 }, - { a, key, r -> a.isNotEmpty() && key in a && r.getOrNull() == a.indexOfFirst { it == key } + 1 }, - // TODO enable it JIRA:1442 - /* - summaryTextChecks = listOf( - keyContain(DocCodeStmt("(found): False")), - keyContain(DocCodeStmt("(found): True")), - keyContain(DocRegularStmt(" BinarySearch::isUnsorted once")), - keyContain(DocRegularStmt("throws NullPointerException in: isUnsorted(array)")), - keyContain(DocRegularStmt("throws IllegalArgumentException after condition: isUnsorted(array)")), - keyContain(DocCodeStmt("(array[middle] < key): True")), - keyContain(DocCodeStmt("(array[middle] == key): True")), - keyContain(DocCodeStmt("(array[middle] < key): True")), - keyMatch(fullSummary) - ), - summaryNameChecks = listOf( - keyContain("testLeftBinSearch_BinarySearchIsUnsorted"), - keyContain("testLeftBinSearch_ThrowIllegalArgumentException"), - keyContain("testLeftBinSearch_NotFound"), - keyContain("testLeftBinSearch_MiddleOfArrayLessThanKey"), - keyContain("testLeftBinSearch_Found") - ), - summaryDisplayNameChecks = listOf( - keyMatch("isUnsorted(array) -> ThrowIllegalArgumentException"), - keyMatch("isUnsorted(array) -> ThrowIllegalArgumentException"), - keyMatch("found : False -> return -1"), - keyMatch("array[middle] == key : True -> return right + 1"), - keyMatch("array[middle] < key : True -> return -1") - ) - */ + { a, key, r -> a.isNotEmpty() && key in a && r.getOrNull() == a.indexOfFirst { it == key } + 1 } ) } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/CorrectBracketSequencesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/CorrectBracketSequencesTest.kt index d19f753319..9e00ce7f15 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/CorrectBracketSequencesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/CorrectBracketSequencesTest.kt @@ -1,73 +1,30 @@ package org.utbot.examples.algorithms -import org.utbot.tests.infrastructure.UtValueTestCaseChecker +import org.junit.jupiter.api.Test import org.utbot.examples.algorithms.CorrectBracketSequences.isBracket import org.utbot.examples.algorithms.CorrectBracketSequences.isOpen -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.* internal class CorrectBracketSequencesTest : UtValueTestCaseChecker( testClass = CorrectBracketSequences::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) // TODO generics in lists - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testIsOpen() { - val isOpenSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("returns from: "), - DocCodeStmt("return a == '(' || a == '{' || a == '[';"), - DocRegularStmt("\n") - ) - ) - ) - checkStaticMethod( CorrectBracketSequences::isOpen, eq(4), { c, r -> c == '(' && r == true }, { c, r -> c == '{' && r == true }, { c, r -> c == '[' && r == true }, - { c, r -> c !in "({[".toList() && r == false }, - summaryNameChecks = listOf( - keyMatch("testIsOpen_AEqualsCharOrAEqualsCharOrAEqualsChar"), - keyMatch("testIsOpen_ANotEqualsCharOrANotEqualsCharOrANotEqualsChar") - ), - summaryDisplayNameChecks = listOf( - keyMatch("return a == '(' || a == '{' || a == '[' : False -> return a == '(' || a == '{' || a == '['"), - keyMatch("return a == '(' || a == '{' || a == '[' : True -> return a == '(' || a == '{' || a == '['") - ), - summaryTextChecks = listOf( - keyMatch(isOpenSummary) - ) + { c, r -> c !in "({[".toList() && r == false } ) } @Test fun testIsBracket() { - val isBracketSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("returns from: "), - DocCodeStmt("return isOpen(a) || a == ')' || a == '}' || a == ']';"), - DocRegularStmt("\n") - ) - ) - ) checkStaticMethod( CorrectBracketSequences::isBracket, eq(7), @@ -77,33 +34,12 @@ internal class CorrectBracketSequencesTest : UtValueTestCaseChecker( { c, r -> c == ')' && r == true }, { c, r -> c == '}' && r == true }, { c, r -> c == ']' && r == true }, - { c, r -> c !in "(){}[]".toList() && r == false }, - summaryNameChecks = listOf( - keyMatch("testIsBracket_IsOpenOrANotEqualsCharOrANotEqualsCharOrANotEqualsChar"), - keyMatch("testIsBracket_IsOpenOrAEqualsCharOrAEqualsCharOrAEqualsChar") - ), - summaryDisplayNameChecks = listOf( - keyMatch("return isOpen(a) || a == ')' || a == '}' || a == ']' : False -> return isOpen(a) || a == ')' || a == '}' || a == ']'"), - keyMatch("return isOpen(a) || a == ')' || a == '}' || a == ']' : True -> return isOpen(a) || a == ')' || a == '}' || a == ']'") - ), - summaryTextChecks = listOf( - keyMatch(isBracketSummary) - ) + { c, r -> c !in "(){}[]".toList() && r == false } ) } @Test fun testIsTheSameType() { - val isTheSameTypeSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("returns from: "), - DocCodeStmt("return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']';"), - DocRegularStmt("\n") - ) - ) - ) checkStaticMethod( CorrectBracketSequences::isTheSameType, ignoreExecutionsNumber, @@ -113,18 +49,7 @@ internal class CorrectBracketSequencesTest : UtValueTestCaseChecker( { a, b, r -> a == '(' && b != ')' && r == false }, { a, b, r -> a == '{' && b != '}' && r == false }, { a, b, r -> a == '[' && b != ']' && r == false }, - { a, b, r -> (a != '(' || b != ')') && (a != '{' || b != '}') && (a != '[' || b != ']') && r == false }, - summaryNameChecks = listOf( - keyMatch("testIsTheSameType_ANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsChar"), - keyMatch("testIsTheSameType_AEqualsCharAndBEqualsCharOrAEqualsCharAndBEqualsCharOrAEqualsCharAndBEqualsChar"), - ), - summaryDisplayNameChecks = listOf( - keyMatch("return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']' : False -> return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']'"), - keyMatch("return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']' : True -> return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']'") - ), - summaryTextChecks = listOf( - keyMatch(isTheSameTypeSummary) - ) + { a, b, r -> (a != '(' || b != ')') && (a != '{' || b != '}') && (a != '[' || b != ']') && r == false } ) } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/GraphTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/GraphTest.kt index 71d13c4383..bf669b9c1e 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/GraphTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/GraphTest.kt @@ -1,11 +1,11 @@ package org.utbot.examples.algorithms -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException internal class GraphTest : UtValueTestCaseChecker(testClass = GraphExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/SortTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/SortTest.kt index c761456052..174b63e7d2 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/SortTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/algorithms/SortTest.kt @@ -1,27 +1,18 @@ package org.utbot.examples.algorithms -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt import org.utbot.framework.plugin.api.MockStrategyApi import org.junit.jupiter.api.Test +import org.utbot.framework.codegen.domain.ParametrizedTestSource import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.* // TODO Kotlin mocks generics https://github.com/UnitTestBot/UTBotJava/issues/88 internal class SortTest : UtValueTestCaseChecker( testClass = Sort::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testQuickSort() { @@ -111,54 +102,12 @@ internal class SortTest : UtValueTestCaseChecker( val connection = lhs.last() >= rhs.last() && r.getOrNull()?.toList() == (lhs + rhs).sorted() lhsCondition && rhsCondition && connection - }, + } ) } @Test fun testDefaultSort() { - val defaultSortSummary1 = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("\n"), - DocRegularStmt("throws NullPointerException in: array.length < 4"), - DocRegularStmt("\n") - ) - ) - ) - - val defaultSortSummary2 = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("executes conditions:\n"), - DocRegularStmt(" "), - DocCodeStmt("(array.length < 4): True"), - DocRegularStmt("\n"), - DocRegularStmt("\n"), - DocRegularStmt("throws IllegalArgumentException after condition: array.length < 4"), - DocRegularStmt("\n") - ) - ) - ) - val defaultSortSummary3 = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("executes conditions:\n"), - DocRegularStmt(" "), - DocCodeStmt("(array.length < 4): False"), - DocRegularStmt("\n"), - DocRegularStmt("invokes:\n"), - DocRegularStmt(" {@link java.util.Arrays#sort(int[])} once"), - DocRegularStmt("\n"), - DocRegularStmt("returns from: "), - DocCodeStmt("return array;"), - DocRegularStmt("\n") - ) - ) - ) checkWithException( Sort::defaultSort, eq(3), @@ -167,22 +116,7 @@ internal class SortTest : UtValueTestCaseChecker( { a, r -> val resultArray = intArrayOf(-100, 0, 100, 200) a != null && r.getOrNull()!!.size >= 4 && r.getOrNull() contentEquals resultArray - }, - summaryTextChecks = listOf( - keyMatch(defaultSortSummary1), - keyMatch(defaultSortSummary2), - keyMatch(defaultSortSummary3), - ), - summaryNameChecks = listOf( - keyMatch("testDefaultSort_ThrowNullPointerException"), - keyMatch("testDefaultSort_ArrayLengthLessThan4"), - keyMatch("testDefaultSort_ArrayLengthGreaterOrEqual4"), - ), - summaryDisplayNameChecks = listOf( - keyMatch("array.length < 4 -> ThrowNullPointerException"), - keyMatch("array.length < 4 -> ThrowIllegalArgumentException"), - keyMatch("array.length < 4 : False -> return array"), - ) + } ) } } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/NotNullAnnotationTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/NotNullAnnotationTest.kt index 0b64573d0e..6bb162add4 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/NotNullAnnotationTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/NotNullAnnotationTest.kt @@ -1,9 +1,10 @@ package org.utbot.examples.annotations -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.AtLeast +import org.utbot.testing.UtValueTestCaseChecker internal class NotNullAnnotationTest : UtValueTestCaseChecker(testClass = NotNullAnnotation::class) { @Test @@ -68,7 +69,8 @@ internal class NotNullAnnotationTest : UtValueTestCaseChecker(testClass = NotNul checkStatics( NotNullAnnotation::notNullStaticField, eq(1), - { statics, result -> result == statics.values.single().value } + { statics, result -> result == statics.values.single().value }, + coverage = AtLeast(66) ) } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/EnumWithAnnotationsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/EnumWithAnnotationsTest.kt index b2987df28c..b4c8097e10 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/EnumWithAnnotationsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/EnumWithAnnotationsTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.annotations.lombok import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker /** * Tests for Lombok annotations diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/EnumWithoutAnnotationsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/EnumWithoutAnnotationsTest.kt index 1aa631f627..c9977326d6 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/EnumWithoutAnnotationsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/EnumWithoutAnnotationsTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.annotations.lombok import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class EnumWithoutAnnotationsTest : UtValueTestCaseChecker(testClass = EnumWithoutAnnotations::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/NotNullAnnotationsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/NotNullAnnotationsTest.kt index b73330d07c..1d3e95b33c 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/NotNullAnnotationsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/annotations/lombok/NotNullAnnotationsTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.annotations.lombok import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker /** * Tests for Lombok NonNull annotation diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayOfArraysTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayOfArraysTest.kt index 1ffe1baf69..db375097eb 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayOfArraysTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayOfArraysTest.kt @@ -1,14 +1,15 @@ package org.utbot.examples.arrays -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.atLeast +import org.junit.jupiter.api.Disabled import org.utbot.examples.casts.ColoredPoint import org.utbot.examples.casts.Point -import org.utbot.tests.infrastructure.ignoreExecutionsNumber import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.withoutMinimization +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.ignoreExecutionsNumber @Suppress("NestedLambdaShadowedImplicitParameter") internal class ArrayOfArraysTest : UtValueTestCaseChecker(testClass = ArrayOfArrays::class) { @@ -178,6 +179,7 @@ internal class ArrayOfArraysTest : UtValueTestCaseChecker(testClass = ArrayOfArr } @Test + @Disabled("Seems not aligning with ksmt version used in USVM") fun testReallyMultiDimensionalArray() { check( ArrayOfArrays::reallyMultiDimensionalArray, @@ -204,6 +206,7 @@ internal class ArrayOfArraysTest : UtValueTestCaseChecker(testClass = ArrayOfArr } @Test + @Disabled("Seems not aligning with ksmt version used in USVM") fun testReallyMultiDimensionalArrayMutation() { checkParamsMutations( ArrayOfArrays::reallyMultiDimensionalArray, @@ -268,10 +271,13 @@ internal class ArrayOfArraysTest : UtValueTestCaseChecker(testClass = ArrayOfArr } @Test + @Disabled("https://github.com/UnitTestBot/UTBotJava/issues/1267") fun testArrayWithItselfAnAsElement() { check( ArrayOfArrays::arrayWithItselfAnAsElement, eq(2), + { a, r -> a !== a[0] && r == null }, + { a, r -> a === a[0] && a.contentDeepEquals(r) }, coverage = atLeast(percents = 94) // because of the assumption ) diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayOfObjectsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayOfObjectsTest.kt index 5f67396996..1b1c3a28a9 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayOfObjectsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayOfObjectsTest.kt @@ -1,25 +1,15 @@ package org.utbot.examples.arrays -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.* // TODO failed Kotlin compilation SAT-1332 internal class ArrayOfObjectsTest : UtValueTestCaseChecker( testClass = ArrayOfObjects::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testDefaultValues() { @@ -103,14 +93,16 @@ internal class ArrayOfObjectsTest : UtValueTestCaseChecker( @Test fun testArrayOfArrays() { - check( - ArrayOfObjects::arrayOfArrays, - between(4..5), // might be two ClassCastExceptions - { a, _ -> a.any { it == null } }, - { a, _ -> a.any { it != null && it !is IntArray } }, - { a, r -> (a.all { it != null && it is IntArray && it.isEmpty() } || a.isEmpty()) && r == 0 }, - { a, r -> a.all { it is IntArray } && r == a.sumBy { (it as IntArray).sum() } }, - coverage = DoNotCalculate - ) + withEnabledTestingCodeGeneration(testCodeGeneration = false) { + check( + ArrayOfObjects::arrayOfArrays, + between(4..5), // might be two ClassCastExceptions + { a, _ -> a.any { it == null } }, + { a, _ -> a.any { it != null && it !is IntArray } }, + { a, r -> (a.all { it != null && it is IntArray && it.isEmpty() } || a.isEmpty()) && r == 0 }, + { a, r -> a.all { it is IntArray } && r == a.sumBy { (it as IntArray).sum() } }, + coverage = DoNotCalculate + ) + } } } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayStoreExceptionExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayStoreExceptionExamplesTest.kt index 2e6fdc5b49..63c14376ff 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayStoreExceptionExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArrayStoreExceptionExamplesTest.kt @@ -2,20 +2,15 @@ package org.utbot.examples.arrays import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.AtLeast -import org.utbot.tests.infrastructure.CodeGeneration -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException +import org.utbot.testing.AtLeast +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException class ArrayStoreExceptionExamplesTest : UtValueTestCaseChecker( testClass = ArrayStoreExceptionExamples::class, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - // Type inference errors in generated Kotlin code - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + // Type inference errors in generated Kotlin code + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testCorrectAssignmentSamePrimitiveType() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArraysOverwriteValueTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArraysOverwriteValueTest.kt index 3f0bab6159..72c7472581 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArraysOverwriteValueTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/ArraysOverwriteValueTest.kt @@ -1,19 +1,14 @@ package org.utbot.examples.arrays -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtValueTestCaseChecker // TODO failed Kotlin compilation SAT-1332 class ArraysOverwriteValueTest : UtValueTestCaseChecker( testClass = ArraysOverwriteValue::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testByteArray() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/CopyOfExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/CopyOfExampleTest.kt new file mode 100644 index 0000000000..16f691b3e9 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/CopyOfExampleTest.kt @@ -0,0 +1,34 @@ +package org.utbot.examples.arrays + +import org.junit.jupiter.api.Test +import org.utbot.testing.AtLeast +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException + +class CopyOfExampleTest : UtValueTestCaseChecker(testClass = CopyOfExample::class) { + @Test + fun testCopyOf() { + checkWithException( + CopyOfExample::copyOfExample, + ignoreExecutionsNumber, + { _, l, r -> l < 0 && r.isException() }, + { arr, l, r -> arr.copyOf(l).contentEquals(r.getOrThrow()) }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testCopyOfRange() { + checkWithException( + CopyOfExample::copyOfRangeExample, + ignoreExecutionsNumber, + { _, from, _, r -> from < 0 && r.isException() }, + { arr, from, _, r -> from > arr.size && r.isException() }, + { _, from, to, r -> from > to && r.isException() }, + { arr, from, to, r -> arr.copyOfRange(from, to).contentEquals(r.getOrThrow()) }, + coverage = AtLeast(82) + ) + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/FinalStaticFieldArrayTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/FinalStaticFieldArrayTest.kt index e6cfd39049..55b0d37eed 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/FinalStaticFieldArrayTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/FinalStaticFieldArrayTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.arrays -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber import org.junit.jupiter.api.Test +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber internal class FinalStaticFieldArrayTest : UtValueTestCaseChecker(testClass = FinalStaticFieldArray::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/IntArrayBasicsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/IntArrayBasicsTest.kt index 89cd68e821..9c7cffb161 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/IntArrayBasicsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/IntArrayBasicsTest.kt @@ -1,23 +1,18 @@ package org.utbot.examples.arrays import org.junit.jupiter.api.Disabled -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException // TODO failed Kotlin compilation SAT-1332 internal class IntArrayBasicsTest : UtValueTestCaseChecker( testClass = IntArrayBasics::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testIntArrayWithAssumeOrExecuteConcretely() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/PrimitiveArraysTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/PrimitiveArraysTest.kt index 93353521fe..3fdb46617b 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/PrimitiveArraysTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/arrays/PrimitiveArraysTest.kt @@ -1,21 +1,16 @@ package org.utbot.examples.arrays -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.isException // TODO failed Kotlin compilation SAT-1332 internal class PrimitiveArraysTest : UtValueTestCaseChecker( testClass = PrimitiveArrays::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testDefaultIntValues() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/ArrayCastExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/ArrayCastExampleTest.kt index e57047e56c..6356b765e9 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/ArrayCastExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/ArrayCastExampleTest.kt @@ -1,22 +1,17 @@ package org.utbot.examples.casts import org.junit.jupiter.api.Disabled -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker // TODO failed Kotlin compilation (generics) SAT-1332 //TODO: SAT-1487 calculate coverage for all methods of this test class internal class ArrayCastExampleTest : UtValueTestCaseChecker( testClass = ArrayCastExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testCastToAncestor() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/CastClassTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/CastClassTest.kt index 8444e963cf..0c65e94758 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/CastClassTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/CastClassTest.kt @@ -1,19 +1,14 @@ package org.utbot.examples.casts import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class CastClassTest : UtValueTestCaseChecker( testClass = CastClass::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testThisTypeChoice() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/CastExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/CastExampleTest.kt index cc6078c0f7..43728dc5d2 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/CastExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/CastExampleTest.kt @@ -1,21 +1,16 @@ package org.utbot.examples.casts -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException // TODO failed Kotlin compilation SAT-1332 internal class CastExampleTest : UtValueTestCaseChecker( testClass = CastExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testSimpleCast() { @@ -80,12 +75,14 @@ internal class CastExampleTest : UtValueTestCaseChecker( @Test fun testComplicatedCast() { - check( - CastExample::complicatedCast, - eq(2), - { i, a, _ -> i == 0 && a != null && a[i] != null && a[i] !is CastClassFirstSucc }, - { i, a, r -> i == 0 && a != null && a[i] != null && a[i] is CastClassFirstSucc && r is CastClassFirstSucc }, - coverage = DoNotCalculate - ) + withEnabledTestingCodeGeneration(testCodeGeneration = false) { // error: package sun.text is not visible + check( + CastExample::complicatedCast, + eq(2), + { i, a, _ -> i == 0 && a != null && a[i] != null && a[i] !is CastClassFirstSucc }, + { i, a, r -> i == 0 && a != null && a[i] != null && a[i] is CastClassFirstSucc && r is CastClassFirstSucc }, + coverage = DoNotCalculate + ) + } } } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/GenericCastExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/GenericCastExampleTest.kt index d903203673..1f86663719 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/GenericCastExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/GenericCastExampleTest.kt @@ -1,21 +1,16 @@ package org.utbot.examples.casts -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between // TODO failed Kotlin compilation SAT-1332 internal class GenericCastExampleTest : UtValueTestCaseChecker( testClass = GenericCastExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testCompareTwoNumbers() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/InstanceOfExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/InstanceOfExampleTest.kt index 2b4561ecf3..cf9cadef4c 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/InstanceOfExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/casts/InstanceOfExampleTest.kt @@ -1,23 +1,18 @@ package org.utbot.examples.casts -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber // TODO failed Kotlin compilation SAT-1332 internal class InstanceOfExampleTest : UtValueTestCaseChecker( testClass = InstanceOfExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testSimpleInstanceOf() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/ClassWithStaticAndInnerClassesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/ClassWithStaticAndInnerClassesTest.kt index ee12e07204..ab50b27ee7 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/ClassWithStaticAndInnerClassesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/ClassWithStaticAndInnerClassesTest.kt @@ -1,12 +1,15 @@ package org.utbot.examples.codegen -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker @Suppress("INACCESSIBLE_TYPE") -internal class ClassWithStaticAndInnerClassesTest : UtValueTestCaseChecker(testClass = ClassWithStaticAndInnerClasses::class) { +internal class ClassWithStaticAndInnerClassesTest : UtValueTestCaseChecker( + testClass = ClassWithStaticAndInnerClasses::class, + configurations = ignoreKotlinCompilationConfigurations, +) { @Test fun testUsePrivateStaticClassWithPrivateField() { check( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/CodegenExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/CodegenExampleTest.kt index ae342017f3..bd07565c1e 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/CodegenExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/CodegenExampleTest.kt @@ -1,11 +1,11 @@ package org.utbot.examples.codegen -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.utbot.examples.mock.MockRandomExamples import kotlin.reflect.full.functions import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.UtValueTestCaseChecker internal class CodegenExampleTest : UtValueTestCaseChecker(testClass = CodegenExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/FileWithTopLevelFunctionsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/FileWithTopLevelFunctionsTest.kt new file mode 100644 index 0000000000..6af41eb6d4 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/FileWithTopLevelFunctionsTest.kt @@ -0,0 +1,43 @@ +package org.utbot.examples.codegen + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import kotlin.reflect.KFunction3 + +@Suppress("UNCHECKED_CAST") +internal class FileWithTopLevelFunctionsTest : UtValueTestCaseChecker(testClass = FileWithTopLevelFunctionsReflectHelper.clazz.kotlin) { + @Test + fun topLevelSumTest() { + check( + ::topLevelSum, + eq(1), + ) + } + + @Test + fun extensionOnBasicTypeTest() { + check( + Int::extensionOnBasicType, + eq(1), + ) + } + + @Test + fun extensionOnCustomClassTest() { + check( + // NB: cast is important here because we need to treat receiver as an argument to be able to check its content in matchers + CustomClass::extensionOnCustomClass as KFunction3<*, CustomClass, CustomClass, Boolean>, + eq(2), + { receiver, argument, result -> receiver === argument && result == true }, + { receiver, argument, result -> receiver !== argument && result == false }, + additionalDependencies = dependenciesForClassExtensions + ) + } + + companion object { + // Compilation of extension methods for ref objects produces call to + // `kotlin.jvm.internal.Intrinsics::checkNotNullParameter`, so we need to add it to dependencies + val dependenciesForClassExtensions = arrayOf>(kotlin.jvm.internal.Intrinsics::class.java) + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/JavaAssertTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/JavaAssertTest.kt index 9c18e0a1a1..2133cf2ef8 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/JavaAssertTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/JavaAssertTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.codegen import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException class JavaAssertTest : UtValueTestCaseChecker( testClass = JavaAssert::class, diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/VoidStaticMethodsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/VoidStaticMethodsTest.kt index 65c76c7bd1..42af7ff925 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/VoidStaticMethodsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/VoidStaticMethodsTest.kt @@ -1,11 +1,12 @@ package org.utbot.examples.codegen -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker -class VoidStaticMethodsTest : UtValueTestCaseChecker(testClass = VoidStaticMethodsTestingClass::class) { +class VoidStaticMethodsTest : UtValueTestCaseChecker( + testClass = VoidStaticMethodsTestingClass::class) { @Test fun testInvokeChangeStaticFieldMethod() { check( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/ClassWithCrossReferenceRelationshipTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/ClassWithCrossReferenceRelationshipTest.kt index 3d62c2acb2..e87b3e4c7e 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/ClassWithCrossReferenceRelationshipTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/ClassWithCrossReferenceRelationshipTest.kt @@ -1,19 +1,14 @@ package org.utbot.examples.codegen.deepequals import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.UtValueTestCaseChecker +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker class ClassWithCrossReferenceRelationshipTest : UtValueTestCaseChecker( testClass = ClassWithCrossReferenceRelationship::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testClassWithCrossReferenceRelationship() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/ClassWithNullableFieldTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/ClassWithNullableFieldTest.kt index 7b29e44c7e..0026204d6c 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/ClassWithNullableFieldTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/ClassWithNullableFieldTest.kt @@ -1,19 +1,14 @@ package org.utbot.examples.codegen.deepequals import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker class ClassWithNullableFieldTest : UtValueTestCaseChecker( testClass = ClassWithNullableField::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testClassWithNullableFieldInCompound() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/DeepEqualsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/DeepEqualsTest.kt index 6fd453ca51..af46240390 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/DeepEqualsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/deepequals/DeepEqualsTest.kt @@ -1,21 +1,16 @@ package org.utbot.examples.codegen.deepequals -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker // TODO failed Kotlin compilation (generics) SAT-1332 class DeepEqualsTest : UtValueTestCaseChecker( testClass = DeepEqualsTestingClass::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testReturnList() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/modifiers/ClassWithPrivateMutableFieldOfPrivateTypeTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/modifiers/ClassWithPrivateMutableFieldOfPrivateTypeTest.kt new file mode 100644 index 0000000000..1a5ce264ab --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/codegen/modifiers/ClassWithPrivateMutableFieldOfPrivateTypeTest.kt @@ -0,0 +1,36 @@ +package org.utbot.examples.codegen.modifiers + +import org.junit.jupiter.api.Test +import org.utbot.common.withAccessibility +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jField +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker + +// TODO failed Kotlin tests execution with non-nullable expected field +class ClassWithPrivateMutableFieldOfPrivateTypeTest : UtValueTestCaseChecker( + testClass = ClassWithPrivateMutableFieldOfPrivateType::class, + testCodeGeneration = true, + configurations = ignoreKotlinCompilationConfigurations, +) { + @Test + fun testChangePrivateMutableFieldWithPrivateType() { + checkAllMutationsWithThis( + ClassWithPrivateMutableFieldOfPrivateType::changePrivateMutableFieldWithPrivateType, + eq(1), + { thisBefore, _, thisAfter, _, r -> + val privateMutableField = FieldId( + ClassWithPrivateMutableFieldOfPrivateType::class.id, + "privateMutableField" + ).jField + + val (privateFieldBeforeValue, privateFieldAfterValue) = privateMutableField.withAccessibility { + privateMutableField.get(thisBefore) to privateMutableField.get(thisAfter) + } + + privateFieldBeforeValue == null && privateFieldAfterValue != null && r == 0 + } + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/CustomerExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/CustomerExamplesTest.kt index 72e5dadcd7..4d114a5e02 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/CustomerExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/CustomerExamplesTest.kt @@ -1,22 +1,17 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.UtConcreteValue import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber internal class CustomerExamplesTest: UtValueTestCaseChecker( testClass = CustomerExamples::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testSimpleExample() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/GenericListsExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/GenericListsExampleTest.kt index f3746e865c..809b2a4222 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/GenericListsExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/GenericListsExampleTest.kt @@ -1,21 +1,16 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker // TODO disabled tests should be fixes with SAT-1441 internal class GenericListsExampleTest : UtValueTestCaseChecker( testClass = GenericListsExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test @Disabled("Doesn't find branches without NPE") diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/LinkedListsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/LinkedListsTest.kt index 7bc1497ffe..645e69c68a 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/LinkedListsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/LinkedListsTest.kt @@ -1,21 +1,16 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException // TODO failed Kotlin compilation (generics) SAT-1332 internal class LinkedListsTest : UtValueTestCaseChecker( testClass = LinkedLists::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListAlgorithmsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListAlgorithmsTest.kt index 714c790228..a9b83353ae 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListAlgorithmsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListAlgorithmsTest.kt @@ -1,20 +1,15 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.atLeast import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast // TODO failed Kotlin compilation SAT-1332 class ListAlgorithmsTest : UtValueTestCaseChecker( testClass = ListAlgorithms::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListIteratorsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListIteratorsTest.kt index 053a1fa1da..f27688be08 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListIteratorsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListIteratorsTest.kt @@ -1,24 +1,46 @@ package org.utbot.examples.collections import org.junit.jupiter.api.Disabled -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.framework.plugin.api.CodegenLanguage import kotlin.math.min import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber // TODO failed Kotlin compilation (generics) SAT-1332 internal class ListIteratorsTest : UtValueTestCaseChecker( testClass = ListIterators::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { + @Test + fun testReturnIterator() { + withoutConcrete { // We need to check that a real class is returned but not `Ut` one + check( + ListIterators::returnIterator, + ignoreExecutionsNumber, + { l, r -> l.isEmpty() && r!!.asSequence().toList().isEmpty() }, + { l, r -> l.isNotEmpty() && r!!.asSequence().toList() == l }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + } + + @Test + fun testReturnListIterator() { + withoutConcrete { // We need to check that a real class is returned but not `Ut` one + check( + ListIterators::returnListIterator, + ignoreExecutionsNumber, + { l, r -> l.isEmpty() && r!!.asSequence().toList().isEmpty() }, + { l, r -> l.isNotEmpty() && r!!.asSequence().toList() == l }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + } @Test fun testIterate() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListWrapperReturnsVoidTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListWrapperReturnsVoidTest.kt index b1356ecb47..462131d110 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListWrapperReturnsVoidTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListWrapperReturnsVoidTest.kt @@ -1,23 +1,20 @@ package org.utbot.examples.collections import org.junit.jupiter.api.Disabled -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isException import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException // TODO failed Kotlin compilation ($ in function names, generics) SAT-1220 SAT-1332 @Disabled("Java 11 transition") internal class ListWrapperReturnsVoidTest : UtValueTestCaseChecker( testClass = ListWrapperReturnsVoidExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testRunForEach() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart1Test.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart1Test.kt index 0564bf685c..3562d5f572 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart1Test.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart1Test.kt @@ -2,20 +2,15 @@ package org.utbot.examples.collections import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber // TODO failed Kotlin compilation SAT-1332 @Disabled internal class ListsPart1Test : UtValueTestCaseChecker( testClass = Lists::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testIterableContains() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart2Test.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart2Test.kt index dd62849c37..d328160986 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart2Test.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart2Test.kt @@ -1,21 +1,16 @@ package org.utbot.examples.collections import org.junit.jupiter.api.Disabled -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.CodeGeneration -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber // TODO failed Kotlin compilation SAT-1332 @Disabled internal class ListsPart2Test : UtValueTestCaseChecker( testClass = Lists::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testCollectionContains() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart3Test.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart3Test.kt index 5242bc47d3..d0b6d1f18f 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart3Test.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/ListsPart3Test.kt @@ -1,24 +1,21 @@ package org.utbot.examples.collections -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.isException import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.isException // TODO failed Kotlin compilation SAT-1332 internal class ListsPart3Test : UtValueTestCaseChecker( testClass = Lists::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun createTest() { @@ -217,6 +214,21 @@ internal class ListsPart3Test : UtValueTestCaseChecker( ) } + @Test + fun testAsListExample() { + withEnabledTestingCodeGeneration(testCodeGeneration = false) { // TODO Assemble model for java.util.ArrayList is returned, but actual type is java.util.Arrays.ArrayList https://github.com/UnitTestBot/UTBotJava/issues/398 + withoutConcrete { // TODO Concrete fail matchers with "Cannot show class" error + check( + Lists::asListExample, + eq(2), + { arr, r -> arr.isEmpty() && r!!.isEmpty() }, + { arr, r -> arr.isNotEmpty() && arr.contentEquals(r!!.toTypedArray()) }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + } + } + @Test @Disabled("TODO: add choosing proper type in list wrapper") fun testRemoveFromList() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapEntrySetTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapEntrySetTest.kt index 0139515e59..43ddf5218c 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapEntrySetTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapEntrySetTest.kt @@ -1,25 +1,20 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge import org.utbot.testcheckers.withPushingStateFromPathSelectorForConcrete -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException // TODO failed Kotlin compilation SAT-1332 class MapEntrySetTest : UtValueTestCaseChecker( testClass = MapEntrySet::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test @Disabled("JIRA:1443") @@ -148,11 +143,12 @@ class MapEntrySetTest : UtValueTestCaseChecker( @Test + @Disabled("Seems not aligning with ksmt version used in USVM") fun testIterateWithIterator() { withPushingStateFromPathSelectorForConcrete { checkWithException( MapEntrySet::iterateWithIterator, - eq(6), + ignoreExecutionsNumber, { map, result -> map == null && result.isException() }, { map, result -> map.isEmpty() && result.getOrThrow().contentEquals(intArrayOf(0, 0)) }, { map, result -> map.size % 2 == 1 && result.isException() }, diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapKeySetTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapKeySetTest.kt index d82f0a87a7..2ade7deaaa 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapKeySetTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapKeySetTest.kt @@ -1,27 +1,22 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.AtLeast -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge import org.utbot.testcheckers.withPushingStateFromPathSelectorForConcrete import org.utbot.testcheckers.withoutMinimization -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.AtLeast +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException // TODO failed Kotlin compilation SAT-1332 class MapKeySetTest : UtValueTestCaseChecker( testClass = MapKeySet::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testRemoveFromKeySet() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapValuesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapValuesTest.kt index 3eb88f6594..41787b2a55 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapValuesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapValuesTest.kt @@ -1,25 +1,20 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.AtLeast -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.ge import org.utbot.testcheckers.withoutMinimization -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.AtLeast +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException // TODO failed Kotlin compilation SAT-1332 class MapValuesTest : UtValueTestCaseChecker( testClass = MapValues::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testRemoveFromValues() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapsPart1Test.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapsPart1Test.kt index 80a318fd8a..3ad15b9b7c 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapsPart1Test.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapsPart1Test.kt @@ -1,28 +1,24 @@ package org.utbot.examples.collections import org.junit.jupiter.api.Tag -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.AtLeast -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MockStrategyApi import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge import org.utbot.testcheckers.withPushingStateFromPathSelectorForConcrete +import org.utbot.testcheckers.withoutConcrete import org.utbot.testcheckers.withoutMinimization -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.AtLeast +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber // TODO failed Kotlin compilation ($ in names, generics) SAT-1220 SAT-1332 internal class MapsPart1Test : UtValueTestCaseChecker( testClass = Maps::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testPutElementIfAbsent() { @@ -84,6 +80,45 @@ internal class MapsPart1Test : UtValueTestCaseChecker( ) } + @Test + fun testMapPutAndGet() { + withoutConcrete { + check( + Maps::mapPutAndGet, + eq(1), + { r -> r == 3 } + ) + } + } + + @Test + fun testPutInMapFromParameters() { + withoutConcrete { + check( + Maps::putInMapFromParameters, + ignoreExecutionsNumber, + { values, _ -> values == null }, + { values, r -> 1 in values.keys && r == 3 }, + { values, r -> 1 !in values.keys && r == 3 }, + ) + } + } + + // This test doesn't check anything specific, but the code from MUT + // caused errors with NPE as results while debugging `testPutInMapFromParameters`. + @Test + fun testContainsKeyAndPuts() { + withoutConcrete { + check( + Maps::containsKeyAndPuts, + ignoreExecutionsNumber, + { values, _ -> values == null }, + { values, r -> 1 !in values.keys && r == 3 }, + coverage = DoNotCalculate + ) + } + } + @Test fun testFindAllChars() { check( @@ -324,4 +359,25 @@ internal class MapsPart1Test : UtValueTestCaseChecker( coverage = DoNotCalculate ) } + + @Test + fun testCreateMapWithString() { + withoutConcrete { + check( + Maps::createMapWithString, + eq(1), + { r -> r!!.isEmpty() } + ) + } + } + @Test + fun testCreateMapWithEnum() { + withoutConcrete { + check( + Maps::createMapWithEnum, + eq(1), + { r -> r != null && r.size == 2 && r[Maps.WorkDays.Monday] == 112 && r[Maps.WorkDays.Friday] == 567 } + ) + } + } } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapsPart2Test.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapsPart2Test.kt index 62150776f1..3e62c790b6 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapsPart2Test.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/MapsPart2Test.kt @@ -1,23 +1,19 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.ge import org.utbot.testcheckers.withPushingStateFromPathSelectorForConcrete import org.utbot.testcheckers.withoutMinimization -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException // TODO failed Kotlin compilation ($ in names, generics) SAT-1220 SAT-1332 internal class MapsPart2Test : UtValueTestCaseChecker( testClass = Maps::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testReplaceEntryWithValue() { @@ -68,7 +64,7 @@ internal class MapsPart2Test : UtValueTestCaseChecker( } } - @Test + @Disabled("Flaky https://github.com/UnitTestBot/UTBotJava/issues/1695") fun testPutAllEntries() { withPushingStateFromPathSelectorForConcrete { check( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/OptionalsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/OptionalsTest.kt index d6bfec1bdd..3b9f1139ec 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/OptionalsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/OptionalsTest.kt @@ -1,23 +1,18 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException import java.util.* class OptionalsTest : UtValueTestCaseChecker( Optionals::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/QueueUsagesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/QueueUsagesTest.kt index f218b23945..69a38564ab 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/QueueUsagesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/QueueUsagesTest.kt @@ -2,19 +2,13 @@ package org.utbot.examples.collections import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException class QueueUsagesTest : UtValueTestCaseChecker( testClass = QueueUsages::class, - testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testCreateArrayDeque() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/SetIteratorsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/SetIteratorsTest.kt index 145a083892..e5d1916058 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/SetIteratorsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/SetIteratorsTest.kt @@ -1,23 +1,33 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.ge -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException // TODO failed Kotlin compilation SAT-1332 class SetIteratorsTest : UtValueTestCaseChecker( testClass = SetIterators::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { + @Test + fun testReturnIterator() { + withoutConcrete { // We need to check that a real class is returned but not `Ut` one + check( + SetIterators::returnIterator, + ignoreExecutionsNumber, + { s, r -> s.isEmpty() && r!!.asSequence().toSet().isEmpty() }, + { s, r -> s.isNotEmpty() && r!!.asSequence().toSet() == s }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + } + @Test fun testIteratorHasNext() { check( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/SetsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/SetsTest.kt index add140e79f..3f65f2676f 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/SetsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/collections/SetsTest.kt @@ -1,27 +1,22 @@ package org.utbot.examples.collections -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.AtLeast -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge import org.utbot.testcheckers.withPushingStateFromPathSelectorForConcrete import org.utbot.testcheckers.withoutMinimization -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.AtLeast +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber // TODO failed Kotlin compilation SAT-1332 internal class SetsTest : UtValueTestCaseChecker( testClass = Sets::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun createTest() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/ConditionsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/ConditionsTest.kt index c781994415..91eac95e15 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/ConditionsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/ConditionsTest.kt @@ -1,51 +1,18 @@ package org.utbot.examples.controlflow -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.keyContain -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber internal class ConditionsTest : UtValueTestCaseChecker(testClass = Conditions::class) { @Test fun testSimpleCondition() { - val conditionSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("executes conditions:\n"), - DocRegularStmt(" "), - DocCodeStmt("(condition): True"), - DocRegularStmt("\n"), - DocRegularStmt("returns from: "), - DocCodeStmt("return 1;"), - DocRegularStmt("\n"), - ) - ) - ) check( Conditions::simpleCondition, eq(2), { condition, r -> !condition && r == 0 }, - { condition, r -> condition && r == 1 }, - summaryTextChecks = listOf( - keyContain(DocCodeStmt("(condition): True")), - keyContain(DocCodeStmt("(condition): False")), - keyMatch(conditionSummary) - ), - summaryNameChecks = listOf( - keyMatch("testSimpleCondition_Condition"), - keyMatch("testSimpleCondition_NotCondition"), - ), - summaryDisplayNameChecks = listOf( - keyContain("condition : True"), - keyContain("condition : False"), - ) + { condition, r -> condition && r == 1 } ) } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/CycleDependedConditionTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/CycleDependedConditionTest.kt index 4899e41931..23f64d33d3 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/CycleDependedConditionTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/CycleDependedConditionTest.kt @@ -1,95 +1,30 @@ package org.utbot.examples.controlflow -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.keyContain -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class CycleDependedConditionTest : UtValueTestCaseChecker(testClass = CycleDependedCondition::class) { @Test fun testCycleDependedOneCondition() { - val conditionSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("does not iterate "), - DocCodeStmt("for(int i = 0; i < x; i++)"), - DocRegularStmt(", "), - DocRegularStmt("returns from: "), - DocCodeStmt("return 0;"), - DocRegularStmt("\n"), - ) - ) - ) check( CycleDependedCondition::oneCondition, eq(3), { x, r -> x <= 0 && r == 0 }, { x, r -> x in 1..2 && r == 0 }, - { x, r -> x > 2 && r == 1 }, - summaryTextChecks = listOf( - keyContain(DocCodeStmt("(i == 2): True")), - keyContain(DocCodeStmt("(i == 2): False")), - keyMatch(conditionSummary) - ), - summaryNameChecks = listOf( - keyMatch("testOneCondition_IEquals2"), - keyMatch("testOneCondition_INotEquals2"), - keyMatch("testOneCondition_ReturnZero"), - ), - summaryDisplayNameChecks = listOf( - keyContain("i == 2 : True"), - keyContain("i == 2 : False"), - keyContain("return 0"), - ) + { x, r -> x > 2 && r == 1 } ) } @Test fun testCycleDependedTwoCondition() { - val conditionSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("does not iterate "), - DocCodeStmt("for(int i = 0; i < x; i++)"), - DocRegularStmt(", "), - DocRegularStmt("returns from: "), - DocCodeStmt("return 0;"), - DocRegularStmt("\n"), - ) - ) - ) check( CycleDependedCondition::twoCondition, eq(4), { x, r -> x <= 0 && r == 0 }, { x, r -> x in 1..3 && r == 0 }, { x, r -> x == 4 && r == 1 }, - { x, r -> x >= 5 && r == 0 }, - summaryTextChecks = listOf( - keyContain(DocCodeStmt("(x == 4): False")), - keyContain(DocCodeStmt("(i > 2): True")), - keyContain(DocCodeStmt("(i > 2): False")), - keyMatch(conditionSummary) - ), - summaryNameChecks = listOf( - keyMatch("testTwoCondition_XNotEquals4"), - keyMatch("testTwoCondition_XEquals4"), - keyMatch("testTwoCondition_ILessOrEqual2"), - keyMatch("testTwoCondition_ReturnZero"), - ), - summaryDisplayNameChecks = listOf( - keyContain("x == 4 : False"), - keyContain("x == 4 : True"), - keyContain("i > 2 : False"), - keyContain("return 0"), - ) + { x, r -> x >= 5 && r == 0 } ) } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/CyclesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/CyclesTest.kt index f3d2d7d981..ab293e1e9e 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/CyclesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/CyclesTest.kt @@ -1,18 +1,12 @@ package org.utbot.examples.controlflow -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.tests.infrastructure.keyContain -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException internal class CyclesTest : UtValueTestCaseChecker(testClass = Cycles::class) { @Test @@ -28,40 +22,12 @@ internal class CyclesTest : UtValueTestCaseChecker(testClass = Cycles::class) { @Test fun testForCycleFour() { - val cycleSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("does not iterate "), - DocCodeStmt("for(int i = 0; i < x; i++)"), - DocRegularStmt(", "), - DocRegularStmt("returns from: "), - DocCodeStmt("return -1;"), - DocRegularStmt("\n"), - ) - ) - ) check( Cycles::forCycleFour, eq(3), { x, r -> x <= 0 && r == -1 }, { x, r -> x in 1..4 && r == -1 }, - { x, r -> x > 4 && r == 1 }, - summaryTextChecks = listOf( - keyContain(DocCodeStmt("(i > 4): True")), - keyContain(DocCodeStmt("(i > 4): False")), - keyMatch(cycleSummary) - ), - summaryNameChecks = listOf( - keyMatch("testForCycleFour_IGreaterThan4"), - keyMatch("testForCycleFour_ILessOrEqual4"), - keyMatch("testForCycleFour_ReturnNegative1") - ), - summaryDisplayNameChecks = listOf( - keyContain("i > 4 : True"), - keyContain("i > 4 : False"), - keyContain("return -1") - ) + { x, r -> x > 4 && r == 1 } ) } @@ -100,101 +66,18 @@ internal class CyclesTest : UtValueTestCaseChecker(testClass = Cycles::class) { check( Cycles::callInnerWhile, between(1..2), - { x, r -> x >= 42 && r == Cycles().callInnerWhile(x) }, - summaryTextChecks = listOf( - keyContain(DocCodeStmt("return innerWhile(value, 42);")), - ), - summaryNameChecks = listOf( - keyMatch("testCallInnerWhile_IterateWhileLoop") - ) + { x, r -> x >= 42 && r == Cycles().callInnerWhile(x) } ) } @Test fun testInnerLoop() { - val innerLoopSummary1 = arrayOf( - DocRegularStmt("Test "), - DocRegularStmt("calls "), - DocRegularStmt("CycleDependedCondition::twoCondition"), - DocRegularStmt(",\n there it "), - DocRegularStmt("iterates the loop "), - DocRegularStmt(""), - DocCodeStmt("for(int i = 0; i < x; i++)"), - DocRegularStmt(" "), - DocRegularStmt("once"), - DocRegularStmt(""), - DocRegularStmt(". "), - DocRegularStmt("\n Test "), -// DocRegularStmt("afterwards "), - DocRegularStmt("returns from: "), - DocCodeStmt("return 0;"), - DocRegularStmt("\n "), - DocRegularStmt("\nTest "), -// DocRegularStmt("afterwards "), - DocRegularStmt("returns from: "), - DocCodeStmt("return cycleDependedCondition.twoCondition(value);"), - DocRegularStmt("\n"), - ) - val innerLoopSummary2 = arrayOf( - DocRegularStmt("Test "), - DocRegularStmt("calls "), - DocRegularStmt("CycleDependedCondition::twoCondition"), - DocRegularStmt(",\n there it "), - DocRegularStmt("iterates the loop "), - DocRegularStmt(""), - DocCodeStmt("for(int i = 0; i < x; i++)"), - DocRegularStmt(" "), - DocRegularStmt("4 times"), - DocRegularStmt(""), - DocRegularStmt(",\n "), - DocRegularStmt(" "), - DocRegularStmt("inside this loop, the test "), - DocRegularStmt("executes conditions:\n "), - DocRegularStmt(""), - DocCodeStmt("(i > 2): True"), - DocRegularStmt(",\n "), - DocCodeStmt("(x == 4): True"), - DocRegularStmt("\n returns from: "), - DocCodeStmt("return 1;"), - DocRegularStmt("\nTest "), -// DocRegularStmt("afterwards "), - DocRegularStmt("returns from: "), - DocCodeStmt("return cycleDependedCondition.twoCondition(value);"), - DocRegularStmt("\n"), - ) - val innerLoopSummary3 = arrayOf( - DocRegularStmt("Test "), - DocRegularStmt("calls "), - DocRegularStmt("CycleDependedCondition::twoCondition"), - DocRegularStmt(",\n there it "), - DocRegularStmt("iterates the loop "), - DocCodeStmt("for(int i = 0; i < x; i++)"), - DocRegularStmt("5 times"), - DocRegularStmt("inside this loop, the test "), - DocRegularStmt("executes conditions:\n "), - DocCodeStmt("(x == 4): False"), - DocRegularStmt("\n Test "), - DocRegularStmt("returns from: "), - DocCodeStmt("return 0;"), - DocCodeStmt("return cycleDependedCondition.twoCondition(value);"), - ) check( Cycles::innerLoop, ignoreExecutionsNumber, { x, r -> x in 1..3 && r == 0 }, { x, r -> x == 4 && r == 1 }, - { x, r -> x >= 5 && r == 0 }, - // TODO JIRA:1442 -/* summaryTextChecks = listOf( - keyContain(*innerLoopSummary1), - keyContain(*innerLoopSummary2), - keyContain(*innerLoopSummary3) - ), - summaryNameChecks = listOf( - keyMatch("testInnerLoop_ReturnZero"), - keyMatch("testInnerLoop_XEquals4"), - keyMatch("testInnerLoop_XNotEquals4") - )*/ + { x, r -> x >= 5 && r == 0 } ) } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/SwitchTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/SwitchTest.kt index e503715393..0163d095d0 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/SwitchTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/controlflow/SwitchTest.kt @@ -1,12 +1,5 @@ package org.utbot.examples.controlflow -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.keyContain -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement import java.math.RoundingMode.CEILING import java.math.RoundingMode.DOWN import java.math.RoundingMode.HALF_DOWN @@ -16,23 +9,11 @@ import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge import org.utbot.testcheckers.withoutMinimization +import org.utbot.testing.UtValueTestCaseChecker internal class SwitchTest : UtValueTestCaseChecker(testClass = Switch::class) { @Test fun testSimpleSwitch() { - val switchCaseSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("activates switch case: "), - DocCodeStmt("default"), - DocRegularStmt(", "), - DocRegularStmt("returns from: "), - DocCodeStmt("return -1;"), - DocRegularStmt("\n"), - ) - ) - ) check( Switch::simpleSwitch, ge(4), @@ -40,23 +21,6 @@ internal class SwitchTest : UtValueTestCaseChecker(testClass = Switch::class) { { x, r -> (x == 11 || x == 12) && r == 12 }, // fall-through has it's own branch { x, r -> x == 13 && r == 13 }, { x, r -> x !in 10..13 && r == -1 }, // one for default is enough - summaryTextChecks = listOf( - keyContain(DocCodeStmt("return 10;")), - keyContain(DocCodeStmt("return 12;")), - keyContain(DocCodeStmt("return 12;")), - keyContain(DocCodeStmt("return 13;")), - keyMatch(switchCaseSummary) - ), - summaryNameChecks = listOf( - keyMatch("testSimpleSwitch_Return10"), - keyMatch("testSimpleSwitch_Return13"), - keyMatch("testSimpleSwitch_ReturnNegative1"), - ), - summaryDisplayNameChecks = listOf( - keyMatch("switch(x) case: 10 -> return 10"), - keyMatch("switch(x) case: 13 -> return 13"), - keyMatch("switch(x) case: Default -> return -1"), - ) ) } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumTest.kt index 8fb8b9b67b..4019cd0168 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumTest.kt @@ -1,18 +1,21 @@ package org.utbot.examples.enums -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate +import org.junit.jupiter.api.Disabled import org.utbot.examples.enums.ClassWithEnum.StatusEnum.ERROR import org.utbot.examples.enums.ClassWithEnum.StatusEnum.READY -import org.utbot.tests.infrastructure.isException 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 +import org.utbot.examples.enums.ClassWithEnum.StatusEnum import org.utbot.framework.plugin.api.util.jField import org.utbot.testcheckers.eq import org.utbot.testcheckers.withPushingStateFromPathSelectorForConcrete import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException class ClassWithEnumTest : UtValueTestCaseChecker(testClass = ClassWithEnum::class) { @Test @@ -36,26 +39,24 @@ class ClassWithEnumTest : UtValueTestCaseChecker(testClass = ClassWithEnum::clas fun testDifficultIfBranch() { check( ClassWithEnum::useEnumInDifficultIf, - eq(2), + ignoreExecutionsNumber, { s, r -> s.equals("TRYIF", ignoreCase = true) && r == 1 }, { s, r -> !s.equals("TRYIF", ignoreCase = true) && r == 2 }, ) } @Test - @Disabled("TODO JIRA:1686") + @Disabled("Seems not aligning with ksmt version used in USVM") fun testNullParameter() { check( ClassWithEnum::nullEnumAsParameter, - eq(3), + between(2..3), { e, _ -> e == null }, - { e, r -> e == READY && r == 0 }, - { e, r -> e == ERROR && r == -1 }, + { e, r -> e == READY && r == 0 || e == ERROR && r == -1 }, ) } @Test - @Disabled("TODO JIRA:1686") fun testNullField() { checkWithException( ClassWithEnum::nullField, @@ -66,22 +67,21 @@ class ClassWithEnumTest : UtValueTestCaseChecker(testClass = ClassWithEnum::clas ) } + @Suppress("KotlinConstantConditions") @Test - @Disabled("TODO JIRA:1686") fun testChangeEnum() { checkWithException( ClassWithEnum::changeEnum, - eq(3), - { e, r -> e == null && r.isException() }, + eq(2), { e, r -> e == READY && r.getOrNull()!! == ERROR.ordinal }, - { e, r -> e == ERROR && r.getOrNull()!! == READY.ordinal }, + { e, r -> (e == ERROR || e == null) && 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 + // should be enabled after solving https://github.com/UnitTestBot/UTBotJava/issues/80 withEnabledTestingCodeGeneration(testCodeGeneration = false) { checkWithException( ClassWithEnum::changeMutableField, @@ -93,7 +93,7 @@ class ClassWithEnumTest : UtValueTestCaseChecker(testClass = ClassWithEnum::clas } @Test - @Disabled("TODO JIRA:1686") + @Disabled("https://github.com/UnitTestBot/UTBotJava/issues/1745") fun testCheckName() { check( ClassWithEnum::checkName, @@ -110,21 +110,35 @@ class ClassWithEnumTest : UtValueTestCaseChecker(testClass = ClassWithEnum::clas ClassWithEnum::changingStaticWithEnumInit, eq(1), { t, staticsAfter, r -> - // for some reasons x is inaccessible - val x = FieldId(t.javaClass.id, "x").jField.get(t) as Int + // We cannot check `x` since it is not a meaningful value since + // it is accessed only in a static initializer. + + // For some reasons x is inaccessible + // val x = FieldId(t.javaClass.id, "x").jField.get(t) as Int val y = staticsAfter[FieldId(ClassWithEnum.ClassWithStaticField::class.id, "y")]!!.value as Int - val areStaticsCorrect = x == 1 && y == 11 + val areStaticsCorrect = /*x == 1 &&*/ y == 11 areStaticsCorrect && r == true } ) } + @Test + fun testVirtualFunction() { + check( + ClassWithEnum::virtualFunction, + eq(3), + { parameter, _ -> parameter == null }, + { parameter, r -> r == 1 && parameter == ERROR }, + { parameter, r -> r == 0 && parameter == READY }, + ) + } + @Test fun testEnumValues() { checkStaticMethod( - ClassWithEnum.StatusEnum::values, + StatusEnum::values, eq(1), { r -> r.contentEquals(arrayOf(READY, ERROR)) }, ) @@ -133,7 +147,7 @@ class ClassWithEnumTest : UtValueTestCaseChecker(testClass = ClassWithEnum::clas @Test fun testFromCode() { checkStaticMethod( - ClassWithEnum.StatusEnum::fromCode, + StatusEnum::fromCode, eq(3), { code, r -> code == 10 && r == READY }, { code, r -> code == -10 && r == ERROR }, @@ -144,7 +158,7 @@ class ClassWithEnumTest : UtValueTestCaseChecker(testClass = ClassWithEnum::clas @Test fun testFromIsReady() { checkStaticMethod( - ClassWithEnum.StatusEnum::fromIsReady, + StatusEnum::fromIsReady, eq(2), { isFirst, r -> isFirst && r == READY }, { isFirst, r -> !isFirst && r == ERROR }, @@ -152,14 +166,11 @@ class ClassWithEnumTest : UtValueTestCaseChecker(testClass = ClassWithEnum::clas } @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 + StatusEnum::publicGetCode, + between(1..2), + { enumInstance, r -> enumInstance == READY && r == 10 || enumInstance == ERROR && r == -10 }, ) } @@ -168,7 +179,7 @@ class ClassWithEnumTest : UtValueTestCaseChecker(testClass = ClassWithEnum::clas withPushingStateFromPathSelectorForConcrete { check( ClassWithEnum::implementingInterfaceEnumInDifficultBranch, - eq(2), + ignoreExecutionsNumber, { s, r -> s.equals("SUCCESS", ignoreCase = true) && r == 0 }, { s, r -> !s.equals("SUCCESS", ignoreCase = true) && r == 2 }, ) diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/enums/ComplexEnumExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/enums/ComplexEnumExamplesTest.kt index 901245dec5..1a3a258d82 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/enums/ComplexEnumExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/enums/ComplexEnumExamplesTest.kt @@ -1,24 +1,18 @@ package org.utbot.examples.enums -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.utbot.examples.enums.ComplexEnumExamples.Color import org.utbot.examples.enums.ComplexEnumExamples.Color.BLUE import org.utbot.examples.enums.ComplexEnumExamples.Color.GREEN import org.utbot.examples.enums.ComplexEnumExamples.Color.RED -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber class ComplexEnumExamplesTest : UtValueTestCaseChecker( testClass = ComplexEnumExamples::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testEnumToEnumMapCountValues() { @@ -75,7 +69,6 @@ class ComplexEnumExamplesTest : UtValueTestCaseChecker( } @Test - @Disabled("TODO: nested anonymous classes are not supported: https://github.com/UnitTestBot/UTBotJava/issues/617") fun testFindState() { check( ComplexEnumExamples::findState, diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/ExceptionClusteringChecker.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/ExceptionClusteringChecker.kt index 53abd8bf53..4e67505768 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/ExceptionClusteringChecker.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/ExceptionClusteringChecker.kt @@ -1,8 +1,5 @@ package org.utbot.examples.exceptions -import org.utbot.tests.infrastructure.UtModelTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.primitiveValue import org.utbot.framework.plugin.api.UtExecutionSuccess import org.utbot.framework.plugin.api.UtExplicitlyThrownException import org.utbot.framework.plugin.api.UtImplicitlyThrownException @@ -10,6 +7,9 @@ import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.UtTimeoutException import org.junit.jupiter.api.Test import org.utbot.testcheckers.ge +import org.utbot.testing.UtModelTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.primitiveValue internal class ExceptionClusteringChecker : UtModelTestCaseChecker(testClass = ExceptionClusteringExamples::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/ExceptionExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/ExceptionExamplesTest.kt index 96bc056f47..efef1f5990 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/ExceptionExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/ExceptionExamplesTest.kt @@ -1,21 +1,18 @@ package org.utbot.examples.exceptions -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException internal class ExceptionExamplesTest : UtValueTestCaseChecker( testClass = ExceptionExamples::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) // TODO: fails because we construct lists with generics - ) + // TODO: Kotlin code generation fails because we construct lists with generics + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testInitAnArray() { @@ -106,6 +103,23 @@ internal class ExceptionExamplesTest : UtValueTestCaseChecker( ) } + /** + * Covers [#656](https://github.com/UnitTestBot/UTBotJava/issues/656). + */ + @Test + fun testCatchExceptionAfterOtherPossibleException() { + withoutConcrete { + checkWithException( + ExceptionExamples::catchExceptionAfterOtherPossibleException, + eq(3), + { i, r -> i == -1 && r.isException() }, + { i, r -> i == 0 && r.getOrThrow() == 2 }, + { i, r -> r.getOrThrow() == 1 }, + coverage = atLeast(100) + ) + } + } + /** * Used for path generation check in [org.utbot.engine.Traverser.fullPath] */ diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/JvmCrashExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/JvmCrashExamplesTest.kt index 2e654c4f06..6a9449d4f7 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/JvmCrashExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/exceptions/JvmCrashExamplesTest.kt @@ -1,11 +1,11 @@ package org.utbot.examples.exceptions -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.withoutSandbox +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class JvmCrashExamplesTest : UtValueTestCaseChecker(testClass = JvmCrashExamples::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/InvokeExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/InvokeExampleTest.kt index 4f8379a08e..5abc14bd7d 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/InvokeExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/InvokeExampleTest.kt @@ -1,11 +1,11 @@ package org.utbot.examples.invokes -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException internal class InvokeExampleTest : UtValueTestCaseChecker(testClass = InvokeExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/NativeExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/NativeExampleTest.kt index 1323697b42..f437801c68 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/NativeExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/NativeExampleTest.kt @@ -1,14 +1,14 @@ package org.utbot.examples.invokes -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.ignoreExecutionsNumber import kotlin.math.ln import kotlin.math.sqrt import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.ignoreExecutionsNumber internal class NativeExampleTest : UtValueTestCaseChecker(testClass = NativeExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/SimpleInterfaceExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/SimpleInterfaceExampleTest.kt index 8b1fc9bec7..98e19a19b8 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/SimpleInterfaceExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/SimpleInterfaceExampleTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.invokes -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class SimpleInterfaceExampleTest : UtValueTestCaseChecker( testClass = SimpleInterfaceExample::class diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/StaticInvokeExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/StaticInvokeExampleTest.kt index 34c54d5a29..5c75b0f0cb 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/StaticInvokeExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/StaticInvokeExampleTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.invokes -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.between import kotlin.math.max import org.junit.jupiter.api.Test +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between internal class StaticInvokeExampleTest : UtValueTestCaseChecker(testClass = StaticInvokeExample::class) { // TODO: inline local variables when types inference bug in Kotlin fixed diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/VirtualInvokeExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/VirtualInvokeExampleTest.kt index ba1ff4814a..57536896dc 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/VirtualInvokeExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/invokes/VirtualInvokeExampleTest.kt @@ -2,12 +2,12 @@ package org.utbot.examples.invokes -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isException import java.lang.Boolean import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException internal class VirtualInvokeExampleTest : UtValueTestCaseChecker(testClass = VirtualInvokeExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/CustomPredicateExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/CustomPredicateExampleTest.kt index 2327358339..39c88329e8 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/CustomPredicateExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/CustomPredicateExampleTest.kt @@ -3,21 +3,18 @@ package org.utbot.examples.lambda import org.junit.jupiter.api.Test import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException +import org.utbot.testing.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException class CustomPredicateExampleTest : UtValueTestCaseChecker( testClass = CustomPredicateExample::class, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - // TODO: https://github.com/UnitTestBot/UTBotJava/issues/88 (generics in Kotlin) - // At the moment, when we create an instance of a functional interface via lambda (through reflection), - // we need to do a type cast (e.g. `obj as Predicate`), but since generics are not supported yet, - // we use a raw type (e.g. `Predicate`) instead (which is not allowed in Kotlin). - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + // TODO: https://github.com/UnitTestBot/UTBotJava/issues/88 (generics in Kotlin) + // At the moment, when we create an instance of a functional interface via lambda (through reflection), + // we need to do a type cast (e.g. `obj as Predicate`), but since generics are not supported yet, + // we use a raw type (e.g. `Predicate`) instead (which is not allowed in Kotlin). + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testNoCapturedValuesPredicateCheck() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/PredicateNotExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/PredicateNotExampleTest.kt index 659b6a63cc..739cff5964 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/PredicateNotExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/PredicateNotExampleTest.kt @@ -3,7 +3,7 @@ package org.utbot.examples.lambda import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.UtValueTestCaseChecker +import org.utbot.testing.UtValueTestCaseChecker class PredicateNotExampleTest : UtValueTestCaseChecker(testClass = PredicateNotExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/SimpleLambdaExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/SimpleLambdaExamplesTest.kt index d3a04948b2..251619631a 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/SimpleLambdaExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/SimpleLambdaExamplesTest.kt @@ -1,20 +1,15 @@ package org.utbot.examples.lambda import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException // TODO failed Kotlin compilation (generics) SAT-1332 class SimpleLambdaExamplesTest : UtValueTestCaseChecker( testClass = SimpleLambdaExamples::class, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration), - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testBiFunctionLambdaExample() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/ThrowingWithLambdaExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/ThrowingWithLambdaExampleTest.kt new file mode 100644 index 0000000000..fb338e5e1d --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/lambda/ThrowingWithLambdaExampleTest.kt @@ -0,0 +1,21 @@ +package org.utbot.examples.lambda + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker + +class ThrowingWithLambdaExampleTest : UtValueTestCaseChecker(testClass = ThrowingWithLambdaExample::class) { + @Test + fun testAnyExample() { + check( + ThrowingWithLambdaExample::anyExample, + eq(4), + { l, _, _ -> l == null }, + { l, _, r -> l.isEmpty() && r == false }, + { l, _, r -> l.isNotEmpty() && 42 in l && r == true }, + { l, _, r -> l.isNotEmpty() && 42 !in l && r == false }, + coverage = DoNotCalculate // TODO failed coverage calculation + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/make/symbolic/ClassWithComplicatedMethodsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/make/symbolic/ClassWithComplicatedMethodsTest.kt index ddba26f794..e2c81d6913 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/make/symbolic/ClassWithComplicatedMethodsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/make/symbolic/ClassWithComplicatedMethodsTest.kt @@ -1,16 +1,18 @@ package org.utbot.examples.make.symbolic -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MockStrategyApi import kotlin.math.abs import kotlin.math.sqrt import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq import org.utbot.testcheckers.withoutConcrete -import org.utbot.tests.infrastructure.Compilation +import org.utbot.testing.Compilation +import org.utbot.testing.Configuration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker // This class is substituted with ComplicatedMethodsSubstitutionsStorage // but we cannot do in code generation. @@ -18,10 +20,11 @@ import org.utbot.tests.infrastructure.Compilation internal class ClassWithComplicatedMethodsTest : UtValueTestCaseChecker( testClass = ClassWithComplicatedMethods::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA, Compilation), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, Compilation) - ) + configurations = listOf( + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.DO_NOT_PARAMETRIZE, Compilation), + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.PARAMETRIZE, Compilation), + Configuration(CodegenLanguage.KOTLIN, ParametrizedTestSource.DO_NOT_PARAMETRIZE, Compilation), + ), ) { @Test @Disabled("[SAT-1419]") diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/BitOperatorsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/BitOperatorsTest.kt index 7f9f63dbe6..a40a390fef 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/BitOperatorsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/BitOperatorsTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.math -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.atLeast import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast internal class BitOperatorsTest : UtValueTestCaseChecker(testClass = BitOperators::class) { @Test @@ -37,7 +37,7 @@ internal class BitOperatorsTest : UtValueTestCaseChecker(testClass = BitOperator } @Test - @kotlin.ExperimentalStdlibApi + @ExperimentalStdlibApi fun testAnd() { check( BitOperators::and, diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/DivRemExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/DivRemExamplesTest.kt index 8d8d8249a3..acd9124d5d 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/DivRemExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/DivRemExamplesTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.math -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException internal class DivRemExamplesTest : UtValueTestCaseChecker(testClass = DivRemExamples::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/DoubleFunctionsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/DoubleFunctionsTest.kt index 0f1c0a7db1..a69f6565f2 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/DoubleFunctionsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/DoubleFunctionsTest.kt @@ -1,13 +1,13 @@ package org.utbot.examples.math -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isException import kotlin.math.abs import kotlin.math.hypot import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException @Suppress("SimplifyNegatedBinaryExpression") internal class DoubleFunctionsTest : UtValueTestCaseChecker(testClass = DoubleFunctions::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/OverflowAsErrorTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/OverflowAsErrorTest.kt index 558fe403d3..4120937cd0 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/OverflowAsErrorTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/math/OverflowAsErrorTest.kt @@ -2,16 +2,14 @@ package org.utbot.examples.math import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.AtLeast +import org.utbot.framework.plugin.api.OverflowDetectionError import org.utbot.examples.algorithms.Sort -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException +import org.utbot.framework.codegen.domain.ParametrizedTestSource import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq import org.utbot.testcheckers.withSolverTimeoutInMillis import org.utbot.testcheckers.withTreatingOverflowAsError -import org.utbot.tests.infrastructure.Compilation +import org.utbot.testing.* import kotlin.math.floor import kotlin.math.sqrt @@ -21,8 +19,9 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( // Don't launch tests, because ArithmeticException will be expected, but it is not supposed to be actually thrown. // ArithmeticException acts as a sign of Overflow. listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA, Compilation), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, Compilation), + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.DO_NOT_PARAMETRIZE, Compilation), + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.PARAMETRIZE, Compilation), + Configuration(CodegenLanguage.KOTLIN, ParametrizedTestSource.DO_NOT_PARAMETRIZE, Compilation), ) ) { @Test @@ -31,12 +30,23 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( 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 -> + val overflowOccurred = kotlin.runCatching { + Math.multiplyExact(x, x) + }.isFailure + overflowOccurred && r.isException() + }, // through overflow + { x, _, r -> + val twoMul = Math.multiplyExact(x, x) + val overflowOccurred = kotlin.runCatching { + Math.multiplyExact(twoMul, x) + }.isFailure + overflowOccurred && 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), + coverage = DoNotCalculate ) } } @@ -47,27 +57,43 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::byteAddOverflow, eq(2), - { _, _, r -> !r.isException() }, + { _, _, 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() + (negOverflow || posOverflow) && r.isException() }, // through overflow ) } } + @Test + fun testByteWithIntOverflow() { + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::byteWithIntOverflow, + eq(2), + { x, y, r -> + runCatching { + Math.addExact(x.toInt(), y) + }.isFailure && r.isException() + }, + { x, y, r -> Math.addExact(x.toInt(), y).toByte() == r.getOrThrow() } + ) + } + } + @Test fun testByteSubOverflow() { withTreatingOverflowAsError { checkWithException( OverflowExamples::byteSubOverflow, eq(2), - { _, _, r -> !r.isException() }, + { _, _, 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() + (negOverflow || posOverflow) && r.isException() }, // through overflow ) } @@ -79,8 +105,8 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::byteMulOverflow, eq(2), - { _, _, r -> !r.isException() }, - { _, _, r -> r.isException() }, // through overflow + { _, _, r -> !r.isException() }, + { _, _, r -> r.isException() }, // through overflow ) } } @@ -91,11 +117,11 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::shortAddOverflow, eq(2), - { _, _, r -> !r.isException() }, + { _, _, 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() + (negOverflow || posOverflow) && r.isException() }, // through overflow ) } @@ -107,11 +133,11 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::shortSubOverflow, eq(2), - { _, _, r -> !r.isException() }, + { _, _, 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() + (negOverflow || posOverflow) && r.isException() }, // through overflow ) } @@ -123,8 +149,8 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::shortMulOverflow, eq(2), - { _, _, r -> !r.isException() }, - { _, _, r -> r.isException() }, // through overflow + { _, _, r -> !r.isException() }, + { _, _, r -> r.isException() }, // through overflow ) } } @@ -135,11 +161,11 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::intAddOverflow, eq(2), - { _, _, r -> !r.isException() }, + { _, _, 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() + (negOverflow || posOverflow) && r.isException() }, // through overflow ) } @@ -151,11 +177,11 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::intSubOverflow, eq(2), - { _, _, r -> !r.isException() }, + { _, _, 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() + (negOverflow || posOverflow) && r.isException() }, // through overflow ) } @@ -171,8 +197,8 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::intMulOverflow, eq(2), - { _, _, r -> !r.isException() }, - { _, _, r -> r.isException() }, // through overflow + { _, _, r -> !r.isException() }, + { _, _, r -> r.isException() }, // through overflow ) } } @@ -184,11 +210,11 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::longAddOverflow, eq(2), - { _, _, r -> !r.isException() }, + { _, _, 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() + (negOverflow || posOverflow) && r.isException() }, // through overflow ) } @@ -200,11 +226,11 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::longSubOverflow, eq(2), - { _, _, r -> !r.isException() }, + { _, _, 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() + (negOverflow || posOverflow) && r.isException() }, // through overflow ) } @@ -221,8 +247,8 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::longMulOverflow, eq(2), - { _, _, r -> !r.isException() }, - { _, _, r -> r.isException() }, // through overflow + { _, _, r -> !r.isException() }, + { _, _, r -> r.isException() }, // through overflow ) } } @@ -234,8 +260,8 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( OverflowExamples::incOverflow, eq(2), - { _, r -> !r.isException() }, - { _, r -> r.isException() }, // through overflow + { _, r -> !r.isException() }, + { _, r -> r.isException() }, // through overflow ) } } @@ -251,13 +277,13 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( // 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 + { 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 + // Generated Kotlin code does not compile, so disabled for now @Test @Disabled fun testQuickSort() { @@ -265,8 +291,8 @@ internal class OverflowAsErrorTest : UtValueTestCaseChecker( checkWithException( Sort::quickSort, ignoreExecutionsNumber, - { _, _, _, r -> !r.isException() }, - { _, _, _, r -> r.isException() }, // through overflow + { _, _, _, r -> !r.isException() }, + { _, _, _, r -> r.isException() }, // through overflow ) } } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/LoggerExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/LoggerExampleTest.kt index 128d55e97c..c60e740445 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/LoggerExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/LoggerExampleTest.kt @@ -1,8 +1,5 @@ package org.utbot.examples.mixed -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.UtConcreteValue import org.utbot.framework.plugin.api.UtInstrumentation import org.utbot.framework.plugin.api.UtModel @@ -10,15 +7,13 @@ import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation import org.utbot.framework.plugin.api.isNull import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class LoggerExampleTest : UtValueTestCaseChecker( testClass = LoggerExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testExample() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/MonitorUsageTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/MonitorUsageTest.kt index 92c29a00f1..09974c3c22 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/MonitorUsageTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/MonitorUsageTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.mixed -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.ignoreExecutionsNumber import org.junit.jupiter.api.Test +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.ignoreExecutionsNumber internal class MonitorUsageTest : UtValueTestCaseChecker(testClass = MonitorUsage::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/OverloadTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/OverloadTest.kt index 7bf7baa316..b6012d8d8b 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/OverloadTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/OverloadTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.mixed -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class OverloadTest : UtValueTestCaseChecker(testClass = Overload::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/PrivateConstructorExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/PrivateConstructorExampleTest.kt index 6884cb0d09..bcf8230e6c 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/PrivateConstructorExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/PrivateConstructorExampleTest.kt @@ -1,11 +1,14 @@ package org.utbot.examples.mixed -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker -internal class PrivateConstructorExampleTest : UtValueTestCaseChecker(testClass = PrivateConstructorExample::class) { +internal class PrivateConstructorExampleTest : UtValueTestCaseChecker( + testClass = PrivateConstructorExample::class, +) { /** * Two branches need to be covered: diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SerializableExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SerializableExampleTest.kt new file mode 100644 index 0000000000..1897093f61 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SerializableExampleTest.kt @@ -0,0 +1,18 @@ +package org.utbot.examples.mixed + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker + +internal class SerializableExampleTest : UtValueTestCaseChecker(testClass = SerializableExample::class) { + + @Test + fun testExample() { + check( + SerializableExample::example, + eq(1), + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SimpleNoConditionTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SimpleNoConditionTest.kt index a68bd75eef..8bddaa3e66 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SimpleNoConditionTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SimpleNoConditionTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.mixed -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class SimpleNoConditionTest : UtValueTestCaseChecker(testClass = SimpleNoCondition::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SimplifierTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SimplifierTest.kt index c6358e1024..0c6b13aacd 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SimplifierTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/SimplifierTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.mixed -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class SimplifierTest: UtValueTestCaseChecker(testClass = Simplifier::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/StaticInitializerExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/StaticInitializerExampleTest.kt index 97d959117b..fd31bb20fe 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/StaticInitializerExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/StaticInitializerExampleTest.kt @@ -3,8 +3,8 @@ package org.utbot.examples.mixed import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.examples.StaticInitializerExample -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker @Disabled("Unknown build failure") internal class StaticInitializerExampleTest : UtValueTestCaseChecker(testClass = StaticInitializerExample::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/StaticMethodExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/StaticMethodExamplesTest.kt index 34bbf5a7fe..7e71562964 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/StaticMethodExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mixed/StaticMethodExamplesTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.mixed -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class StaticMethodExamplesTest : UtValueTestCaseChecker(testClass = StaticMethodExamples::class) { // TODO: inline local variables when types inference bug in Kotlin fixed diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/ArgumentsMockTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/ArgumentsMockTest.kt index 36e997b811..00c1bcfd13 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/ArgumentsMockTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/ArgumentsMockTest.kt @@ -1,17 +1,17 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.isParameter import org.utbot.examples.mock.provider.Provider import org.utbot.examples.mock.service.impl.ExampleClass import org.utbot.examples.mock.service.impl.ServiceWithArguments -import org.utbot.tests.infrastructure.mocksMethod -import org.utbot.tests.infrastructure.value import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_PACKAGES import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.isParameter +import org.utbot.testing.mocksMethod +import org.utbot.testing.value internal class ArgumentsMockTest : UtValueTestCaseChecker(testClass = ServiceWithArguments::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/CommonMocksExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/CommonMocksExampleTest.kt index 0e5d1dc4f8..ae2609d787 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/CommonMocksExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/CommonMocksExampleTest.kt @@ -1,20 +1,35 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.framework.plugin.api.MockStrategyApi import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast internal class CommonMocksExampleTest: UtValueTestCaseChecker(testClass = CommonMocksExample::class) { + + //TODO: coverage values here require further investigation by experts + + @Test + fun testMockInterfaceWithoutImplementorsWithNoMocksStrategy() { + checkMocks( + CommonMocksExample::mockInterfaceWithoutImplementors, + eq(1), + { v, mocks, _ -> v == null && mocks.isEmpty() }, + mockStrategy = MockStrategyApi.NO_MOCKS, + coverage = atLeast(75), + ) + } + @Test - fun testMockInterfaceWithoutImplementors() { + fun testMockInterfaceWithoutImplementorsWithMockingStrategy() { checkMocks( CommonMocksExample::mockInterfaceWithoutImplementors, eq(2), { v, mocks, _ -> v == null && mocks.isEmpty() }, { _, mocks, _ -> mocks.singleOrNull() != null }, - coverage = DoNotCalculate + mockStrategy = MockStrategyApi.OTHER_CLASSES, + coverage = atLeast(75), ) } @@ -27,7 +42,7 @@ internal class CommonMocksExampleTest: UtValueTestCaseChecker(testClass = Common { fst, _, mocks, _ -> fst == null && mocks.isEmpty() }, { _, _, mocks, _ -> mocks.isEmpty() }, // should be changed to not null fst when 1449 will be finished mockStrategy = MockStrategyApi.OTHER_PACKAGES, - coverage = DoNotCalculate + coverage = atLeast(75) ) } @@ -42,7 +57,7 @@ internal class CommonMocksExampleTest: UtValueTestCaseChecker(testClass = Common // node == node.next // node.next.value == node.value + 1 mockStrategy = MockStrategyApi.OTHER_CLASSES, - coverage = DoNotCalculate + coverage = atLeast(13) ) } @@ -53,7 +68,17 @@ internal class CommonMocksExampleTest: UtValueTestCaseChecker(testClass = Common eq(1), { r -> r == -420 }, mockStrategy = MockStrategyApi.OTHER_CLASSES, - coverage = DoNotCalculate + coverage = atLeast(70), + ) + } + + @Test + fun testMocksForNullOfDifferentTypes() { + check( + CommonMocksExample::mocksForNullOfDifferentTypes, + eq(1), + mockStrategy = MockStrategyApi.OTHER_PACKAGES, + coverage = atLeast(75) ) } } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/FieldMockTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/FieldMockTest.kt index caa273ea18..ff94de21b2 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/FieldMockTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/FieldMockTest.kt @@ -1,18 +1,21 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between import org.utbot.examples.mock.provider.Provider import org.utbot.examples.mock.service.impl.ExampleClass import org.utbot.examples.mock.service.impl.ServiceWithField -import org.utbot.tests.infrastructure.mocksMethod -import org.utbot.tests.infrastructure.value import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_PACKAGES import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.mocksMethod +import org.utbot.testing.value -internal class FieldMockTest : UtValueTestCaseChecker(testClass = ServiceWithField::class) { +internal class FieldMockTest : UtValueTestCaseChecker( + testClass = ServiceWithField::class, +) { @Test fun testMockForField_callMultipleMethods() { checkMocksAndInstrumentation( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/InnerMockWithFieldChecker.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/InnerMockWithFieldChecker.kt index f648affdce..30a746e0c1 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/InnerMockWithFieldChecker.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/InnerMockWithFieldChecker.kt @@ -1,15 +1,15 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtModelTestCaseChecker -import org.utbot.tests.infrastructure.primitiveValue import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_PACKAGES import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.getOrThrow import org.utbot.framework.plugin.api.isMockModel import org.utbot.framework.plugin.api.isNotNull import org.utbot.framework.plugin.api.isNull import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtModelTestCaseChecker +import org.utbot.testing.getOrThrow +import org.utbot.testing.primitiveValue internal class InnerMockWithFieldChecker : UtModelTestCaseChecker(testClass = InnerMockWithFieldExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockFinalClassTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockFinalClassTest.kt index 0e96e4ee1a..8f0f2ad1b3 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockFinalClassTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockFinalClassTest.kt @@ -1,15 +1,18 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.mock.others.FinalClass -import org.utbot.tests.infrastructure.singleMock -import org.utbot.tests.infrastructure.value import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_CLASSES import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.ge +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.singleMock +import org.utbot.testing.value -internal class MockFinalClassTest : UtValueTestCaseChecker(testClass = MockFinalClassExample::class) { +internal class MockFinalClassTest : UtValueTestCaseChecker( + testClass = MockFinalClassExample::class, +) { @Test fun testFinalClass() { checkMocks( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockRandomTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockRandomTest.kt index 05e1793014..5d7bba68ff 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockRandomTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockRandomTest.kt @@ -1,28 +1,23 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isParameter -import org.utbot.tests.infrastructure.mockValues -import org.utbot.tests.infrastructure.mocksMethod -import org.utbot.tests.infrastructure.singleMock -import org.utbot.tests.infrastructure.value import org.utbot.framework.plugin.api.UtCompositeModel import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation import java.util.Random import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isParameter +import org.utbot.testing.mockValues +import org.utbot.testing.mocksMethod +import org.utbot.testing.singleMock +import org.utbot.testing.value // TODO Kotlin mocks generics https://github.com/UnitTestBot/UTBotJava/issues/88 internal class MockRandomTest : UtValueTestCaseChecker( testClass = MockRandomExamples::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testRandomAsParameter() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockReturnObjectExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockReturnObjectExampleTest.kt index 446f5d25c6..51cd5f279b 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockReturnObjectExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockReturnObjectExampleTest.kt @@ -1,17 +1,17 @@ package org.utbot.examples.mock import org.junit.jupiter.api.Disabled -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.mock.others.Generator import org.utbot.examples.mock.others.Locator -import org.utbot.tests.infrastructure.mockValue -import org.utbot.tests.infrastructure.singleMock -import org.utbot.tests.infrastructure.singleMockOrNull -import org.utbot.tests.infrastructure.value import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_PACKAGES import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.mockValue +import org.utbot.testing.singleMock +import org.utbot.testing.singleMockOrNull +import org.utbot.testing.value internal class MockReturnObjectExampleTest : UtValueTestCaseChecker(testClass = MockReturnObjectExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockStaticFieldExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockStaticFieldExampleTest.kt index 907d9b5829..a08efc9175 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockStaticFieldExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockStaticFieldExampleTest.kt @@ -1,20 +1,28 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.mock.others.Generator -import org.utbot.tests.infrastructure.singleMock -import org.utbot.tests.infrastructure.singleMockOrNull -import org.utbot.tests.infrastructure.value import org.utbot.framework.plugin.api.FieldMockTarget import org.utbot.framework.plugin.api.MockInfo import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_PACKAGES import kotlin.reflect.KClass import org.junit.jupiter.api.Test +import org.utbot.examples.mock.others.ClassWithStaticField +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation import org.utbot.testcheckers.eq import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.singleMock +import org.utbot.testing.singleMockOrNull +import org.utbot.testing.value -internal class MockStaticFieldExampleTest : UtValueTestCaseChecker(testClass = MockStaticFieldExample::class) { +internal class MockStaticFieldExampleTest : UtValueTestCaseChecker( + testClass = MockStaticFieldExample::class, + testCodeGeneration = true, + // disabled due to https://github.com/UnitTestBot/UTBotJava/issues/88 + configurations = ignoreKotlinCompilationConfigurations, +) { @Test fun testMockStaticField() { @@ -68,6 +76,42 @@ internal class MockStaticFieldExampleTest : UtValueTestCaseChecker(testClass = M } } + @Test + fun testCheckMocksInLeftAndRightAssignPartFinalField() { + checkMocks( + MockStaticFieldExample::checkMocksInLeftAndRightAssignPartFinalField, + eq(1), + { mocks, _ -> + val mock = mocks.singleMock("staticFinalField", ClassWithStaticField::foo) + + mock.mocksStaticField(MockStaticFieldExample::class) + }, + coverage = DoNotCalculate, + mockStrategy = OTHER_PACKAGES + ) + } + + @Test + fun testCheckMocksInLeftAndRightAssignPart() { + checkMocksAndInstrumentation( + MockStaticFieldExample::checkMocksInLeftAndRightAssignPart, + eq(2), + { _, statics, _ -> + val instrumentation = statics.single() as UtNewInstanceInstrumentation + val model = instrumentation.instances.last() as UtCompositeModel + + model.fields.isEmpty() // NPE + }, + { mocks, _, _ -> + val mock = mocks.singleMock("staticField", ClassWithStaticField::foo) + + mock.mocksStaticField(MockStaticFieldExample::class) + }, + coverage = DoNotCalculate, + mockStrategy = OTHER_PACKAGES + ) + } + private fun MockInfo.mocksStaticField(kClass: KClass<*>) = when (val mock = mock) { is FieldMockTarget -> mock.ownerClassName == kClass.qualifiedName && mock.owner == null else -> false diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockStaticMethodExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockStaticMethodExampleTest.kt index 37ffbefa2f..1515664910 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockStaticMethodExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockStaticMethodExampleTest.kt @@ -1,25 +1,21 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.framework.util.singleModel import org.utbot.framework.util.singleStaticMethod import org.utbot.framework.util.singleValue import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.util.id import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker // TODO Kotlin mocks generics https://github.com/UnitTestBot/UTBotJava/issues/88 internal class MockStaticMethodExampleTest : UtValueTestCaseChecker( testClass = MockStaticMethodExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testUseStaticMethod() { @@ -46,4 +42,14 @@ internal class MockStaticMethodExampleTest : UtValueTestCaseChecker( mockStrategy = MockStrategyApi.OTHER_PACKAGES ) } + + @Test + fun testMockStaticMethodFromAlwaysMockClass() { + checkMocksAndInstrumentation( + MockStaticMethodExample::mockStaticMethodFromAlwaysMockClass, + eq(1), + coverage = DoNotCalculate, + additionalMockAlwaysClasses = setOf(System::class.id) + ) + } } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockWithFieldChecker.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockWithFieldChecker.kt index ac185f2b82..397981d54e 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockWithFieldChecker.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockWithFieldChecker.kt @@ -1,14 +1,14 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtModelTestCaseChecker -import org.utbot.tests.infrastructure.primitiveValue import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_PACKAGES import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.getOrThrow import org.utbot.framework.plugin.api.isMockModel import org.utbot.framework.plugin.api.isNull import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtModelTestCaseChecker +import org.utbot.testing.getOrThrow +import org.utbot.testing.primitiveValue internal class MockWithFieldChecker : UtModelTestCaseChecker(testClass = MockWithFieldExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockWithSideEffectExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockWithSideEffectExampleTest.kt index 5b234c9e0f..bb31390546 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockWithSideEffectExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/MockWithSideEffectExampleTest.kt @@ -1,11 +1,11 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isException import org.utbot.framework.plugin.api.MockStrategyApi import org.junit.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException internal class MockWithSideEffectExampleTest : UtValueTestCaseChecker(testClass = MockWithSideEffectExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/StaticFieldMockTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/StaticFieldMockTest.kt index 4207e1585c..e963a1e6b6 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/StaticFieldMockTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/StaticFieldMockTest.kt @@ -1,16 +1,15 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.mock.provider.Provider import org.utbot.examples.mock.service.impl.ExampleClass import org.utbot.examples.mock.service.impl.ServiceWithStaticField -import org.utbot.tests.infrastructure.mocksMethod -import org.utbot.tests.infrastructure.value import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_PACKAGES import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq - +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.mocksMethod +import org.utbot.testing.value internal class StaticFieldMockTest : UtValueTestCaseChecker(testClass = ServiceWithStaticField::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/UseNetworkTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/UseNetworkTest.kt index 277ed3aff0..772990f206 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/UseNetworkTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/UseNetworkTest.kt @@ -1,12 +1,12 @@ package org.utbot.examples.mock -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isException import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.UtConcreteValue import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException internal class UseNetworkTest : UtValueTestCaseChecker(testClass = UseNetwork::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/aliasing/AliasingInParamsExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/aliasing/AliasingInParamsExampleTest.kt index 6177ac7922..6efcdf3089 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/aliasing/AliasingInParamsExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/aliasing/AliasingInParamsExampleTest.kt @@ -1,10 +1,10 @@ package org.utbot.examples.mock.aliasing -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.framework.plugin.api.MockStrategyApi import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class AliasingInParamsExampleTest : UtValueTestCaseChecker(testClass = AliasingInParamsExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/fields/ClassUsingClassWithRandomFieldTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/fields/ClassUsingClassWithRandomFieldTest.kt new file mode 100644 index 0000000000..bca47545b0 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/fields/ClassUsingClassWithRandomFieldTest.kt @@ -0,0 +1,37 @@ +package org.utbot.examples.mock.fields + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.id +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker + +class ClassUsingClassWithRandomFieldTest : UtValueTestCaseChecker( + testClass = ClassUsingClassWithRandomField::class, + testCodeGeneration = true, + // because of mocks + configurations = ignoreKotlinCompilationConfigurations, +) { + @Test + fun testUseClassWithRandomField() { + checkMocksAndInstrumentation( + ClassUsingClassWithRandomField::useClassWithRandomField, + eq(1), + { mocks, instrumentation, r -> + val noMocks = mocks.isEmpty() + + val constructorMock = instrumentation.single() as UtNewInstanceInstrumentation + val classIdEquality = constructorMock.classId == java.util.Random::class.id + val callSiteIdEquality = constructorMock.callSites.single() == ClassWithRandomField::class.id + val instance = constructorMock.instances.single() as UtCompositeModel + val methodMock = instance.mocks.entries.single() + val methodNameEquality = methodMock.key.name == "nextInt" + val mockValueResult = r == (methodMock.value.single() as UtPrimitiveModel).value as Int + + noMocks && classIdEquality && callSiteIdEquality && instance.isMock && methodNameEquality && mockValueResult + } + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/model/FieldMockChecker.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/model/FieldMockChecker.kt index 25cd7d4d88..07fb1ae337 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/model/FieldMockChecker.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/model/FieldMockChecker.kt @@ -2,14 +2,14 @@ package org.utbot.examples.mock.model import org.utbot.examples.mock.provider.impl.ProviderImpl import org.utbot.examples.mock.service.impl.ServiceWithField -import org.utbot.tests.infrastructure.primitiveValue import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_PACKAGES import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.isNotNull import org.utbot.framework.plugin.api.isNull import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.UtModelTestCaseChecker import org.utbot.testcheckers.eq +import org.utbot.testing.UtModelTestCaseChecker +import org.utbot.testing.primitiveValue internal class FieldMockChecker : UtModelTestCaseChecker(testClass = ServiceWithField::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/model/UseNetworkModelBasedTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/model/UseNetworkModelBasedTest.kt index 063c199216..b405533f6c 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/model/UseNetworkModelBasedTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/mock/model/UseNetworkModelBasedTest.kt @@ -1,12 +1,12 @@ package org.utbot.examples.mock.model -import org.utbot.tests.infrastructure.UtModelTestCaseChecker import org.utbot.examples.mock.UseNetwork import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.UtCompositeModel import org.utbot.framework.plugin.api.UtVoidModel import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtModelTestCaseChecker internal class UseNetworkModelBasedTest : UtModelTestCaseChecker(testClass = UseNetwork::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/models/CompositeModelMinimizationChecker.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/models/CompositeModelMinimizationChecker.kt index dcd76468f3..b8b7d11c2d 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/models/CompositeModelMinimizationChecker.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/models/CompositeModelMinimizationChecker.kt @@ -1,27 +1,24 @@ package org.utbot.examples.models -import org.utbot.tests.infrastructure.UtModelTestCaseChecker -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtCompositeModel import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.UtReferenceModel import org.junit.Test +import org.utbot.framework.plugin.api.UtCustomModel import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtModelTestCaseChecker internal class CompositeModelMinimizationChecker : UtModelTestCaseChecker( testClass = CompositeModelMinimizationExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { private fun UtModel.getFieldsOrNull(): Map? = when(this) { is UtAssembleModel -> origin?.fields is UtCompositeModel -> fields + is UtCustomModel -> origin?.fields else -> null } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/models/ModelsIdEqualityChecker.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/models/ModelsIdEqualityChecker.kt index 2a13e9a976..f528a9a6e7 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/models/ModelsIdEqualityChecker.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/models/ModelsIdEqualityChecker.kt @@ -1,7 +1,5 @@ package org.utbot.examples.models -import org.utbot.tests.infrastructure.UtModelTestCaseChecker -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtDirectSetFieldModel @@ -9,16 +7,13 @@ import org.utbot.framework.plugin.api.UtExecutionSuccess import org.utbot.framework.plugin.api.UtReferenceModel import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtModelTestCaseChecker // TODO failed Kotlin compilation SAT-1332 internal class ModelsIdEqualityChecker : UtModelTestCaseChecker( testClass = ModelsIdEqualityExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testObjectItself() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/natives/NativeExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/natives/NativeExamplesTest.kt index 863bff2d53..1873e21210 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/natives/NativeExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/natives/NativeExamplesTest.kt @@ -1,22 +1,17 @@ package org.utbot.examples.natives -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge import org.utbot.testcheckers.withSolverTimeoutInMillis -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker // TODO Kotlin mocks generics https://github.com/UnitTestBot/UTBotJava/issues/88 internal class NativeExamplesTest : UtValueTestCaseChecker( testClass = NativeExamples::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/AbstractAnonymousClassTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/AbstractAnonymousClassTest.kt new file mode 100644 index 0000000000..c0f7304310 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/AbstractAnonymousClassTest.kt @@ -0,0 +1,27 @@ +package org.utbot.examples.objects + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker + +class AbstractAnonymousClassTest : UtValueTestCaseChecker(testClass = AbstractAnonymousClass::class) { + @Test + fun testNonOverriddenMethod() { + check( + AbstractAnonymousClass::methodWithoutOverrides, + eq(1) + ) + } + + @Test + fun testOverriddenMethod() { + // check we have error during execution + assertThrows { + check( + AbstractAnonymousClass::methodWithOverride, + eq(0) + ) + } + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/AnonymousClassesExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/AnonymousClassesExampleTest.kt index 11e152f510..c3e7ed1ecf 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/AnonymousClassesExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/AnonymousClassesExampleTest.kt @@ -1,12 +1,15 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.Full -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq +import org.utbot.testing.Full +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException -class AnonymousClassesExampleTest : UtValueTestCaseChecker(testClass = AnonymousClassesExample::class) { +class AnonymousClassesExampleTest : UtValueTestCaseChecker( + testClass = AnonymousClassesExample::class, +) { @Test fun testAnonymousClassAsParam() { checkWithException( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassForTestClinitSectionsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassForTestClinitSectionsTest.kt new file mode 100644 index 0000000000..2121db3aef --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassForTestClinitSectionsTest.kt @@ -0,0 +1,68 @@ +package org.utbot.examples.objects + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testcheckers.withProcessingAllClinitSectionsConcretely +import org.utbot.testcheckers.withoutConcrete +import org.utbot.testcheckers.withProcessingClinitSections +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast + +internal class ClassForTestClinitSectionsTest : UtValueTestCaseChecker(testClass = ClassForTestClinitSections::class) { + @Test + fun testClinitWithoutClinitAnalysis() { + withoutConcrete { + withProcessingClinitSections(value = false) { + check( + ClassForTestClinitSections::resultDependingOnStaticSection, + eq(2), + { r -> r == -1 }, + { r -> r == 1 } + ) + } + } + } + + @Test + fun testClinitWithClinitAnalysis() { + withoutConcrete { + check( + ClassForTestClinitSections::resultDependingOnStaticSection, + eq(2), + { r -> r == -1 }, + { r -> r == 1 } + ) + } + } + + @Test + fun testProcessConcretelyWithoutClinitAnalysis() { + withoutConcrete { + withProcessingClinitSections(value = false) { + withProcessingAllClinitSectionsConcretely(value = true) { + check( + ClassForTestClinitSections::resultDependingOnStaticSection, + eq(2), + { r -> r == -1 }, + { r -> r == 1 } + ) + } + } + } + } + + @Test + fun testProcessClinitConcretely() { + withoutConcrete { + withProcessingAllClinitSectionsConcretely(value = true) { + check( + ClassForTestClinitSections::resultDependingOnStaticSection, + eq(1), + { r -> r == -1 }, + coverage = atLeast(71) + ) + } + } + } +} + diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassRefTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassRefTest.kt index 28ad43a36b..955d05e607 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassRefTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassRefTest.kt @@ -2,25 +2,20 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.atLeast -import org.utbot.framework.plugin.api.CodegenLanguage import java.lang.Boolean import kotlin.Array import kotlin.Suppress import kotlin.arrayOf import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast internal class ClassRefTest : UtValueTestCaseChecker( testClass = ClassRef::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - // TODO: SAT-1457 Restore Kotlin codegen for a group of tests with type casts - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + // TODO: SAT-1457 Restore Kotlin codegen for a group of tests with type casts + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testTakeBooleanClassRef() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassWithClassRefTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassWithClassRefTest.kt index 474293cf44..fbc060324a 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassWithClassRefTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ClassWithClassRefTest.kt @@ -1,23 +1,22 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.isException import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test +import org.utbot.framework.codegen.domain.ParametrizedTestSource import org.utbot.testcheckers.eq import org.utbot.testcheckers.withoutConcrete -import org.utbot.tests.infrastructure.CodeGeneration -import org.utbot.tests.infrastructure.Compilation +import org.utbot.testing.* // TODO Kotlin compilation SAT-1332 // Code generation executions fail due we cannot analyze strings properly for now internal class ClassWithClassRefTest : UtValueTestCaseChecker( testClass = ClassWithClassRef::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA, Compilation), // TODO JIRA:1479 - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) + // TODO JIRA:1479 + configurations = listOf( + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.DO_NOT_PARAMETRIZE, Compilation), + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.PARAMETRIZE, Compilation), + Configuration(CodegenLanguage.KOTLIN, ParametrizedTestSource.DO_NOT_PARAMETRIZE, CodeGeneration), ) ) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/HiddenFieldAccessModifiersTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/HiddenFieldAccessModifiersTest.kt index 84820fb0d2..c9ef29e55e 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/HiddenFieldAccessModifiersTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/HiddenFieldAccessModifiersTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class HiddenFieldAccessModifiersTest : UtValueTestCaseChecker(testClass = HiddenFieldAccessModifiersExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/HiddenFieldExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/HiddenFieldExampleTest.kt index 8f8ebf7771..5aa703df56 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/HiddenFieldExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/HiddenFieldExampleTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class HiddenFieldExampleTest : UtValueTestCaseChecker(testClass = HiddenFieldExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/LocalClassExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/LocalClassExampleTest.kt new file mode 100644 index 0000000000..ed5f188458 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/LocalClassExampleTest.kt @@ -0,0 +1,16 @@ +package org.utbot.examples.objects + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker + +class LocalClassExampleTest : UtValueTestCaseChecker(testClass = LocalClassExample::class) { + @Test + fun testLocalClassFieldExample() { + check( + LocalClassExample::localClassFieldExample, + eq(1), + { y, r -> r == y + 42 } + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ModelMinimizationExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ModelMinimizationExamplesTest.kt index 1752e2b7cd..658f834229 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ModelMinimizationExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ModelMinimizationExamplesTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class ModelMinimizationExamplesTest : UtValueTestCaseChecker(testClass = ModelMinimizationExamples::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithFinalStaticTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithFinalStaticTest.kt index c14cea0dec..388968bdf2 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithFinalStaticTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithFinalStaticTest.kt @@ -1,20 +1,15 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.singleValue -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.singleValue class ObjectWithFinalStaticTest : UtValueTestCaseChecker( testClass = ObjectWithFinalStatic::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testParameterEqualsFinalStatic() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithPrimitivesClassTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithPrimitivesClassTest.kt index ef9b11d1c5..6cc5aa08e3 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithPrimitivesClassTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithPrimitivesClassTest.kt @@ -1,11 +1,11 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import kotlin.reflect.KFunction0 import kotlin.reflect.KFunction3 import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class ObjectWithPrimitivesClassTest : UtValueTestCaseChecker(testClass = ObjectWithPrimitivesClass::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithPrimitivesExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithPrimitivesExampleTest.kt index e2eff5b8e8..c712d08715 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithPrimitivesExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithPrimitivesExampleTest.kt @@ -1,13 +1,13 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException internal class ObjectWithPrimitivesExampleTest : UtValueTestCaseChecker(testClass = ObjectWithPrimitivesExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithRefFieldsExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithRefFieldsExampleTest.kt index d85212e204..af99f58ec1 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithRefFieldsExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithRefFieldsExampleTest.kt @@ -1,12 +1,12 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException internal class ObjectWithRefFieldsExampleTest : UtValueTestCaseChecker(testClass = ObjectWithRefFieldExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithStaticFieldsExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithStaticFieldsExampleTest.kt index 6717bd4d3d..dcd1e9955a 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithStaticFieldsExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithStaticFieldsExampleTest.kt @@ -1,12 +1,12 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.findByName -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.singleValue import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.findByName +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.singleValue internal class ObjectWithStaticFieldsExampleTest : UtValueTestCaseChecker(testClass = ObjectWithStaticFieldsExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithThrowableConstructorTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithThrowableConstructorTest.kt index 45f999e00d..297bf4c932 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithThrowableConstructorTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/ObjectWithThrowableConstructorTest.kt @@ -1,11 +1,11 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import kotlin.reflect.KFunction2 import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class ObjectWithThrowableConstructorTest : UtValueTestCaseChecker(testClass = ObjectWithThrowableConstructor::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/PrivateFieldsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/PrivateFieldsTest.kt index 08c26a5c40..a32d8036cb 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/PrivateFieldsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/PrivateFieldsTest.kt @@ -1,10 +1,9 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq - +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException internal class PrivateFieldsTest : UtValueTestCaseChecker(testClass = PrivateFields::class) { @Test fun testAccessWithGetter() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/RecursiveTypeTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/RecursiveTypeTest.kt index 44e7c9cee3..e1dc21ad88 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/RecursiveTypeTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/RecursiveTypeTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class RecursiveTypeTest : UtValueTestCaseChecker(testClass = RecursiveType::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/SimpleClassExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/SimpleClassExampleTest.kt index ed71ca4e24..218721e731 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/SimpleClassExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/SimpleClassExampleTest.kt @@ -1,17 +1,11 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.isException -import org.utbot.tests.infrastructure.keyContain -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between +import org.utbot.testing.isException internal class SimpleClassExampleTest : UtValueTestCaseChecker(testClass = SimpleClassExample::class) { @Test @@ -66,41 +60,12 @@ internal class SimpleClassExampleTest : UtValueTestCaseChecker(testClass = Simpl @Test fun immutableFieldAccessTest() { - val immutableFieldAccessSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("executes conditions:\n"), - DocRegularStmt(" "), - DocCodeStmt("(c.b == 10): True"), - DocRegularStmt("\n"), - DocRegularStmt("returns from: "), - DocCodeStmt("return 0;"), - DocRegularStmt("\n"), - ) - ) - ) checkWithException( SimpleClassExample::immutableFieldAccess, eq(3), { c, r -> c == null && r.isException() }, { c, r -> c.b == 10 && r.getOrNull() == 0 }, - { c, r -> c.b != 10 && r.getOrNull() == 1 }, - summaryTextChecks = listOf( - keyContain(DocRegularStmt("throws NullPointerException in: c.b == 10")), - keyContain(DocCodeStmt("(c.b == 10): False")), - keyMatch(immutableFieldAccessSummary) - ), - summaryNameChecks = listOf( - keyMatch("testImmutableFieldAccess_ThrowNullPointerException"), - keyMatch("testImmutableFieldAccess_CBNotEquals10"), - keyMatch("testImmutableFieldAccess_CBEquals10") - ), - summaryDisplayNameChecks = listOf( - keyContain("NullPointerException", "c.b == 10"), - keyContain("c.b == 10 : False"), - keyContain("c.b == 10 : True") - ) + { c, r -> c.b != 10 && r.getOrNull() == 1 } ) } } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/SimpleClassMultiInstanceExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/SimpleClassMultiInstanceExampleTest.kt index 92134bfb17..7ba04d569a 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/SimpleClassMultiInstanceExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/objects/SimpleClassMultiInstanceExampleTest.kt @@ -1,11 +1,12 @@ package org.utbot.examples.objects -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker -internal class SimpleClassMultiInstanceExampleTest : UtValueTestCaseChecker(testClass = SimpleClassMultiInstanceExample::class) { +internal class SimpleClassMultiInstanceExampleTest : UtValueTestCaseChecker(testClass = + SimpleClassMultiInstanceExample::class) { @Test fun singleObjectChangeTest() { check( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/ByteExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/ByteExamplesTest.kt index d418b38e89..4b709e11f5 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/ByteExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/ByteExamplesTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.primitives -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class ByteExamplesTest : UtValueTestCaseChecker(testClass = ByteExamples::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/CharExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/CharExamplesTest.kt index d3621440be..c93b615eb9 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/CharExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/CharExamplesTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.primitives -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException internal class CharExamplesTest : UtValueTestCaseChecker(testClass = CharExamples::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/DoubleExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/DoubleExamplesTest.kt index 4318c6ef2c..157d5be275 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/DoubleExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/DoubleExamplesTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.primitives -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker @Suppress("SimplifyNegatedBinaryExpression") internal class DoubleExamplesTest : UtValueTestCaseChecker(testClass = DoubleExamples::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/FloatExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/FloatExamplesTest.kt index 390714174a..87baa7a2a3 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/FloatExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/FloatExamplesTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.primitives -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class FloatExamplesTest : UtValueTestCaseChecker(testClass = FloatExamples::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/IntExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/IntExamplesTest.kt index 07e0b463fc..baa6ded345 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/IntExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/primitives/IntExamplesTest.kt @@ -2,8 +2,8 @@ package org.utbot.examples.primitives import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker @Suppress("ConvertTwoComparisonsToRangeCheck") internal class IntExamplesTest : UtValueTestCaseChecker(testClass = IntExamples::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/recursion/RecursionTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/recursion/RecursionTest.kt index aa30f2ace5..127e3cd1b4 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/recursion/RecursionTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/recursion/RecursionTest.kt @@ -1,66 +1,29 @@ package org.utbot.examples.recursion -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.isException -import org.utbot.tests.infrastructure.keyContain -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement import kotlin.math.pow import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq import org.utbot.testcheckers.ge -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.between +import org.utbot.testing.isException // TODO Kotlin mocks generics https://github.com/UnitTestBot/UTBotJava/issues/88 internal class RecursionTest : UtValueTestCaseChecker( testClass = Recursion::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun testFactorial() { - val factorialSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("returns from: "), - DocCodeStmt("return 1;"), - DocRegularStmt("\n"), - ) - ) - ) - checkWithException( Recursion::factorial, eq(3), { x, r -> x < 0 && r.isException() }, { x, r -> x == 0 && r.getOrNull() == 1 }, - { x, r -> x > 0 && r.getOrNull() == (1..x).reduce { a, b -> a * b } }, - summaryTextChecks = listOf( - keyContain(DocCodeStmt("(n < 0): True")), - keyMatch(factorialSummary), - keyContain(DocCodeStmt("(n == 0): False")) - ), - summaryNameChecks = listOf( - keyMatch("testFactorial_NLessThanZero"), - keyMatch("testFactorial_Return1"), - keyMatch("testFactorial_NNotEqualsZero") - ), - summaryDisplayNameChecks = listOf( - keyMatch("-> return 1"), - keyMatch("n < 0 -> ThrowIllegalArgumentException"), - keyMatch("-> return 1") - ) + { x, r -> x > 0 && r.getOrNull() == (1..x).reduce { a, b -> a * b } } ) } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/reflection/NewInstanceExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/reflection/NewInstanceExampleTest.kt new file mode 100644 index 0000000000..813360bf27 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/reflection/NewInstanceExampleTest.kt @@ -0,0 +1,16 @@ +package org.utbot.examples.reflection + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker + +class NewInstanceExampleTest : UtValueTestCaseChecker(NewInstanceExample::class) { + @Test + fun testNewInstanceExample() { + check( + NewInstanceExample::createWithReflectionExample, + eq(1), + { r -> r == 0 } + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/statics/substitution/StaticsSubstitutionTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/statics/substitution/StaticsSubstitutionTest.kt index 4b495cc750..ff5c014a68 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/statics/substitution/StaticsSubstitutionTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/statics/substitution/StaticsSubstitutionTest.kt @@ -1,10 +1,10 @@ package org.utbot.examples.statics.substitution -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.withoutSubstituteStaticsWithSymbolicVariable +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker class StaticsSubstitutionTest : UtValueTestCaseChecker(testClass = StaticSubstitutionExamples::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/DateExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/DateExampleTest.kt index 1e04bb4799..af887d6c4c 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/DateExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/DateExampleTest.kt @@ -3,10 +3,10 @@ package org.utbot.examples.stdlib import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.isException import org.utbot.testcheckers.eq import org.utbot.testcheckers.withUsingReflectionForMaximizingCoverage +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException import java.util.Date @Disabled("Java 11 transition -- these tests seems to take too much time and memory") diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/JavaIOFileInputStreamCheckTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/JavaIOFileInputStreamCheckTest.kt new file mode 100644 index 0000000000..db4082881e --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/JavaIOFileInputStreamCheckTest.kt @@ -0,0 +1,39 @@ +package org.utbot.examples.stdlib + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.id +import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker + +internal class JavaIOFileInputStreamCheckTest : UtValueTestCaseChecker( + testClass = JavaIOFileInputStreamCheck::class, + testCodeGeneration = true, + // because of mocks + configurations = ignoreKotlinCompilationConfigurations, +) { + @Test + fun testRead() { + checkMocksAndInstrumentation( + JavaIOFileInputStreamCheck::read, + eq(1), + { _, _, instrumentation, r -> + val constructorMock = instrumentation.single() as UtNewInstanceInstrumentation + + val classIdEquality = constructorMock.classId == java.io.FileInputStream::class.id + val callSiteIdEquality = constructorMock.callSites.single() == JavaIOFileInputStreamCheck::class.id + val instance = constructorMock.instances.single() as UtCompositeModel + val methodMock = instance.mocks.entries.single() + val methodNameEquality = methodMock.key.name == "read" + val mockValueResult = r == (methodMock.value.single() as UtPrimitiveModel).value as Int + + classIdEquality && callSiteIdEquality && instance.isMock && methodNameEquality && mockValueResult + }, + additionalMockAlwaysClasses = setOf(java.io.FileInputStream::class.id), + coverage = DoNotCalculate // there is a problem with coverage calculation of mocked values + ) + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/StaticsPathDiversionTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/StaticsPathDiversionTest.kt new file mode 100644 index 0000000000..5d46100f88 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stdlib/StaticsPathDiversionTest.kt @@ -0,0 +1,36 @@ +package org.utbot.examples.stdlib + +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.util.id +import org.utbot.testcheckers.ge +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import java.io.File + +internal class StaticsPathDiversionTest : UtValueTestCaseChecker( + testClass = StaticsPathDiversion::class, +) { + @Test + @Disabled("See https://github.com/UnitTestBot/UTBotJava/issues/716") + fun testJavaIOFile() { + // TODO Here we have a path diversion example - the static field `java.io.File#separator` is considered as not meaningful, + // so it is not passed to the concrete execution because of absence in the `stateBefore` models. + // So, the symbolic engine has 2 results - true and false, as expected, but the concrete executor may produce 1 or 2, + // depending on the model for the argument of the MUT produced by the solver. + // Such diversion was predicted to some extent - see `org.utbot.common.WorkaroundReason.IGNORE_STATICS_FROM_TRUSTED_LIBRARIES` + // and the corresponding issue https://github.com/UnitTestBot/UTBotJava/issues/716 + check( + StaticsPathDiversion::separatorEquality, + ge(2), // We cannot guarantee the exact number of branches without minimization + + // In the matchers below we check that the symbolic does not change the static field `File.separator` - we should + // change the parameter, not the static field + { s, separator -> separator == File.separator && s == separator }, + { s, separator -> separator == File.separator && s != separator }, + additionalMockAlwaysClasses = setOf(java.io.File::class.id), // From the use-case + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/BaseStreamExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/BaseStreamExampleTest.kt index 919352f53e..c0f4abe0ef 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/BaseStreamExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/BaseStreamExampleTest.kt @@ -3,53 +3,34 @@ 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.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.Full -import org.utbot.tests.infrastructure.FullWithAssumptions -import org.utbot.tests.infrastructure.StaticsType -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq import org.utbot.testcheckers.withoutConcrete -import org.utbot.tests.infrastructure.AtLeast -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.AtLeast +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.Full +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.StaticsType +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException import java.util.Optional import java.util.stream.Stream -import kotlin.streams.toList +import org.utbot.testing.asList // TODO 1 instruction is always uncovered https://github.com/UnitTestBot/UTBotJava/issues/193 // TODO failed Kotlin compilation (generics) JIRA:1332 class BaseStreamExampleTest : UtValueTestCaseChecker( testClass = BaseStreamExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { - @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() }, + { s, r -> s != null && s.asList() == r!!.asList() }, coverage = FullWithAssumptions(assumeCallsNumber = 1) ) } diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/DoubleStreamExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/DoubleStreamExampleTest.kt index 6d7b4f7ec4..ebfa983bae 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/DoubleStreamExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/DoubleStreamExampleTest.kt @@ -2,11 +2,15 @@ package org.utbot.examples.stream import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq import org.utbot.testcheckers.withPathSelectorStepsLimit import org.utbot.testcheckers.withoutConcrete -import org.utbot.tests.infrastructure.* +import org.utbot.testing.AtLeast +import org.utbot.testing.Full +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException import java.util.OptionalDouble import java.util.stream.DoubleStream import kotlin.streams.toList @@ -15,24 +19,8 @@ import kotlin.streams.toList @Tag("slow") // we do not really need to always use this test in CI because it is almost the same as BaseStreamExampleTest class DoubleStreamExampleTest : UtValueTestCaseChecker( testClass = DoubleStreamExample::class, - testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { - @Test - fun testReturningStreamExample() { - check( - DoubleStreamExample::returningStreamExample, - ignoreExecutionsNumber, - // NOTE: the order of the matchers is important because Stream could be used only once - { c, r -> c.isNotEmpty() && c.doubles().contentEquals(r!!.toArray()) }, - { c, r -> c.isEmpty() && r!!.count() == 0L }, - coverage = FullWithAssumptions(assumeCallsNumber = 1) - ) - } - @Test fun testReturningStreamAsParameterExample() { withoutConcrete { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/IntStreamExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/IntStreamExampleTest.kt index b91045e00d..4aad1a7855 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/IntStreamExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/IntStreamExampleTest.kt @@ -2,11 +2,15 @@ package org.utbot.examples.stream import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq import org.utbot.testcheckers.withPathSelectorStepsLimit import org.utbot.testcheckers.withoutConcrete -import org.utbot.tests.infrastructure.* +import org.utbot.testing.AtLeast +import org.utbot.testing.Full +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException import java.util.OptionalDouble import java.util.OptionalInt import java.util.stream.IntStream @@ -17,23 +21,8 @@ import kotlin.streams.toList class IntStreamExampleTest : UtValueTestCaseChecker( testClass = IntStreamExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { - @Test - fun testReturningStreamExample() { - check( - IntStreamExample::returningStreamExample, - ignoreExecutionsNumber, - // NOTE: the order of the matchers is important because Stream could be used only once - { c, r -> c.isNotEmpty() && c.ints().contentEquals(r!!.toArray()) }, - { c, r -> c.isEmpty() && r!!.count() == 0L }, - coverage = FullWithAssumptions(assumeCallsNumber = 1) - ) - } - @Test fun testReturningStreamAsParameterExample() { withoutConcrete { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/LongStreamExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/LongStreamExampleTest.kt index 704ed750b0..bf9731a27b 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/LongStreamExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/LongStreamExampleTest.kt @@ -2,11 +2,15 @@ package org.utbot.examples.stream import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq import org.utbot.testcheckers.withPathSelectorStepsLimit import org.utbot.testcheckers.withoutConcrete -import org.utbot.tests.infrastructure.* +import org.utbot.testing.AtLeast +import org.utbot.testing.Full +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException import java.util.OptionalDouble import java.util.OptionalLong import java.util.stream.LongStream @@ -17,23 +21,8 @@ import kotlin.streams.toList class LongStreamExampleTest : UtValueTestCaseChecker( testClass = LongStreamExample::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { - @Test - fun testReturningStreamExample() { - check( - LongStreamExample::returningStreamExample, - ignoreExecutionsNumber, - // NOTE: the order of the matchers is important because Stream could be used only once - { c, r -> c.isNotEmpty() && c.longs().contentEquals(r!!.toArray()) }, - { c, r -> c.isEmpty() && r!!.count() == 0L }, - coverage = FullWithAssumptions(assumeCallsNumber = 1) - ) - } - @Test fun testReturningStreamAsParameterExample() { withoutConcrete { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/StreamsAsMethodResultExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/StreamsAsMethodResultExampleTest.kt new file mode 100644 index 0000000000..a35347eea0 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/stream/StreamsAsMethodResultExampleTest.kt @@ -0,0 +1,65 @@ +package org.utbot.examples.stream + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.visible.UtStreamConsumingException +import org.utbot.testcheckers.eq +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException +import kotlin.streams.toList +import org.utbot.testing.asList + +// TODO 1 instruction is always uncovered https://github.com/UnitTestBot/UTBotJava/issues/193 +// TODO failed Kotlin compilation (generics) JIRA:1332 +class StreamsAsMethodResultExampleTest : UtValueTestCaseChecker( + testClass = StreamsAsMethodResultExample::class, + testCodeGeneration = true, + configurations = ignoreKotlinCompilationConfigurations, +) { + @Test + fun testReturningStreamExample() { + check( + StreamsAsMethodResultExample::returningStreamExample, + eq(2), + { c, r -> c.isEmpty() && c == r!!.asList() }, + { c, r -> c.isNotEmpty() && c == r!!.asList() }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testReturningIntStreamExample() { + checkWithException( + StreamsAsMethodResultExample::returningIntStreamExample, + eq(3), + { c, r -> c.isEmpty() && c == r.getOrThrow().toList() }, + { c, r -> c.isNotEmpty() && c.none { it == null } && c.toIntArray().contentEquals(r.getOrThrow().toArray()) }, + { c, r -> c.isNotEmpty() && c.any { it == null } && r.isException() }, + coverage = FullWithAssumptions(assumeCallsNumber = 2) + ) + } + + @Test + fun testReturningLongStreamExample() { + checkWithException( + StreamsAsMethodResultExample::returningLongStreamExample, + eq(3), + { c, r -> c.isEmpty() && c == r.getOrThrow().toList() }, + { c, r -> c.isNotEmpty() && c.none { it == null } && c.map { it.toLong() }.toLongArray().contentEquals(r.getOrThrow().toArray()) }, + { c, r -> c.isNotEmpty() && c.any { it == null } && r.isException() }, + coverage = FullWithAssumptions(assumeCallsNumber = 2) + ) + } + + @Test + fun testReturningDoubleStreamExample() { + checkWithException( + StreamsAsMethodResultExample::returningDoubleStreamExample, + eq(3), + { c, r -> c.isEmpty() && c == r.getOrThrow().toList() }, + { c, r -> c.isNotEmpty() && c.none { it == null } && c.map { it.toDouble() }.toDoubleArray().contentEquals(r.getOrThrow().toArray()) }, + { c, r -> c.isNotEmpty() && c.any { it == null } && r.isException() }, + coverage = FullWithAssumptions(assumeCallsNumber = 2) + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings/GenericExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings/GenericExamplesTest.kt new file mode 100644 index 0000000000..8b7077705e --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings/GenericExamplesTest.kt @@ -0,0 +1,33 @@ +package org.utbot.examples.strings + +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException + +@Disabled("TODO: Fails and takes too long") +internal class GenericExamplesTest : UtValueTestCaseChecker( + testClass = GenericExamples::class, + testCodeGeneration = true, + configurations = ignoreKotlinCompilationConfigurations, +) { + @Test + fun testContainsOkWithIntegerType() { + checkWithException( + GenericExamples::containsOk, + eq(2), + { obj, result -> obj == null && result.isException() }, + { obj, result -> obj != null && result.isSuccess && result.getOrNull() == false } + ) + } + + @Test + fun testContainsOkExampleTest() { + check( + GenericExamples::containsOkExample, + eq(1), + { result -> result == true } + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings/StringExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings/StringExamplesTest.kt index 6275940f52..6dbf32c930 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings/StringExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings/StringExamplesTest.kt @@ -1,13 +1,5 @@ package org.utbot.examples.strings -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.atLeast -import org.utbot.tests.infrastructure.between -import org.utbot.tests.infrastructure.ignoreExecutionsNumber -import org.utbot.tests.infrastructure.isException -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq @@ -15,28 +7,47 @@ import org.utbot.testcheckers.ge import org.utbot.testcheckers.withPushingStateFromPathSelectorForConcrete import org.utbot.testcheckers.withSolverTimeoutInMillis import org.utbot.testcheckers.withoutMinimization -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.FullWithAssumptions +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast +import org.utbot.testing.between +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException +import java.util.Locale internal class StringExamplesTest : UtValueTestCaseChecker( testClass = StringExamples::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test - @Disabled("Flaky test: https://github.com/UnitTestBot/UTBotJava/issues/131 (will be enabled in new strings PR)") fun testByteToString() { - // 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() }, - ) - } + check( + StringExamples::byteToString, + eq(2), + { a, b, r -> a > b && r == a.toString() }, + { a, b, r -> a <= b && r == b.toString() }, + ) + } + + @Test + fun testByteToStringWithConstants() { + val values: Array = arrayOf( + Byte.MIN_VALUE, + (Byte.MIN_VALUE + 100).toByte(), + 0.toByte(), + (Byte.MAX_VALUE - 100).toByte(), + Byte.MAX_VALUE + ) + + val expected = values.map { it.toString() } + + check( + StringExamples::byteToStringWithConstants, + eq(1), + { r -> r != null && r.indices.all { r[it] == expected[it] } } + ) } @Test @@ -53,45 +64,91 @@ internal class StringExamplesTest : UtValueTestCaseChecker( @Test fun testShortToString() { - // 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() }, - ) - } + check( + StringExamples::shortToString, + ignoreExecutionsNumber, + { a, b, r -> a > b && r == a.toString() }, + { a, b, r -> a <= b && r == b.toString() }, + ) } + @Test + fun testShortToStringWithConstants() { + val values: Array = arrayOf( + Short.MIN_VALUE, + (Short.MIN_VALUE + 100).toShort(), + 0.toShort(), + (Short.MAX_VALUE - 100).toShort(), + Short.MAX_VALUE + ) + + val expected = values.map { it.toString() } + + check( + StringExamples::shortToStringWithConstants, + eq(1), + { r -> r != null && r.indices.all { r[it] == expected[it] } } + ) + } @Test fun testIntToString() { - // 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() }, - ) - } + check( + StringExamples::intToString, + ignoreExecutionsNumber, + { a, b, r -> a > b && r == a.toString() }, + { a, b, r -> a <= b && r == b.toString() }, + ) } + @Test + fun testIntToStringWithConstants() { + val values: Array = arrayOf( + Integer.MIN_VALUE, + Integer.MIN_VALUE + 100, + 0, + Integer.MAX_VALUE - 100, + Integer.MAX_VALUE + ) + + val expected = values.map { it.toString() } + + check( + StringExamples::intToStringWithConstants, + eq(1), + { r -> r != null && r.indices.all { r[it] == expected[it] } } + ) + } @Test fun testLongToString() { - // 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() }, - ) - } + check( + StringExamples::longToString, + ignoreExecutionsNumber, + { a, b, r -> a > b && r == a.toString() }, + { a, b, r -> a <= b && r == b.toString() }, + ) } + @Test + fun testLongToStringWithConstants() { + val values: Array = arrayOf( + Long.MIN_VALUE, + Long.MIN_VALUE + 100L, + 0L, + Long.MAX_VALUE - 100L, + Long.MAX_VALUE + ) + + val expected = values.map { it.toString() } + + check( + StringExamples::longToStringWithConstants, + eq(1), + { r -> r != null && r.indices.all { r[it] == expected[it] } } + ) + } + @Test fun testStartsWithLiteral() { check( @@ -100,7 +157,7 @@ internal class StringExamplesTest : UtValueTestCaseChecker( { v, _ -> v == null }, { v, r -> v != null && v.startsWith("1234567890") && r!!.startsWith("12a4567890") }, { v, r -> v != null && v[0] == 'x' && r!![0] == 'x' }, - { v, r -> v != null && v.toLowerCase() == r } + { v, r -> v != null && v.lowercase(Locale.getDefault()) == r } ) } @@ -184,7 +241,7 @@ internal class StringExamplesTest : UtValueTestCaseChecker( check( StringExamples::concat, between(1..2), - { fst, snd, r -> fst == null || snd == null && r == fst + snd }, + { fst, snd, r -> (fst == null || snd == null) && r == fst + snd }, { fst, snd, r -> r == fst + snd }, ) } @@ -238,6 +295,15 @@ internal class StringExamplesTest : UtValueTestCaseChecker( ) } + @Test + fun testStringBuilderAsParameterExample() { + check( + StringExamples::stringBuilderAsParameterExample, + eq(1), + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + @Test fun testNullableStringBuffer() { checkWithException( @@ -250,6 +316,15 @@ internal class StringExamplesTest : UtValueTestCaseChecker( ) } + @Test + fun testIsStringBuilderEmpty() { + check( + StringExamples::isStringBuilderEmpty, + eq(2), + { stringBuilder, result -> result == stringBuilder.isEmpty() } + ) + } + @Test @Disabled("Flaky on GitHub: https://github.com/UnitTestBot/UTBotJava/issues/1004") fun testIsValidUuid() { @@ -257,9 +332,9 @@ internal class StringExamplesTest : UtValueTestCaseChecker( check( StringExamples::isValidUuid, ignoreExecutionsNumber, - { uuid, r -> uuid == null || uuid.length == 0 && r == false }, - { uuid, r -> uuid.length > 0 && uuid.isBlank() && r == false }, - { uuid, r -> uuid.length > 0 && uuid.isNotBlank() && r == false }, + { uuid, r -> uuid == null || uuid.isEmpty() && r == false }, + { uuid, r -> uuid.isNotEmpty() && uuid.isBlank() && r == false }, + { uuid, r -> uuid.isNotEmpty() && uuid.isNotBlank() && r == false }, { uuid, r -> uuid.length > 1 && uuid.isNotBlank() && !uuid.matches(pattern) && r == false }, { uuid, r -> uuid.length > 1 && uuid.isNotBlank() && uuid.matches(pattern) && r == true }, ) @@ -277,20 +352,29 @@ internal class StringExamplesTest : UtValueTestCaseChecker( ) } + @Test + fun testSplitExample() { + check( + StringExamples::splitExample, + ignoreExecutionsNumber, + { s, r -> s.all { it.isWhitespace() } && r == 0 }, + { s, r -> s.none { it.isWhitespace() } && r == 1 }, + { s, r -> s[0].isWhitespace() && s.any { !it.isWhitespace() } && r == 2 }, + { s, r -> !s[0].isWhitespace() && s[2].isWhitespace() && r == 1 }, + { s, r -> !s[0].isWhitespace() && s[1].isWhitespace() && !s[2].isWhitespace() && r == 2 }, + coverage = FullWithAssumptions(assumeCallsNumber = 2) + ) + } + @Test fun testIsBlank() { check( StringExamples::isBlank, ge(4), { cs, r -> cs == null && r == true }, - { cs, r -> cs.length == 0 && r == true }, - { cs, r -> cs.length > 0 && cs.isBlank() && r == true }, - { cs, r -> cs.length > 0 && cs.isNotBlank() && r == false }, - summaryNameChecks = listOf( - keyMatch("testIsBlank_StrLenEqualsZero"), - keyMatch("testIsBlank_NotCharacterIsWhitespace"), - keyMatch("testIsBlank_CharacterIsWhitespace") - ) + { cs, r -> cs.isEmpty() && r == true }, + { cs, r -> cs.isNotEmpty() && cs.isBlank() && r == true }, + { cs, r -> cs.isNotEmpty() && cs.isNotBlank() && r == false } ) } @@ -312,7 +396,7 @@ internal class StringExamplesTest : UtValueTestCaseChecker( { _, i, r -> i <= 0 && r.isException() }, { cs, i, r -> i > 0 && cs == null && !r.getOrThrow() }, { cs, i, r -> i > 0 && cs != null && cs.length > i && r.getOrThrow() }, - coverage = DoNotCalculate // TODO: Coverage calculation fails in the child process with Illegal Argument Exception + coverage = DoNotCalculate // TODO: Coverage calculation fails in the instrumented process with Illegal Argument Exception ) } @@ -332,7 +416,7 @@ internal class StringExamplesTest : UtValueTestCaseChecker( fun testSubstring() { checkWithException( StringExamples::substring, - between(5..7), + between(5..8), { s, _, r -> s == null && r.isException() }, { s, i, r -> s != null && i < 0 || i > s.length && r.isException() }, { s, i, r -> s != null && i in 0..s.length && r.getOrThrow() == s.substring(i) && s.substring(i) != "password" }, @@ -471,9 +555,9 @@ internal class StringExamplesTest : UtValueTestCaseChecker( check( StringExamples::endsWith, between(5..6), - { s, suffix, r -> suffix == null }, - { s, suffix, r -> suffix != null && suffix.length < 2 }, - { s, suffix, r -> suffix != null && suffix.length >= 2 && s == null }, + { _, suffix, _ -> suffix == null }, + { _, suffix, _ -> suffix != null && suffix.length < 2 }, + { s, suffix, _ -> suffix != null && suffix.length >= 2 && s == null }, { s, suffix, r -> suffix != null && suffix.length >= 2 && s != null && s.endsWith(suffix) && r == true }, { s, suffix, r -> suffix != null && suffix.length >= 2 && s != null && !s.endsWith(suffix) && r == false } ) @@ -539,10 +623,10 @@ internal class StringExamplesTest : UtValueTestCaseChecker( checkWithException( StringExamples::compareCodePoints, between(8..10), - { s, t, i, r -> s == null && r.isException() }, - { s, t, i, r -> s != null && i < 0 || i >= s.length && r.isException() }, - { s, t, i, r -> s != null && t == null && r.isException() }, - { s, t, i, r -> t != null && i < 0 || i >= t.length && r.isException() }, + { s, _, _, r -> s == null && r.isException() }, + { s, _, i, r -> s != null && i < 0 || i >= s.length && r.isException() }, + { s, t, _, r -> s != null && t == null && r.isException() }, + { _, t, i, r -> t != null && i < 0 || i >= t.length && r.isException() }, { s, t, i, r -> s != null && t != null && s.codePointAt(i) < t.codePointAt(i) && i == 0 && r.getOrThrow() == 0 }, { s, t, i, r -> s != null && t != null && s.codePointAt(i) < t.codePointAt(i) && i != 0 && r.getOrThrow() == 1 }, { s, t, i, r -> s != null && t != null && s.codePointAt(i) >= t.codePointAt(i) && i == 0 && r.getOrThrow() == 2 }, @@ -555,7 +639,7 @@ internal class StringExamplesTest : UtValueTestCaseChecker( check( StringExamples::toCharArray, eq(2), - { s, r -> s == null }, + { s, _ -> s == null }, { s, r -> s.toCharArray().contentEquals(r) } ) } @@ -585,13 +669,14 @@ internal class StringExamplesTest : UtValueTestCaseChecker( withPushingStateFromPathSelectorForConcrete { check( StringExamples::equalsIgnoreCase, - eq(2), + ignoreExecutionsNumber, { s, r -> "SUCCESS".equals(s, ignoreCase = true) && r == "success" }, { s, r -> !"SUCCESS".equals(s, ignoreCase = true) && r == "failure" }, ) } } + // TODO: This test fails without concrete execution as it uses a symbolic variable @Test fun testListToString() { check( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings11/StringConcatTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings11/StringConcatTest.kt new file mode 100644 index 0000000000..d412589611 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/strings11/StringConcatTest.kt @@ -0,0 +1,115 @@ +package org.utbot.examples.strings11 + +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException + +class StringConcatTest : UtValueTestCaseChecker( + testClass = StringConcat::class, + testCodeGeneration = true, + configurations = ignoreKotlinCompilationConfigurations, +) { + @Test + fun testConcatArguments() { + withoutConcrete { + check( + StringConcat::concatArguments, + eq(1), + { a, b, c, r -> "$a$b$c" == r } + ) + } + } + + @Test + fun testConcatWithConstants() { + withoutConcrete { + check( + StringConcat::concatWithConstants, + eq(4), + { a, r -> a == "head" && r == 1 }, + { a, r -> a == "body" && r == 2 }, + { a, r -> a == null && r == 3 }, + { a, r -> a != "head" && a != "body" && a != null && r == 4 }, + ) + } + } + + @Disabled("Flickers too much with JVM 17") + @Test + fun testConcatWithPrimitives() { + withoutConcrete { + check( + StringConcat::concatWithPrimitives, + eq(1), + { a, r -> "$a#4253.0" == r} + ) + } + } + + @Test + fun testExceptionInToString() { + withoutConcrete { + checkWithException( + StringConcat::exceptionInToString, + ignoreExecutionsNumber, + { t, r -> t.x == 42 && r.isException() }, + { t, r -> t.x != 42 && r.getOrThrow() == "Test: x = ${t.x}!" }, + coverage = DoNotCalculate + ) + } + } + + @Test + fun testConcatWithField() { + withoutConcrete { + checkWithThis( + StringConcat::concatWithField, + eq(1), + { o, a, r -> "$a${o.str}#" == r } + ) + } + } + + @Test + fun testConcatWithPrimitiveWrappers() { + withoutConcrete { + check( + StringConcat::concatWithPrimitiveWrappers, + ignoreExecutionsNumber, + { b, c, r -> b.toString().endsWith("4") && c == '2' && r == 1 }, + { _, c, r -> !c.toString().endsWith("42") && r == 2 }, + coverage = DoNotCalculate + ) + } + } + + @Test + fun testSameConcat() { + withoutConcrete { + check( + StringConcat::sameConcat, + ignoreExecutionsNumber, + { a, b, r -> a == b && r == 0 }, + { a, b, r -> a != b && r == 1 }, + coverage = DoNotCalculate + ) + } + } + + @Test + fun testConcatStrangeSymbols() { + withoutConcrete { + check( + StringConcat::concatStrangeSymbols, + eq(1), + { r -> r == "\u0000#\u0001!\u0002@\u0012\t" } + ) + } + } + +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/HeapTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/HeapTest.kt index d28e3ecbe0..dd405f15b0 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/HeapTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/HeapTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.structures -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.ignoreExecutionsNumber import org.junit.jupiter.api.Test +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber internal class HeapTest : UtValueTestCaseChecker(testClass = Heap::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/MinStackExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/MinStackExampleTest.kt index 42a7a77913..f391339671 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/MinStackExampleTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/MinStackExampleTest.kt @@ -1,11 +1,11 @@ package org.utbot.examples.structures -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.between import kotlin.math.min import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.between internal class MinStackExampleTest : UtValueTestCaseChecker(testClass = MinStackExample::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/StandardStructuresTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/StandardStructuresTest.kt index aff33bc2d3..34092a108d 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/StandardStructuresTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/structures/StandardStructuresTest.kt @@ -1,27 +1,16 @@ package org.utbot.examples.structures -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.keyContain -import org.utbot.tests.infrastructure.keyMatch -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement import java.util.LinkedList import java.util.TreeMap import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class StandardStructuresTest : UtValueTestCaseChecker( testClass = StandardStructures::class, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test @Disabled("TODO down cast for object wrapper JIRA:1480") @@ -54,24 +43,6 @@ internal class StandardStructuresTest : UtValueTestCaseChecker( @Test fun testGetDeque() { - val dequeSummary = listOf( - DocPreTagStatement( - listOf( - DocRegularStmt("Test "), - DocRegularStmt("executes conditions:\n"), - DocRegularStmt(" "), - DocCodeStmt("(deque instanceof LinkedList): False"), - DocRegularStmt(",\n"), - DocRegularStmt(" "), - DocCodeStmt("(deque == null): True"), - DocRegularStmt("\n"), - DocRegularStmt("returns from: "), - DocCodeStmt("return null;"), - DocRegularStmt("\n") - ) - ) - ) - check( StandardStructures::getDeque, eq(4), @@ -81,25 +52,7 @@ internal class StandardStructuresTest : UtValueTestCaseChecker( { d, r -> d !is java.util.ArrayDeque<*> && d !is LinkedList && d != null && r !is java.util.ArrayDeque<*> && r !is LinkedList && r != null }, - coverage = DoNotCalculate, - summaryTextChecks = listOf( - keyContain(DocCodeStmt("(deque instanceof ArrayDeque): True")), - keyContain(DocCodeStmt("(deque instanceof LinkedList): True")), - keyContain(DocCodeStmt("(deque == null): True")), - keyMatch(dequeSummary) - ), - summaryNameChecks = listOf( - keyMatch("testGetDeque_DequeInstanceOfArrayDeque"), - keyMatch("testGetDeque_DequeInstanceOfLinkedList"), - keyMatch("testGetDeque_DequeEqualsNull"), - keyMatch("testGetDeque_DequeNotEqualsNull"), - ), - summaryDisplayNameChecks = listOf( - keyContain("deque", "instance", "ArrayDeque"), - keyContain("deque", "instance", "LinkedList"), - keyContain("deque == null", "True"), - keyContain("deque == null", "False"), - ) + coverage = DoNotCalculate ) } } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintBranchingTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintBranchingTest.kt new file mode 100644 index 0000000000..c884ed8517 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintBranchingTest.kt @@ -0,0 +1,31 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintBranchingTest : UtValueTestCaseCheckerForTaint( + testClass = TaintBranching::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintBranchingConfig.yaml") +) { + @Test + fun testTaintBad() { + checkWithException( + TaintBranching::bad, + eq(3), // success (x2) & taint error + { cond, r -> cond == r.isException() }, + ) + } + + @Test + fun testTaintGood() { + checkWithException( + TaintBranching::good, + eq(2), // success in both cases + { _, r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintCleanerConditionsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintCleanerConditionsTest.kt new file mode 100644 index 0000000000..e33f1bd692 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintCleanerConditionsTest.kt @@ -0,0 +1,70 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintCleanerConditionsTest : UtValueTestCaseCheckerForTaint( + testClass = TaintCleanerConditions::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintCleanerConditionsConfig.yaml") +) { + @Test + fun testTaintBadArg() { + checkWithException( + TaintCleanerConditions::badArg, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadReturn() { + checkWithException( + TaintCleanerConditions::badReturn, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadThis() { + checkWithException( + TaintCleanerConditions::badThis, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodArg() { + checkWithException( + TaintCleanerConditions::goodArg, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodReturn() { + checkWithException( + TaintCleanerConditions::goodReturn, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodThis() { + checkWithException( + TaintCleanerConditions::goodThis, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintCleanerSimpleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintCleanerSimpleTest.kt new file mode 100644 index 0000000000..3c214cb00a --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintCleanerSimpleTest.kt @@ -0,0 +1,32 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintCleanerSimpleTest : UtValueTestCaseCheckerForTaint( + testClass = TaintCleanerSimple::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintCleanerSimpleConfig.yaml") +) { + @Test + fun testTaintBad() { + checkWithException( + TaintCleanerSimple::bad, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGood() { + checkWithException( + TaintCleanerSimple::good, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintLongPathTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintLongPathTest.kt new file mode 100644 index 0000000000..e4aeeed6da --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintLongPathTest.kt @@ -0,0 +1,32 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintLongPathTest : UtValueTestCaseCheckerForTaint( + testClass = TaintLongPath::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintLongPathConfig.yaml") +) { + @Test + fun testTaintBad() { + checkWithException( + TaintLongPath::bad, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGood() { + checkWithException( + TaintLongPath::good, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintOtherClassTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintOtherClassTest.kt new file mode 100644 index 0000000000..d915e3e1f9 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintOtherClassTest.kt @@ -0,0 +1,32 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintOtherClassTest : UtValueTestCaseCheckerForTaint( + testClass = TaintOtherClass::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintOtherClassConfig.yaml") +) { + @Test + fun testTaintBad() { + checkWithException( + TaintOtherClass::bad, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGood() { + checkWithException( + TaintOtherClass::good, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintPassConditionsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintPassConditionsTest.kt new file mode 100644 index 0000000000..f46e65f7ed --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintPassConditionsTest.kt @@ -0,0 +1,70 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintPassConditionsTest : UtValueTestCaseCheckerForTaint( + testClass = TaintPassConditions::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintPassConditionsConfig.yaml") +) { + @Test + fun testTaintBadArg() { + checkWithException( + TaintPassConditions::badArg, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadReturn() { + checkWithException( + TaintPassConditions::badReturn, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadThis() { + checkWithException( + TaintPassConditions::badThis, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodArg() { + checkWithException( + TaintPassConditions::goodArg, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodReturn() { + checkWithException( + TaintPassConditions::goodReturn, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodThis() { + checkWithException( + TaintPassConditions::goodThis, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintPassSimpleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintPassSimpleTest.kt new file mode 100644 index 0000000000..3a40ea053c --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintPassSimpleTest.kt @@ -0,0 +1,42 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintPassSimpleTest : UtValueTestCaseCheckerForTaint( + testClass = TaintPassSimple::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintPassSimpleConfig.yaml") +) { + @Test + fun testTaintBad() { + checkWithException( + TaintPassSimple::bad, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadDoublePass() { + checkWithException( + TaintPassSimple::badDoublePass, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGood() { + checkWithException( + TaintPassSimple::good, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSeveralMarksTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSeveralMarksTest.kt new file mode 100644 index 0000000000..0df3a948d6 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSeveralMarksTest.kt @@ -0,0 +1,173 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testcheckers.withoutThrowTaintErrorForEachMarkSeparately +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintSeveralMarksTest : UtValueTestCaseCheckerForTaint( + testClass = TaintSeveralMarks::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintSeveralMarksConfig.yaml") +) { + @Test + fun testTaintBad1() { + checkWithException( + TaintSeveralMarks::bad1, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBad2() { + checkWithException( + TaintSeveralMarks::bad2, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBad13() { + checkWithException( + TaintSeveralMarks::bad13, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBad13NotSeparately() { + withoutThrowTaintErrorForEachMarkSeparately { + checkWithException( + TaintSeveralMarks::bad13, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + } + + @Test + fun testTaintBad123() { + checkWithException( + TaintSeveralMarks::bad123, + eq(3), // success & taint error (x2, for each mark separately) + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBad123NotSeparately() { + withoutThrowTaintErrorForEachMarkSeparately { + checkWithException( + TaintSeveralMarks::bad123, + eq(2), // success & taint error (one for two marks) + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + } + + @Test + fun testTaintBadSourceAll() { + checkWithException( + TaintSeveralMarks::badSourceAll, + eq(4), // success & taint error (x3) + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadSinkAll() { + checkWithException( + TaintSeveralMarks::badSinkAll, + eq(3), // success & taint error (x2) + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadWrongCleaner() { + checkWithException( + TaintSeveralMarks::badWrongCleaner, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGood1() { + checkWithException( + TaintSeveralMarks::good1, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGood2() { + checkWithException( + TaintSeveralMarks::good2, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGood13() { + checkWithException( + TaintSeveralMarks::good13, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGood13NotSeparately() { + withoutThrowTaintErrorForEachMarkSeparately { + checkWithException( + TaintSeveralMarks::good13, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + } + + @Test + fun testTaintGoodWrongSource() { + checkWithException( + TaintSeveralMarks::goodWrongSource, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodWrongSink() { + checkWithException( + TaintSeveralMarks::goodWrongSink, + eq(1), // only success + { r -> r.isSuccess } + ) + } + + @Test + fun testTaintGoodWrongPass() { + checkWithException( + TaintSeveralMarks::goodWrongPass, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSignatureTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSignatureTest.kt new file mode 100644 index 0000000000..57a2632749 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSignatureTest.kt @@ -0,0 +1,59 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintSignatureTest : UtValueTestCaseCheckerForTaint( + testClass = TaintSignature::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintSignatureConfig.yaml") +) { + @Test + fun testTaintBad() { + checkWithException( + TaintSignature::badFakeCleaner, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodCleaner() { + checkWithException( + TaintSignature::goodCleaner, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodFakeSources() { + checkWithException( + TaintSignature::goodFakeSources, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodFakePass() { + checkWithException( + TaintSignature::goodFakePass, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodFakeSink() { + checkWithException( + TaintSignature::goodFakeSink, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSimpleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSimpleTest.kt new file mode 100644 index 0000000000..a7ab2b5558 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSimpleTest.kt @@ -0,0 +1,32 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintSimpleTest : UtValueTestCaseCheckerForTaint( + testClass = TaintSimple::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintSimpleConfig.yaml") +) { + @Test + fun testTaintBad() { + checkWithException( + TaintSimple::bad, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGood() { + checkWithException( + TaintSimple::good, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSinkConditionsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSinkConditionsTest.kt new file mode 100644 index 0000000000..a78c40a359 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSinkConditionsTest.kt @@ -0,0 +1,51 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintSinkConditionsTest : UtValueTestCaseCheckerForTaint( + testClass = TaintSinkConditions::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintSinkConditionsConfig.yaml") +) { + @Test + fun testTaintBadArg() { + checkWithException( + TaintSinkConditions::badArg, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadThis() { + checkWithException( + TaintSinkConditions::badThis, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodArg() { + checkWithException( + TaintSinkConditions::goodArg, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodThis() { + checkWithException( + TaintSinkConditions::goodThis, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSourceConditionsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSourceConditionsTest.kt new file mode 100644 index 0000000000..198d0fe8b1 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/taint/TaintSourceConditionsTest.kt @@ -0,0 +1,70 @@ +package org.utbot.examples.taint + +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.taint.TaintConfigurationProviderResources +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseCheckerForTaint +import org.utbot.testing.isException + +internal class TaintSourceConditionsTest : UtValueTestCaseCheckerForTaint( + testClass = TaintSourceConditions::class, + taintConfigurationProvider = TaintConfigurationProviderResources("taint/TaintSourceConditionsConfig.yaml") +) { + @Test + fun testTaintBadArg() { + checkWithException( + TaintSourceConditions::badArg, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadReturn() { + checkWithException( + TaintSourceConditions::badReturn, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintBadThis() { + checkWithException( + TaintSourceConditions::badThis, + eq(2), // success & taint error + { r -> r.isException() }, + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodArg() { + checkWithException( + TaintSourceConditions::goodArg, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodReturn() { + checkWithException( + TaintSourceConditions::goodReturn, + eq(1), // only success + { r -> r.isSuccess }, + ) + } + + @Test + fun testTaintGoodThis() { + checkWithException( + TaintSourceConditions::goodThis, + eq(1), // only success + { r -> r.isSuccess }, + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/thirdparty/numbers/ArithmeticUtilsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/thirdparty/numbers/ArithmeticUtilsTest.kt index 54a24c8124..fc7658c50d 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/thirdparty/numbers/ArithmeticUtilsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/thirdparty/numbers/ArithmeticUtilsTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.thirdparty.numbers -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker // example from Apache common-numbers internal class ArithmeticUtilsTest : UtValueTestCaseChecker(testClass = ArithmeticUtils::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/CountDownLatchExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/CountDownLatchExamplesTest.kt new file mode 100644 index 0000000000..125251f73f --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/CountDownLatchExamplesTest.kt @@ -0,0 +1,23 @@ +package org.utbot.examples.threads + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testing.AtLeast +import org.utbot.testing.UtValueTestCaseChecker + +class CountDownLatchExamplesTest : UtValueTestCaseChecker(testClass = CountDownLatchExamples::class) { + @Test + fun testGetAndDown() { + check( + CountDownLatchExamples::getAndDown, + eq(2), + { countDownLatch, l -> countDownLatch.count == 0L && l == 0L }, + { countDownLatch, l -> + val firstCount = countDownLatch.count + + firstCount != 0L && l == firstCount - 1 + }, + coverage = AtLeast(83) + ) + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/ExecutorServiceExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/ExecutorServiceExamplesTest.kt new file mode 100644 index 0000000000..dcd8af4731 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/ExecutorServiceExamplesTest.kt @@ -0,0 +1,40 @@ +package org.utbot.examples.threads + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.AtLeast +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException + +// IMPORTANT: most of the these tests test only the symbolic engine +// and should not be used for testing conrete or code generation since they are possibly flaky in the concrete execution +// (see https://github.com/UnitTestBot/UTBotJava/issues/1610) +class ExecutorServiceExamplesTest : UtValueTestCaseChecker(testClass = ExecutorServiceExamples::class) { + @Test + fun testExceptionInExecute() { + withoutConcrete { + withEnabledTestingCodeGeneration(false) { + checkWithException( + ExecutorServiceExamples::throwingInExecute, + ignoreExecutionsNumber, + { r -> r.isException() } + ) + } + } + } + + @Test + fun testChangingCollectionInExecute() { + withoutConcrete { + withEnabledTestingCodeGeneration(false) { + check( + ExecutorServiceExamples::changingCollectionInExecute, + ignoreExecutionsNumber, + { r -> r == 42 }, + coverage = AtLeast(78) + ) + } + } + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/FutureExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/FutureExamplesTest.kt new file mode 100644 index 0000000000..c6c600b46f --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/FutureExamplesTest.kt @@ -0,0 +1,60 @@ +package org.utbot.examples.threads + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.AtLeast +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.isException +import java.util.concurrent.ExecutionException + +// IMPORTANT: most of the these tests test only the symbolic engine +// and should not be used for testing conrete or code generation since they are possibly flaky in the concrete execution +// (see https://github.com/UnitTestBot/UTBotJava/issues/1610) +class FutureExamplesTest : UtValueTestCaseChecker(testClass = FutureExamples::class) { + @Test + fun testThrowingRunnable() { + withoutConcrete { + checkWithException( + FutureExamples::throwingRunnableExample, + eq(1), + { r -> r.isException() }, + coverage = AtLeast(71) + ) + } + } + + @Test + fun testResultFromGet() { + check( + FutureExamples::resultFromGet, + eq(1), + { r -> r == 42 }, + ) + } + + @Test + fun testChangingCollectionInFuture() { + withEnabledTestingCodeGeneration(false) { + check( + FutureExamples::changingCollectionInFuture, + eq(1), + { r -> r == 42 }, + ) + } + } + + @Test + fun testChangingCollectionInFutureWithoutGet() { + withoutConcrete { + withEnabledTestingCodeGeneration(false) { + check( + FutureExamples::changingCollectionInFutureWithoutGet, + eq(1), + { r -> r == 42 }, + coverage = AtLeast(78) + ) + } + } + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/ThreadExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/ThreadExamplesTest.kt new file mode 100644 index 0000000000..3432eb8779 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/threads/ThreadExamplesTest.kt @@ -0,0 +1,54 @@ +package org.utbot.examples.threads + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.withoutConcrete +import org.utbot.testing.AtLeast +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException + +// IMPORTANT: most of the these tests test only the symbolic engine +// and should not be used for testing conrete or code generation since they are possibly flaky in the concrete execution +// (see https://github.com/UnitTestBot/UTBotJava/issues/1610) +class ThreadExamplesTest : UtValueTestCaseChecker(testClass = ThreadExamples::class) { + @Test + fun testExceptionInStart() { + withoutConcrete { + withEnabledTestingCodeGeneration(false) { + checkWithException( + ThreadExamples::explicitExceptionInStart, + ignoreExecutionsNumber, + { r -> r.isException() } + ) + } + } + } + + @Test + fun testChangingCollectionInThread() { + withoutConcrete { + withEnabledTestingCodeGeneration(false) { + check( + ThreadExamples::changingCollectionInThread, + ignoreExecutionsNumber, + { r -> r == 42 }, + coverage = AtLeast(81) + ) + } + } + } + + @Test + fun testChangingCollectionInThreadWithoutStart() { + withoutConcrete { + withEnabledTestingCodeGeneration(false) { + checkWithException( + ThreadExamples::changingCollectionInThreadWithoutStart, + ignoreExecutionsNumber, + { r -> r.isException() }, + coverage = AtLeast(81) + ) + } + } + } +} diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/CastExamplesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/CastExamplesTest.kt index a30bcb4f39..1cc939a373 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/CastExamplesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/CastExamplesTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.types -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker @Suppress("SimplifyNegatedBinaryExpression") internal class CastExamplesTest : UtValueTestCaseChecker(testClass = CastExamples::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/GenericsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/GenericsTest.kt new file mode 100644 index 0000000000..d77d4db529 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/GenericsTest.kt @@ -0,0 +1,74 @@ +package org.utbot.examples.types + +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.ignoreExecutionsNumber + +internal class GenericsTest : UtValueTestCaseChecker( + testClass = GenericsTest::class, + testCodeGeneration = false // TODO empty files are generated https://github.com/UnitTestBot/UTBotJava/issues/1616 +) { + @Test + fun mapAsParameterTest() { + check( + Generics<*>::mapAsParameter, + eq(2), + { map, _ -> map == null }, + { map, r -> map != null && r == "value" }, + ) + } + + @Test + @Disabled("https://github.com/UnitTestBot/UTBotJava/issues/1620 wrong equals") + fun genericAsFieldTest() { + check( + Generics<*>::genericAsField, + ignoreExecutionsNumber, + { obj, r -> obj?.field == null && r == false }, + // we can cover this line with any of these two conditions + { obj, r -> (obj.field != null && obj.field != "abc" && r == false) || (obj.field == "abc" && r == true) }, + ) + } + + @Test + fun mapAsStaticFieldTest() { + check( + Generics<*>::mapAsStaticField, + ignoreExecutionsNumber, + { r -> r == "value" }, + ) + } + + @Test + fun mapAsNonStaticFieldTest() { + check( + Generics<*>::mapAsNonStaticField, + ignoreExecutionsNumber, + { map, _ -> map == null }, + { map, r -> map != null && r == "value" }, + ) + } + + @Test + fun methodWithRawTypeTest() { + check( + Generics<*>::methodWithRawType, + eq(2), + { map, _ -> map == null }, + { map, r -> map != null && r == "value" }, + ) + } + + @Test + fun testMethodWithArrayTypeBoundary() { + checkWithException( + Generics<*>::methodWithArrayTypeBoundary, + eq(1), + { r -> r.exceptionOrNull() is java.lang.NullPointerException }, + coverage = DoNotCalculate + ) + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/PathDependentGenericsExampleTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/PathDependentGenericsExampleTest.kt new file mode 100644 index 0000000000..14ff4ed25f --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/PathDependentGenericsExampleTest.kt @@ -0,0 +1,33 @@ +package org.utbot.examples.types + +import org.junit.jupiter.api.Test +import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast + +internal class PathDependentGenericsExampleTest : UtValueTestCaseChecker( + testClass = PathDependentGenericsExample::class, + configurations = ignoreKotlinCompilationConfigurations, +) { + @Test + fun testPathDependentGenerics() { + check( + PathDependentGenericsExample::pathDependentGenerics, + eq(3), + { elem, r -> elem is ClassWithOneGeneric<*> && r == 1 }, + { elem, r -> elem is ClassWithTwoGenerics<*, *> && r == 2 }, + { elem, r -> elem !is ClassWithOneGeneric<*> && elem !is ClassWithTwoGenerics<*, *> && r == 3 }, + ) + } + + @Test + fun testFunctionWithSeveralTypeConstraintsForTheSameObject() { + check( + PathDependentGenericsExample::functionWithSeveralTypeConstraintsForTheSameObject, + eq(2), + { e, r -> e !is List<*> && r == 3 }, + { e, r -> e is List<*> && r == 1 }, + coverage = atLeast(89) + ) + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/TypeBordersTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/TypeBordersTest.kt index a379e4518b..a2251d00c4 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/TypeBordersTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/TypeBordersTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.types -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.atLeast import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.atLeast internal class TypeBordersTest : UtValueTestCaseChecker(testClass = TypeBorders::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/TypeMatchesTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/TypeMatchesTest.kt index 1b0735f765..4a3f8d3c76 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/TypeMatchesTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/types/TypeMatchesTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.types -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker @Suppress("SimplifyNegatedBinaryExpression") internal class TypeMatchesTest : UtValueTestCaseChecker(testClass = TypeMatches::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/unsafe/UnsafeOperationsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/unsafe/UnsafeOperationsTest.kt index 5995993cee..03d29c8eab 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/unsafe/UnsafeOperationsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/unsafe/UnsafeOperationsTest.kt @@ -4,8 +4,8 @@ import org.junit.jupiter.api.Test import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.testcheckers.eq import org.utbot.testcheckers.withoutSandbox -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.tests.infrastructure.UtValueTestCaseChecker +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class UnsafeOperationsTest : UtValueTestCaseChecker(testClass = UnsafeOperations::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/unsafe/UnsafeWithFieldTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/unsafe/UnsafeWithFieldTest.kt index 919c0da3cf..4b486a3d23 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/unsafe/UnsafeWithFieldTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/unsafe/UnsafeWithFieldTest.kt @@ -1,8 +1,8 @@ package org.utbot.examples.unsafe -import org.utbot.tests.infrastructure.UtValueTestCaseChecker import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.UtValueTestCaseChecker internal class UnsafeWithFieldTest: UtValueTestCaseChecker(UnsafeWithField::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/BooleanWrapperTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/BooleanWrapperTest.kt index 649735906f..6f97af67d6 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/BooleanWrapperTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/BooleanWrapperTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.wrappers -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class BooleanWrapperTest : UtValueTestCaseChecker(testClass = BooleanWrapper::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/ByteWrapperTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/ByteWrapperTest.kt index c8db0cbec7..489e04fcf7 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/ByteWrapperTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/ByteWrapperTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.wrappers -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class ByteWrapperTest : UtValueTestCaseChecker(testClass = ByteWrapper::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/CharacterWrapperTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/CharacterWrapperTest.kt index e9f4bb5f3a..0995b70559 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/CharacterWrapperTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/CharacterWrapperTest.kt @@ -1,21 +1,16 @@ package org.utbot.examples.wrappers -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker // TODO failed Kotlin compilation internal class CharacterWrapperTest : UtValueTestCaseChecker( testClass = CharacterWrapper::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun primitiveToWrapperTest() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/DoubleWrapperTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/DoubleWrapperTest.kt index 3c7cb5f2f1..d355ca86ac 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/DoubleWrapperTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/DoubleWrapperTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.wrappers -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker @Suppress("SimplifyNegatedBinaryExpression") internal class DoubleWrapperTest : UtValueTestCaseChecker(testClass = DoubleWrapper::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/FloatWrapperTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/FloatWrapperTest.kt index 2265aa1bda..fcb3eeae52 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/FloatWrapperTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/FloatWrapperTest.kt @@ -1,9 +1,9 @@ package org.utbot.examples.wrappers -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker @Suppress("SimplifyNegatedBinaryExpression") internal class FloatWrapperTest : UtValueTestCaseChecker(testClass = FloatWrapper::class) { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/IntegerWrapperTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/IntegerWrapperTest.kt index 29231c1b1a..f9e360c352 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/IntegerWrapperTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/IntegerWrapperTest.kt @@ -1,10 +1,10 @@ package org.utbot.examples.wrappers -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class IntegerWrapperTest : UtValueTestCaseChecker(testClass = IntegerWrapper::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/LongWrapperTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/LongWrapperTest.kt index 51b4de5557..4f418a35ac 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/LongWrapperTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/LongWrapperTest.kt @@ -1,21 +1,16 @@ package org.utbot.examples.wrappers -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq import org.utbot.testcheckers.withoutMinimization -import org.utbot.tests.infrastructure.CodeGeneration +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class LongWrapperTest : UtValueTestCaseChecker( testClass = LongWrapper::class, testCodeGeneration = true, - languagePipelines = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) - ) + configurations = ignoreKotlinCompilationConfigurations, ) { @Test fun primitiveToWrapperTest() { diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/ShortWrapperTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/ShortWrapperTest.kt index a8b179d805..ec4e0498e2 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/ShortWrapperTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/examples/wrappers/ShortWrapperTest.kt @@ -1,10 +1,10 @@ package org.utbot.examples.wrappers -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.DoNotCalculate import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.utbot.testcheckers.eq +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.UtValueTestCaseChecker internal class ShortWrapperTest : UtValueTestCaseChecker(testClass = ShortWrapper::class) { @Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/assemble/AssembleModelGeneratorTests.kt b/utbot-framework-test/src/test/kotlin/org/utbot/framework/assemble/AssembleModelGeneratorTests.kt index 7a252bb33e..c2bee2ab8d 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/assemble/AssembleModelGeneratorTests.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/framework/assemble/AssembleModelGeneratorTests.kt @@ -49,6 +49,7 @@ import org.utbot.framework.plugin.api.UtNullModel import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.UtContext.Companion.setUtContext +import org.utbot.framework.plugin.api.util.arrayTypeOf import org.utbot.framework.plugin.api.util.executableId import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.intArrayClassId @@ -56,14 +57,14 @@ import org.utbot.framework.plugin.api.util.intClassId import org.utbot.framework.plugin.services.JdkInfoDefaultProvider import org.utbot.framework.util.SootUtils import org.utbot.framework.util.instanceCounter -import org.utbot.framework.util.modelIdCounter +import java.util.concurrent.atomic.AtomicInteger import kotlin.reflect.full.functions -import org.utbot.framework.codegen.model.constructor.util.arrayTypeOf /** * Test classes must be located in the same folder as [AssembleTestUtils] class. */ class AssembleModelGeneratorTests { + val modelIdCounter = AtomicInteger(0) private lateinit var context: AutoCloseable private lateinit var statementsChain: MutableList diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/BaseConstructorTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/BaseConstructorTest.kt deleted file mode 100644 index 71c63531fa..0000000000 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/BaseConstructorTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.utbot.framework.concrete.constructors - -import org.utbot.engine.ValueConstructor -import org.utbot.framework.concrete.UtModelConstructor -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.framework.plugin.api.util.id -import java.util.IdentityHashMap -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.BeforeEach - -abstract class BaseConstructorTest { - private lateinit var cookie: AutoCloseable - - @BeforeEach - fun setup() { - cookie = UtContext.setUtContext(UtContext(ClassLoader.getSystemClassLoader())) - } - - @AfterEach - fun tearDown() { - cookie.close() - } - - protected fun computeReconstructed(value: T): T { - val model = UtModelConstructor(IdentityHashMap()).construct(value, value::class.java.id) - - Assertions.assertTrue(model is UtAssembleModel) - - @Suppress("UNCHECKED_CAST") - return ValueConstructor().construct(listOf(model)).single().value as T - } -} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/minimization/MinimizationGreedyEssentialTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/framework/minimization/MinimizationGreedyEssentialTest.kt index 993f23059a..2a96a677de 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/minimization/MinimizationGreedyEssentialTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/framework/minimization/MinimizationGreedyEssentialTest.kt @@ -68,4 +68,18 @@ class MinimizationGreedyEssentialTest { val minimizedExecutions = GreedyEssential.minimize(executions) assertEquals((1..size).toList(), minimizedExecutions.sorted()) } + + @Test + fun testExecutionPriority() { + val executions = mapOf( + 0 to listOf(0, 1, 2, 3, 4), + 1 to listOf(0, 1, 2, 3, 4) + ) + val executionToPriority = mapOf( + 0 to 1, + 1 to 0 + ) + val minimizedExecutions = GreedyEssential.minimize(executions, executionToPriority) + assertEquals(listOf(1), minimizedExecutions) + } } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/modificators/UtBotFieldModificatorsTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/framework/modificators/UtBotFieldModificatorsTest.kt index 7fd12c48b1..b552dc7176 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/modificators/UtBotFieldModificatorsTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/framework/modificators/UtBotFieldModificatorsTest.kt @@ -12,10 +12,10 @@ import org.utbot.examples.modificators.StronglyConnectedComponents import org.utbot.examples.modificators.coupling.ClassA import org.utbot.examples.modificators.coupling.ClassB import org.utbot.examples.modificators.hierarchy.InheritedModifications -import org.utbot.framework.modifications.AnalysisMode -import org.utbot.framework.modifications.AnalysisMode.AllModificators -import org.utbot.framework.modifications.AnalysisMode.SettersAndDirectAccessors -import org.utbot.framework.modifications.UtBotFieldsModificatorsSearcher +import org.utbot.modifications.AnalysisMode +import org.utbot.modifications.AnalysisMode.AllModificators +import org.utbot.modifications.AnalysisMode.SettersAndDirectAccessors +import org.utbot.modifications.UtBotFieldsModificatorsSearcher import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.id import kotlin.reflect.KClass @@ -24,9 +24,9 @@ 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 -import org.utbot.common.nameOfPackage import org.utbot.framework.plugin.services.JdkInfoDefaultProvider import org.utbot.framework.util.SootUtils +import org.utbot.modifications.FieldInvolvementMode internal class UtBotFieldModificatorsTest { private lateinit var fieldsModificatorsSearcher: UtBotFieldsModificatorsSearcher @@ -176,7 +176,9 @@ internal class UtBotFieldModificatorsTest { forceReload = false, jdkInfo = JdkInfoDefaultProvider().info ) - fieldsModificatorsSearcher = UtBotFieldsModificatorsSearcher() + fieldsModificatorsSearcher = UtBotFieldsModificatorsSearcher( + fieldInvolvementMode = FieldInvolvementMode.WriteOnly + ) } private fun runUpdate(classes: Set>) { @@ -193,7 +195,7 @@ internal class UtBotFieldModificatorsTest { //We use sorting here to make comparing with sorted in advance expected collections easier private fun runFieldModificatorsSearch(analysisMode: AnalysisMode) = - fieldsModificatorsSearcher.findModificators(analysisMode) + fieldsModificatorsSearcher.getFieldToModificators(analysisMode) .map { (key, value) -> val modificatorNames = value.filterNot { it.name.startsWith("direct_set_") }.map { it.name } key.name to modificatorNames.toSortedSet() diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/plugin/api/MockStrategyApiTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/framework/plugin/api/MockStrategyApiTest.kt index 4ce43e1a95..21e3730342 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/plugin/api/MockStrategyApiTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/framework/plugin/api/MockStrategyApiTest.kt @@ -31,6 +31,16 @@ internal class MockStrategyApiTest { ) } + @Test + fun ensureDefaultStrategyIsOtherClassesInSpringApplication() { + assertEquals( + MockStrategyApi.OTHER_CLASSES, + MockStrategyApi.springDefaultItem, + "Expecting that ${MockStrategyApi.OTHER_CLASSES} is the default policy for Mocks in Spring application" + + "but ${MockStrategyApi.springDefaultItem} found" + ) + } + @Test fun testLabelToEnum() { assertEquals( diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/z3/extension/Z3ExtensionForTests.kt b/utbot-framework-test/src/test/kotlin/org/utbot/framework/z3/extension/Z3ExtensionForTests.kt index fdc65fd713..a0e9efcb9f 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/z3/extension/Z3ExtensionForTests.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/framework/z3/extension/Z3ExtensionForTests.kt @@ -5,19 +5,19 @@ package com.microsoft.z3 import kotlin.reflect.KProperty -operator fun ArithExpr.plus(other: ArithExpr): ArithExpr = context.mkAdd(this, other) -operator fun ArithExpr.plus(other: Int): ArithExpr = this + context.mkInt(other) +operator fun ArithExpr<*>.plus(other: ArithExpr<*>): ArithExpr<*> = context.mkAdd(this, other) +operator fun ArithExpr<*>.plus(other: Int): ArithExpr<*> = this + context.mkInt(other) -operator fun ArithExpr.minus(other: ArithExpr): ArithExpr = context.mkSub(this, other) -operator fun ArithExpr.minus(other: Int): ArithExpr = this - context.mkInt(other) +operator fun ArithExpr<*>.minus(other: ArithExpr<*>): ArithExpr<*> = context.mkSub(this, other) +operator fun ArithExpr<*>.minus(other: Int): ArithExpr<*> = this - context.mkInt(other) -operator fun ArithExpr.times(other: ArithExpr): ArithExpr = context.mkMul(this, other) -operator fun ArithExpr.times(other: Int): ArithExpr = this * context.mkInt(other) +operator fun ArithExpr<*>.times(other: ArithExpr<*>): ArithExpr<*> = context.mkMul(this, other) +operator fun ArithExpr<*>.times(other: Int): ArithExpr<*> = this * context.mkInt(other) -infix fun Expr.`=`(other: Int): BoolExpr = this eq context.mkInt(other) -infix fun Expr.`=`(other: Expr): BoolExpr = this eq other -infix fun Expr.eq(other: Expr): BoolExpr = context.mkEq(this, other) -infix fun Expr.`!=`(other: Expr): BoolExpr = context.mkNot(this `=` other) +infix fun Expr<*>.`=`(other: Int): BoolExpr = this eq context.mkInt(other) +infix fun Expr<*>.`=`(other: Expr<*>): BoolExpr = this eq other +infix fun Expr<*>.eq(other: Expr<*>): BoolExpr = context.mkEq(this, other) +infix fun Expr<*>.`!=`(other: Expr<*>): BoolExpr = context.mkNot(this `=` other) operator fun BoolExpr.not(): BoolExpr = context.mkNot(this) infix fun BoolExpr.`⇒`(other: BoolExpr): BoolExpr = this implies other @@ -27,20 +27,24 @@ infix fun BoolExpr.implies(other: BoolExpr): BoolExpr = context.mkImplies(this, infix fun BoolExpr.and(other: BoolExpr): BoolExpr = context.mkAnd(this, other) infix fun BoolExpr.or(other: BoolExpr): BoolExpr = context.mkOr(this, other) -infix fun ArithExpr.gt(other: ArithExpr): BoolExpr = context.mkGt(this, other) -infix fun ArithExpr.gt(other: Int): BoolExpr = context.mkGt(this, context.mkInt(other)) +infix fun ArithExpr<*>.gt(other: ArithExpr<*>): BoolExpr = context.mkGt(this, other) +infix fun ArithExpr<*>.gt(other: Int): BoolExpr = context.mkGt(this, context.mkInt(other)) -infix fun ArithExpr.`=`(other: Int): BoolExpr = context.mkEq(this, context.mkInt(other)) +infix fun ArithExpr<*>.`=`(other: Int): BoolExpr = context.mkEq(this, context.mkInt(other)) -operator fun ArrayExpr.get(index: IntExpr): Expr = context.mkSelect(this, index) -operator fun ArrayExpr.get(index: Int): Expr = this[context.mkInt(index)] -fun ArrayExpr.set(index: IntExpr, value: Expr): ArrayExpr = context.mkStore(this, index, value) -fun ArrayExpr.set(index: Int, value: Expr): ArrayExpr = set(context.mkInt(index), value) +operator fun ArrayExpr<*, *>.get(index: IntExpr): Expr<*> = context.mkSelect(this, index.cast()) +operator fun ArrayExpr<*, *>.get(index: Int): Expr<*> = this[context.mkInt(index)] +fun ArrayExpr<*, *>.set(index: IntExpr, value: Expr<*>): ArrayExpr<*, *> = context.mkStore(this, index.cast(), value.cast()) +fun ArrayExpr<*, *>.set(index: Int, value: Expr<*>): ArrayExpr<*, *> = set(context.mkInt(index), value) -operator fun SeqExpr.plus(other: SeqExpr): SeqExpr = context.mkConcat(this, other) -operator fun SeqExpr.plus(other: String): SeqExpr = context.mkConcat(context.mkString(other)) +operator fun SeqExpr<*>.plus(other: SeqExpr<*>): SeqExpr<*> = context.mkConcat(this, other.cast()) +operator fun SeqExpr<*>.plus(other: String): SeqExpr<*> = context.mkConcat(context.mkString(other)) -infix fun SeqExpr.`=`(other: String): BoolExpr = context.mkEq(this, context.mkString(other)) +@Suppress("UNCHECKED_CAST") +fun Expr.cast(): Expr = this as Expr + + +infix fun SeqExpr<*>.`=`(other: String): BoolExpr = context.mkEq(this, context.mkString(other)) class Const(private val ctx: Context, val produce: (Context, name: String) -> T) { operator fun getValue(thisRef: Nothing?, property: KProperty<*>): T = produce(ctx, property.name) @@ -49,12 +53,12 @@ class Const(private val ctx: Context, val produce: (Context, name: String) -> fun Context.declareInt() = Const(this) { ctx, name -> ctx.mkIntConst(name) } fun Context.declareBool() = Const(this) { ctx, name -> ctx.mkBoolConst(name) } fun Context.declareReal() = Const(this) { ctx, name -> ctx.mkRealConst(name) } -fun Context.declareString() = Const(this) { ctx, name -> ctx.mkConst(name, stringSort) as SeqExpr } -fun Context.declareList(sort: ListSort) = Const(this) { ctx, name -> ctx.mkConst(name, sort) } +fun Context.declareString() = Const(this) { ctx, name -> ctx.mkConst(name, stringSort) as SeqExpr<*> } +fun Context.declareList(sort: ListSort<*>) = Const(this) { ctx, name -> ctx.mkConst(name, sort) } fun Context.declareIntArray() = Const(this) { ctx, name -> ctx.mkArrayConst(name, intSort, intSort) } -operator fun FuncDecl.invoke(vararg expr: Expr): Expr = context.mkApp(this, *expr) +operator fun FuncDecl<*>.invoke(vararg expr: Expr<*>): Expr<*> = context.mkApp(this, *expr) -fun Model.eval(expr: Expr): Expr = this.eval(expr, true) -fun Model.eval(vararg exprs: Expr): List = exprs.map { this.eval(it, true) } \ No newline at end of file +fun Model.eval(expr: Expr<*>): Expr<*> = this.eval(expr, true) +fun Model.eval(vararg exprs: Expr<*>): List> = exprs.map { this.eval(it, true) } \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/z3/extension/Z3ExtensionTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/framework/z3/extension/Z3ExtensionTest.kt index c31298bac8..896d1e3a98 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/z3/extension/Z3ExtensionTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/framework/z3/extension/Z3ExtensionTest.kt @@ -99,8 +99,8 @@ class Z3ExtensionTest : Z3Initializer() { val (aVal, bVal, cVal) = solver.model .eval(a, b, c) - .filterIsInstance() - .map(SeqExpr::getString) + .filterIsInstance>() + .map(SeqExpr<*>::getString) .also { list -> assertEquals(3, list.size) } assertEquals("abcd", "$aVal$bVal") diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt index 888d02a816..cfc4fdf4f6 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt +++ b/utbot-framework-test/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt @@ -1,15 +1,8 @@ package org.utbot.sarif -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue import org.junit.Test import org.mockito.Mockito -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.UtExecution -import org.utbot.framework.plugin.api.UtImplicitlyThrownException -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.UtMethodTestSet -import org.utbot.framework.plugin.api.UtSymbolicExecution +import org.utbot.framework.plugin.api.* class SarifReportTest { @@ -19,7 +12,7 @@ class SarifReportTest { testSets = listOf(), generatedTestsCode = "", sourceFindingEmpty - ).createReport() + ).createReport().toJson() assert(actualReport.isNotEmpty()) } @@ -30,7 +23,7 @@ class SarifReportTest { testSets = listOf(testSet), generatedTestsCode = "", sourceFindingEmpty - ).createReport().toSarif() + ).createReport() assert(sarif.runs.first().results.isEmpty()) } @@ -51,6 +44,9 @@ class SarifReportTest { ) Mockito.`when`(mockUtExecutionAIOBE.stateBefore.parameters).thenReturn(listOf()) + defaultMockCoverage(mockUtExecutionNPE) + defaultMockCoverage(mockUtExecutionAIOBE) + val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecutionNPE)), UtMethodTestSet(mockExecutableId, listOf(mockUtExecutionAIOBE)) @@ -60,7 +56,7 @@ class SarifReportTest { testSets = testSets, generatedTestsCode = "", sourceFindingEmpty - ).createReport().toSarif() + ).createReport() assert(report.runs.first().results[0].message.text.contains("NullPointerException")) assert(report.runs.first().results[1].message.text.contains("ArrayIndexOutOfBoundsException")) @@ -73,14 +69,14 @@ class SarifReportTest { Mockito.`when`(mockUtExecution.result).thenReturn( UtImplicitlyThrownException(NullPointerException(), false) ) + mockCoverage(mockUtExecution, 1337, "Main") Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) - Mockito.`when`(mockUtExecution.path.lastOrNull()?.stmt?.javaSourceStartLineNumber).thenReturn(1337) Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain_ThrowArithmeticException") - val report = sarifReportMain.createReport().toSarif() + val report = sarifReportMain.createReport() val result = report.runs.first().results.first() - val location = result.locations.first().physicalLocation + val location = result.locations.filterIsInstance().first().physicalLocation val relatedLocation = result.relatedLocations.first().physicalLocation assert(location.artifactLocation.uri.contains("Main.java")) @@ -105,7 +101,9 @@ class SarifReportTest { ) ) - val report = sarifReportMain.createReport().toSarif() + defaultMockCoverage(mockUtExecution) + + val report = sarifReportMain.createReport() val result = report.runs.first().results.first() assert(result.message.text.contains("227")) @@ -128,7 +126,9 @@ class SarifReportTest { ) Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) - val report = sarifReportMain.createReport().toSarif() + defaultMockCoverage(mockUtExecution) + + val report = sarifReportMain.createReport() val result = report.runs.first().results.first().codeFlows.first().threadFlows.first().locations.map { it.location.physicalLocation @@ -139,6 +139,65 @@ class SarifReportTest { } } + @Test + fun testProcessSandboxFailure() { + mockUtMethodNames() + + val uncheckedException = Mockito.mock(java.security.NoSuchAlgorithmException::class.java) + Mockito.`when`(uncheckedException.stackTrace).thenReturn(arrayOf()) + Mockito.`when`(mockUtExecution.result).thenReturn(UtSandboxFailure(uncheckedException)) + + defaultMockCoverage(mockUtExecution) + + val report = sarifReportMain.createReport() + val result = report.runs.first().results.first() + assert(result.message.text.contains("NoSuchAlgorithmException")) + } + + @Test + fun testProcessTimeoutException() { + mockUtMethodNames() + + val timeoutMessage = "Timeout 1000 elapsed" + val timeoutException = TimeoutException(timeoutMessage) + Mockito.`when`(mockUtExecution.result).thenReturn(UtTimeoutException(timeoutException)) + + defaultMockCoverage(mockUtExecution) + + val report = sarifReportMain.createReport() + val result = report.runs.first().results.first() + assert(result.message.text.contains(timeoutMessage)) + } + + @Test + fun testConvertCoverageToStackTrace() { + mockUtMethodNames() + + val timeoutException = TimeoutException("Timeout 1000 elapsed") + Mockito.`when`(mockUtExecution.result).thenReturn(UtTimeoutException(timeoutException)) + + val classMainPath = "com/abc/Main" + val classUtilPath = "com/cba/Util" + Mockito.`when`(mockUtExecution.symbolicSteps).thenReturn(listOf()) + Mockito.`when`(mockUtExecution.coverage?.coveredInstructions).thenReturn(listOf( + Instruction(classMainPath, "main(l)l", 3, 1), + Instruction(classMainPath, "main(l)l", 4, 2), + Instruction(classMainPath, "main(l)l", 4, 3), // last for main + Instruction(classUtilPath, "util(ll)l", 6, 4), + Instruction(classUtilPath, "util(ll)l", 7, 5), // last for util + )) + + val report = sarifReportMain.createReport() + val result = report.runs.first().results.first() + val stackTrace = result.codeFlows.first().threadFlows.first().locations + + assert(stackTrace[0].location.physicalLocation.artifactLocation.uri == "$classMainPath.java") + assert(stackTrace[0].location.physicalLocation.region.startLine == 4) + + assert(stackTrace[1].location.physicalLocation.artifactLocation.uri == "$classUtilPath.java") + assert(stackTrace[1].location.physicalLocation.region.startLine == 7) + } + @Test fun testCodeFlowsStartsWithMethodCall() { mockUtMethodNames() @@ -153,7 +212,9 @@ class SarifReportTest { Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain_ThrowArithmeticException") - val report = sarifReportMain.createReport().toSarif() + defaultMockCoverage(mockUtExecution) + + val report = sarifReportMain.createReport() val codeFlowPhysicalLocations = report.runs[0].results[0].codeFlows[0].threadFlows[0].locations.map { it.location.physicalLocation @@ -177,7 +238,9 @@ class SarifReportTest { Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain_ThrowArithmeticException") - val report = sarifReportPrivateMain.createReport().toSarif() + defaultMockCoverage(mockUtExecution) + + val report = sarifReportPrivateMain.createReport() val codeFlowPhysicalLocations = report.runs[0].results[0].codeFlows[0].threadFlows[0].locations.map { it.location.physicalLocation @@ -194,6 +257,8 @@ class SarifReportTest { val mockUtExecution = Mockito.mock(UtExecution::class.java, Mockito.RETURNS_DEEP_STUBS) Mockito.`when`(mockUtExecution.result).thenReturn(UtImplicitlyThrownException(NullPointerException(), false)) + defaultMockCoverage(mockUtExecution) + val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecution)), UtMethodTestSet(mockExecutableId, listOf(mockUtExecution)) // duplicate @@ -203,7 +268,7 @@ class SarifReportTest { testSets = testSets, generatedTestsCode = "", sourceFindingMain - ).createReport().toSarif() + ).createReport() assert(report.runs.first().results.size == 1) // no duplicates } @@ -219,6 +284,9 @@ class SarifReportTest { Mockito.`when`(mockUtExecution1.result).thenReturn(UtImplicitlyThrownException(NullPointerException(), false)) Mockito.`when`(mockUtExecution2.result).thenReturn(UtImplicitlyThrownException(ArithmeticException(), false)) + defaultMockCoverage(mockUtExecution1) + defaultMockCoverage(mockUtExecution2) + val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecution1)), UtMethodTestSet(mockExecutableId, listOf(mockUtExecution2)) // not a duplicate @@ -228,7 +296,7 @@ class SarifReportTest { testSets = testSets, generatedTestsCode = "", sourceFindingMain - ).createReport().toSarif() + ).createReport() assert(report.runs.first().results.size == 2) // no results have been removed } @@ -245,8 +313,8 @@ class SarifReportTest { Mockito.`when`(mockUtExecution2.result).thenReturn(UtImplicitlyThrownException(NullPointerException(), false)) // different locations - Mockito.`when`(mockUtExecution1.path.lastOrNull()?.stmt?.javaSourceStartLineNumber).thenReturn(11) - Mockito.`when`(mockUtExecution2.path.lastOrNull()?.stmt?.javaSourceStartLineNumber).thenReturn(22) + mockCoverage(mockUtExecution1, 11, "Main") + mockCoverage(mockUtExecution2, 22, "Main") val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecution1)), @@ -257,7 +325,7 @@ class SarifReportTest { testSets = testSets, generatedTestsCode = "", sourceFindingMain - ).createReport().toSarif() + ).createReport() assert(report.runs.first().results.size == 2) // no results have been removed } @@ -282,6 +350,9 @@ class SarifReportTest { Mockito.`when`(mockNPE1.stackTrace).thenReturn(arrayOf(stackTraceElement1)) Mockito.`when`(mockNPE2.stackTrace).thenReturn(arrayOf(stackTraceElement1, stackTraceElement2)) + defaultMockCoverage(mockUtExecution1) + defaultMockCoverage(mockUtExecution2) + val testSets = listOf( UtMethodTestSet(mockExecutableId, listOf(mockUtExecution1)), UtMethodTestSet(mockExecutableId, listOf(mockUtExecution2)) // duplicate with a longer stack trace @@ -291,7 +362,7 @@ class SarifReportTest { testSets = testSets, generatedTestsCode = "", sourceFindingMain - ).createReport().toSarif() + ).createReport() assert(report.runs.first().results.size == 1) // no duplicates assert(report.runs.first().results.first().totalCodeFlowLocations() == 1) // with a shorter stack trace @@ -310,7 +381,21 @@ class SarifReportTest { Mockito.`when`(mockExecutableId.classId.name).thenReturn("Main") } - private fun String.toSarif(): Sarif = jacksonObjectMapper().readValue(this) + private fun mockCoverage(mockExecution: UtExecution, lineNumber: Int, className: String) { + Mockito.`when`(mockExecution.coverage?.coveredInstructions?.lastOrNull()?.lineNumber).thenReturn(1) + Mockito.`when`(mockExecution.coverage?.coveredInstructions?.lastOrNull()?.internalName).thenReturn("Main") + Mockito.`when`(mockExecution.coverage?.coveredInstructions?.lastOrNull()?.className).thenReturn("Main") + (mockExecution as? UtSymbolicExecution)?.let { mockSymbolicSteps(it, lineNumber, className) } + } + + private fun mockSymbolicSteps(mockExecution: UtSymbolicExecution, lineNumber: Int, className: String) { + Mockito.`when`(mockExecution.symbolicSteps.lastOrNull()?.lineNumber).thenReturn(lineNumber) + Mockito.`when`(mockExecution.symbolicSteps.lastOrNull()?.method?.declaringClass?.name).thenReturn(className) + } + + private fun defaultMockCoverage(mockExecution: UtExecution) { + mockCoverage(mockExecution, 1, "Main") + } // constants diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/Constants.kt b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/Constants.kt new file mode 100644 index 0000000000..0dfc8745d6 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/Constants.kt @@ -0,0 +1,25 @@ +package org.utbot.taint.parser.yaml + +internal const val k_sources = Constants.KEY_SOURCES +internal const val k_passes = Constants.KEY_PASSES +internal const val k_cleaners = Constants.KEY_CLEANERS +internal const val k_sinks = Constants.KEY_SINKS + +internal const val k_addTo = Constants.KEY_ADD_TO +internal const val k_getFrom = Constants.KEY_GET_FROM +internal const val k_removeFrom = Constants.KEY_REMOVE_FROM +internal const val k_check = Constants.KEY_CHECK +internal const val k_marks = Constants.KEY_MARKS + +internal const val k_signature = Constants.KEY_SIGNATURE +internal const val k_conditions = Constants.KEY_CONDITIONS + +internal const val k_not = Constants.KEY_CONDITION_NOT + +internal const val k_this = Constants.KEY_THIS +internal const val k_return = Constants.KEY_RETURN +internal const val k_arg = Constants.KEY_ARG + +internal const val k_lt = Constants.ARGUMENT_TYPE_PREFIX +internal const val k_gt = Constants.ARGUMENT_TYPE_SUFFIX +internal const val k__ = Constants.ARGUMENT_TYPE_UNDERSCORE diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/MethodArgumentParserTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/MethodArgumentParserTest.kt new file mode 100644 index 0000000000..290cb8d483 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/MethodArgumentParserTest.kt @@ -0,0 +1,156 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.Yaml +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.junit.jupiter.api.assertThrows + +class MethodArgumentParserTest { + + @Nested + @DisplayName("isArgumentType") + inner class IsArgumentTypeTest { + @Test + fun `should return true on underscore`() { + val yamlScalar = Yaml.default.parseToYamlNode(k__) + assertTrue(MethodArgumentParser.isArgumentType(yamlScalar)) + } + + @Test + fun `should return true on type fqn in brackets`() { + val yamlScalar = Yaml.default.parseToYamlNode("${k_lt}java.lang.String$k_gt") + assertTrue(MethodArgumentParser.isArgumentType(yamlScalar)) + } + + @Test + fun `should return false on values`() { + val yamlScalar = Yaml.default.parseToYamlNode("some-value") + assertFalse(MethodArgumentParser.isArgumentType(yamlScalar)) + } + + @Test + fun `should return false on another yaml type`() { + val yamlList = Yaml.default.parseToYamlNode("[ ${k_lt}int$k_gt ]") + assertFalse(MethodArgumentParser.isArgumentType(yamlList)) + } + } + + @Nested + @DisplayName("isArgumentValue") + inner class IsArgumentValueTest { + @Test + fun `should return true on scalar`() { + val yamlScalar = Yaml.default.parseToYamlNode("0") + assertTrue(MethodArgumentParser.isArgumentValue(yamlScalar)) + } + + @Test + fun `should return true on type fqn in brackets`() { + val yamlNull = Yaml.default.parseToYamlNode("null") + assertTrue(MethodArgumentParser.isArgumentValue(yamlNull)) + } + + @Test + fun `should return false on type fqn in brackets`() { + val yamlScalar = Yaml.default.parseToYamlNode("${k_lt}float$k_gt") + assertFalse(MethodArgumentParser.isArgumentValue(yamlScalar)) + } + + @Test + fun `should return false on another yaml type`() { + val yamlList = Yaml.default.parseToYamlNode("[ test ]") + assertFalse(MethodArgumentParser.isArgumentValue(yamlList)) + } + } + + @Nested + @DisplayName("parseArgumentType") + inner class ParseArgumentTypeTest { + @Test + fun `should parse underscore as ArgumentTypeAny`() { + val yamlScalar = Yaml.default.parseToYamlNode(k__) + val expectedArgumentType = YamlArgumentTypeAny + + val actualArgumentType = MethodArgumentParser.parseArgumentType(yamlScalar) + assertEquals(expectedArgumentType, actualArgumentType) + } + + @Test + fun `should parse type fqn in brackets`() { + val yamlScalar = Yaml.default.parseToYamlNode("${k_lt}double$k_gt") + val expectedArgumentType = YamlArgumentTypeString("double") + + val actualArgumentType = MethodArgumentParser.parseArgumentType(yamlScalar) + assertEquals(expectedArgumentType, actualArgumentType) + } + + @Test + fun `should fail on another yaml type`() { + val yamlMap = Yaml.default.parseToYamlNode("type: ${k_lt}double$k_gt") + + assertThrows { + MethodArgumentParser.parseArgumentType(yamlMap) + } + } + } + + @Nested + @DisplayName("parseArgumentValue") + inner class ParseArgumentValueTest { + @Test + fun `should parse yaml null as ArgumentValueNull`() { + val yamlNull = Yaml.default.parseToYamlNode("null") + val expectedArgumentValue = YamlArgumentValueNull + + val actualArgumentValue = MethodArgumentParser.parseArgumentValue(yamlNull) + assertEquals(expectedArgumentValue, actualArgumentValue) + } + + @Test + fun `should parse boolean yaml scalar`() { + val yamlScalar = Yaml.default.parseToYamlNode("false") + val expectedArgumentValue = YamlArgumentValueBoolean(false) + + val actualArgumentValue = MethodArgumentParser.parseArgumentValue(yamlScalar) + assertEquals(expectedArgumentValue, actualArgumentValue) + } + + @Test + fun `should parse long yaml scalar`() { + val yamlScalar = Yaml.default.parseToYamlNode("17") + val expectedArgumentValue = YamlArgumentValueLong(17L) + + val actualArgumentValue = MethodArgumentParser.parseArgumentValue(yamlScalar) + assertEquals(expectedArgumentValue, actualArgumentValue) + } + + @Test + fun `should parse double yaml scalar`() { + val yamlScalar = Yaml.default.parseToYamlNode("1.2") + val expectedArgumentValue = YamlArgumentValueDouble(1.2) + + val actualArgumentValue = MethodArgumentParser.parseArgumentValue(yamlScalar) + assertEquals(expectedArgumentValue, actualArgumentValue) + } + + @Test + fun `should parse string yaml scalar`() { + val yamlScalar = Yaml.default.parseToYamlNode("some-string") + val expectedArgumentValue = YamlArgumentValueString("some-string") + + val actualArgumentValue = MethodArgumentParser.parseArgumentValue(yamlScalar) + assertEquals(expectedArgumentValue, actualArgumentValue) + } + + @Test + fun `should fail on another yaml type`() { + val yamlList = Yaml.default.parseToYamlNode("[ some-string ]") + + assertThrows { + MethodArgumentParser.parseArgumentValue(yamlList) + } + } + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintConditionParserTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintConditionParserTest.kt new file mode 100644 index 0000000000..5c9968e565 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintConditionParserTest.kt @@ -0,0 +1,188 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.* +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 + +class TaintConditionParserTest { + + @Nested + @DisplayName("parseCondition") + inner class ParseConditionTest { + @Test + fun `should parse yaml null as ValueCondition`() { + val yamlNull = Yaml.default.parseToYamlNode("null") + val expectedCondition = YamlTaintConditionEqualValue(YamlArgumentValueNull) + + val actualCondition = TaintConditionParser.parseCondition(yamlNull) + assertEquals(expectedCondition, actualCondition) + } + + @Test + fun `should parse yaml scalar with argument value as ValueCondition`() { + val yamlScalar = Yaml.default.parseToYamlNode("some-string") + val expectedCondition = YamlTaintConditionEqualValue(YamlArgumentValueString("some-string")) + + val actualCondition = TaintConditionParser.parseCondition(yamlScalar) + assertEquals(expectedCondition, actualCondition) + } + + @Test + fun `should parse yaml scalar with argument type as TypeCondition`() { + val yamlScalar = Yaml.default.parseToYamlNode("${k_lt}java.lang.Integer$k_gt") + val expectedCondition = YamlTaintConditionIsType(YamlArgumentTypeString("java.lang.Integer")) + + val actualCondition = TaintConditionParser.parseCondition(yamlScalar) + assertEquals(expectedCondition, actualCondition) + } + + @Test + fun `should parse yaml list as OrCondition`() { + val yamlList = Yaml.default.parseToYamlNode("[ 1, true, ${k_lt}java.lang.Integer$k_gt ]") + val expectedCondition = YamlTaintConditionOr( + listOf( + YamlTaintConditionEqualValue(YamlArgumentValueLong(1L)), + YamlTaintConditionEqualValue(YamlArgumentValueBoolean(true)), + YamlTaintConditionIsType(YamlArgumentTypeString("java.lang.Integer")), + ) + ) + + val actualCondition = TaintConditionParser.parseCondition(yamlList) + assertEquals(expectedCondition, actualCondition) + } + + @Test + fun `should parse yaml map with a key 'not' as NotCondition`() { + val yamlMap = Yaml.default.parseToYamlNode("$k_not: ${k_lt}int$k_gt") + val expectedCondition = YamlTaintConditionNot(YamlTaintConditionIsType(YamlArgumentTypeString("int"))) + + val actualCondition = TaintConditionParser.parseCondition(yamlMap) + assertEquals(expectedCondition, actualCondition) + } + + @Test + fun `should fail on yaml map without a key 'not'`() { + val yamlMap = Yaml.default.parseToYamlNode("net: ${k_lt}int$k_gt") + + assertThrows { + TaintConditionParser.parseCondition(yamlMap) + } + } + + @Test + fun `should fail on yaml map with unknown keys`() { + val yamlMap = Yaml.default.parseToYamlNode("{ $k_not: ${k_lt}int$k_gt, unknown-key: 0 }") + + assertThrows { + TaintConditionParser.parseCondition(yamlMap) + } + } + + @Test + fun `should parse complicated yaml node`() { + val yamlMap = Yaml.default.parseToYamlNode("$k_not: [ { $k_not: 0 }, ${k_lt}int$k_gt, { $k_not: null } ]") + val expectedCondition = YamlTaintConditionNot( + YamlTaintConditionOr( + listOf( + YamlTaintConditionNot(YamlTaintConditionEqualValue(YamlArgumentValueLong(0L))), + YamlTaintConditionIsType(YamlArgumentTypeString("int")), + YamlTaintConditionNot(YamlTaintConditionEqualValue(YamlArgumentValueNull)) + ) + ) + ) + + val actualCondition = TaintConditionParser.parseCondition(yamlMap) + assertEquals(expectedCondition, actualCondition) + } + + @Test + fun `should fail on another yaml type`() { + val yamlTaggedNode = YamlTaggedNode("some-tag", YamlNull(YamlPath.root)) + + assertThrows { + TaintConditionParser.parseCondition(yamlTaggedNode) + } + } + } + + @Nested + @DisplayName("parseConditions") + inner class ParseConditionsTest { + @Test + fun `should parse correct yaml map as Conditions`() { + val yamlMap = + Yaml.default.parseToYamlNode("{ $k_this: \"\", ${k_arg}2: { $k_not: ${k_lt}int$k_gt }, $k_return: [ 0, 1 ] }") + val expectedConditions = YamlTaintConditionsMap( + mapOf( + YamlTaintEntityThis to YamlTaintConditionEqualValue(YamlArgumentValueString("")), + YamlTaintEntityArgument(2u) to YamlTaintConditionNot( + YamlTaintConditionIsType( + YamlArgumentTypeString( + "int" + ) + ) + ), + YamlTaintEntityReturn to YamlTaintConditionOr( + listOf( + YamlTaintConditionEqualValue(YamlArgumentValueLong(0L)), YamlTaintConditionEqualValue( + YamlArgumentValueLong(1L) + ) + ) + ) + ) + ) + + val actualConditions = TaintConditionParser.parseConditions(yamlMap) + assertEquals(expectedConditions, actualConditions) + } + + @Test + fun `should parse empty yaml map as NoConditions`() { + val yamlMap = Yaml.default.parseToYamlNode("{}") + val expectedConditions = YamlNoTaintConditions + + val actualConditions = TaintConditionParser.parseConditions(yamlMap) + assertEquals(expectedConditions, actualConditions) + } + + @Test + fun `should fail on another yaml type`() { + val yamlList = Yaml.default.parseToYamlNode("[]") + + assertThrows { + TaintConditionParser.parseConditions(yamlList) + } + } + } + + @Nested + @DisplayName("parseConditionsKey") + inner class ParseConditionsKeyTest { + @Test + fun `should parse yaml map with a key 'conditions'`() { + val yamlMap = Yaml.default.parseToYamlNode("$k_conditions: { $k_return: null }").yamlMap + val expectedConditions = YamlTaintConditionsMap( + mapOf( + YamlTaintEntityReturn to YamlTaintConditionEqualValue( + YamlArgumentValueNull + ) + ) + ) + + val actualConditions = TaintConditionParser.parseConditionsKey(yamlMap) + assertEquals(expectedConditions, actualConditions) + } + + @Test + fun `should parse yaml map without a key 'conditions' as NoConditions`() { + val yamlMap = Yaml.default.parseToYamlNode("$k_marks: []").yamlMap + val expectedConditions = YamlNoTaintConditions + + val actualConditions = TaintConditionParser.parseConditionsKey(yamlMap) + assertEquals(expectedConditions, actualConditions) + } + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintConfigurationParserTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintConfigurationParserTest.kt new file mode 100644 index 0000000000..3a57811a41 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintConfigurationParserTest.kt @@ -0,0 +1,54 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.Yaml +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 + +class TaintConfigurationParserTest { + + @Nested + @DisplayName("parseConfiguration") + inner class ParseConfigurationTest { + @Test + fun `should parse yaml map as Configuration`() { + val yamlMap = + Yaml.default.parseToYamlNode("{ $k_sources: [], $k_passes: [], $k_cleaners: [], $k_sinks: [] }") + val expectedConfiguration = YamlTaintConfiguration(listOf(), listOf(), listOf(), listOf()) + + val actualConfiguration = TaintConfigurationParser.parseConfiguration(yamlMap) + assertEquals(expectedConfiguration, actualConfiguration) + } + + @Test + fun `should not fail if yaml map does not contain some keys`() { + val yamlMap = Yaml.default.parseToYamlNode("{ $k_sources: [], $k_sinks: [] }") + val expectedConfiguration = YamlTaintConfiguration(listOf(), listOf(), listOf(), listOf()) + + val actualConfiguration = TaintConfigurationParser.parseConfiguration(yamlMap) + assertEquals(expectedConfiguration, actualConfiguration) + } + + @Test + fun `should fail on other yaml types`() { + val yamlList = Yaml.default.parseToYamlNode("[]") + + assertThrows { + TaintConfigurationParser.parseConfiguration(yamlList) + } + } + + @Test + fun `should fail on yaml map with unknown keys`() { + val yamlMap = Yaml.default.parseToYamlNode( + "{ $k_sources: [], $k_passes: [], $k_cleaners: [], $k_sinks: [], unknown-key: [] }" + ) + + assertThrows { + TaintConfigurationParser.parseConfiguration(yamlMap) + } + } + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintEntityParserTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintEntityParserTest.kt new file mode 100644 index 0000000000..ab73546696 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintEntityParserTest.kt @@ -0,0 +1,104 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.Yaml +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 + +class TaintEntityParserTest { + + @Nested + @DisplayName("taintEntityByName") + inner class TaintEntityByNameTest { + @Test + fun `should return ThisObject on 'this'`() { + val actualEntity = TaintEntityParser.taintEntityByName(k_this) + val expectedEntity = YamlTaintEntityThis + assertEquals(expectedEntity, actualEntity) + } + + @Test + fun `should return ReturnValue on 'return'`() { + val actualEntity = TaintEntityParser.taintEntityByName(k_return) + val expectedEntity = YamlTaintEntityReturn + assertEquals(expectedEntity, actualEntity) + } + + @Test + fun `should return MethodArgument(1) on 'arg1'`() { + val actualEntity = TaintEntityParser.taintEntityByName("${k_arg}1") + val expectedEntity = YamlTaintEntityArgument(1u) + assertEquals(expectedEntity, actualEntity) + } + + @Test + fun `should return MethodArgument(227) on 'arg227'`() { + val actualEntity = TaintEntityParser.taintEntityByName("${k_arg}227") + val expectedEntity = YamlTaintEntityArgument(227u) + assertEquals(expectedEntity, actualEntity) + } + + @Test + fun `should fail on zero index 'arg0'`() { + assertThrows { + TaintEntityParser.taintEntityByName("${k_arg}0") + } + } + + @Test + fun `should fail on another entity name`() { + assertThrows { + TaintEntityParser.taintEntityByName("argument1") + } + } + } + + @Nested + @DisplayName("parseTaintEntities") + inner class ParseTaintEntitiesTest { + @Test + fun `should parse yaml scalar`() { + val yamlScalar = Yaml.default.parseToYamlNode(k_this) + val expectedEntities = YamlTaintEntitiesSet(setOf(YamlTaintEntityThis)) + + val actualEntities = TaintEntityParser.parseTaintEntities(yamlScalar) + assertEquals(expectedEntities, actualEntities) + } + + @Test + fun `should parse yaml list`() { + val yamlList = Yaml.default.parseToYamlNode("[ $k_this, ${k_arg}1, ${k_arg}5, $k_return ]") + val expectedEntities = YamlTaintEntitiesSet( + setOf( + YamlTaintEntityThis, + YamlTaintEntityArgument(1u), + YamlTaintEntityArgument(5u), + YamlTaintEntityReturn + ) + ) + + val actualEntities = TaintEntityParser.parseTaintEntities(yamlList) + assertEquals(expectedEntities, actualEntities) + } + + @Test + fun `should fail on empty yaml list`() { + val yamlListEmpty = Yaml.default.parseToYamlNode("[]") + + assertThrows { + TaintEntityParser.parseTaintEntities(yamlListEmpty) + } + } + + @Test + fun `should fail on another yaml type`() { + val yamlMap = Yaml.default.parseToYamlNode("$k_addTo: $k_return") + + assertThrows { + TaintEntityParser.parseTaintEntities(yamlMap) + } + } + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintMarkParserTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintMarkParserTest.kt new file mode 100644 index 0000000000..3be0b44982 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintMarkParserTest.kt @@ -0,0 +1,58 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.Yaml +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 + +class TaintMarkParserTest { + + @Nested + @DisplayName("parseTaintMarks") + inner class ParseTaintMarksTest { + @Test + fun `should parse yaml scalar`() { + val yamlScalar = Yaml.default.parseToYamlNode("sensitive-data") + val expectedMarks = YamlTaintMarksSet(setOf(YamlTaintMark("sensitive-data"))) + + val actualMarks = TaintMarkParser.parseTaintMarks(yamlScalar) + assertEquals(expectedMarks, actualMarks) + } + + @Test + fun `should parse yaml list`() { + val yamlList = Yaml.default.parseToYamlNode("[ xss, sensitive-data, sql-injection ]") + val expectedMarks = + YamlTaintMarksSet( + setOf( + YamlTaintMark("xss"), + YamlTaintMark("sensitive-data"), + YamlTaintMark("sql-injection") + ) + ) + + val actualMarks = TaintMarkParser.parseTaintMarks(yamlList) + assertEquals(expectedMarks, actualMarks) + } + + @Test + fun `should parse empty yaml list`() { + val yamlListEmpty = Yaml.default.parseToYamlNode("[]") + val expectedMarks = YamlTaintMarksAll + + val actualMarks = TaintMarkParser.parseTaintMarks(yamlListEmpty) + assertEquals(expectedMarks, actualMarks) + } + + @Test + fun `should fail on another yaml type`() { + val yamlMap = Yaml.default.parseToYamlNode("$k_marks: [ xss ]") + + assertThrows { + TaintMarkParser.parseTaintMarks(yamlMap) + } + } + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintRuleParserTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintRuleParserTest.kt new file mode 100644 index 0000000000..e669c27dbd --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintRuleParserTest.kt @@ -0,0 +1,287 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.Yaml +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* + +class TaintRuleParserTest { + + @Nested + @DisplayName("isRule") + inner class IsRuleTest { + + private val isRuleData = listOf( + SourceData to TaintRuleParser::isSourceRule, + PassData to TaintRuleParser::isPassRule, + CleanerData to TaintRuleParser::isCleanerRule, + SinkData to TaintRuleParser::isSinkRule + ) + + @TestFactory + fun `should return true on a rule`() = isRuleData.map { (ruleData, isRule) -> + DynamicTest.dynamicTest(isRule.name) { + val yamlMap = Yaml.default.parseToYamlNode(ruleData.yamlInput) + assertTrue(isRule(yamlMap)) + } + } + + @TestFactory + fun `should return true on a rule with optional keys`() = isRuleData.map { (ruleData, isRule) -> + DynamicTest.dynamicTest(isRule.name) { + val yamlMap = Yaml.default.parseToYamlNode(ruleData.yamlInputAdvanced) + assertTrue(isRule(yamlMap)) + } + } + + @TestFactory + fun `should return false on an invalid rule`() = isRuleData.map { (ruleData, isRule) -> + DynamicTest.dynamicTest(isRule.name) { + val yamlMap = Yaml.default.parseToYamlNode(ruleData.yamlInputInvalid) + assertFalse(isRule(yamlMap)) + } + } + + @TestFactory + fun `should return false on rules node`() = isRuleData.map { (ruleData, isRule) -> + DynamicTest.dynamicTest(isRule.name) { + val rulesData = RulesData(ruleData) + val yamlMap = Yaml.default.parseToYamlNode(rulesData.yamlInput) + assertFalse(isRule(yamlMap)) + } + } + } + + @Nested + @DisplayName("parseRule") + inner class ParseRuleTest { + + private val parseRuleData = listOf( + SourceData to TaintRuleParser::parseSourceRule, + PassData to TaintRuleParser::parsePassRule, + CleanerData to TaintRuleParser::parseCleanerRule, + SinkData to TaintRuleParser::parseSinkRule + ) + + @TestFactory + fun `should parse yaml map that satisfies isRule`() = parseRuleData.map { (ruleData, parseRule) -> + DynamicTest.dynamicTest(parseRule.name) { + val yamlMap = Yaml.default.parseToYamlNode(ruleData.yamlInput) + val expectedRule = ruleData.yamlInputParsed(defaultMethodFqn) + + val actualRule = parseRule(yamlMap, defaultMethodNameParts) + assertEquals(expectedRule, actualRule) + } + } + + @TestFactory + fun `should parse yaml map with optional keys that satisfies isRule`() = + parseRuleData.map { (ruleData, parseRule) -> + DynamicTest.dynamicTest(parseRule.name) { + val yamlMap = Yaml.default.parseToYamlNode(ruleData.yamlInputAdvanced) + val expectedRule = ruleData.yamlInputAdvancedParsed(defaultMethodFqn) + + val actualRule = parseRule(yamlMap, defaultMethodNameParts) + assertEquals(expectedRule, actualRule) + } + } + + @TestFactory + fun `should fail on yaml map with unknown keys`() = parseRuleData.map { (ruleData, parseRule) -> + DynamicTest.dynamicTest(parseRule.name) { + val yamlMap = Yaml.default.parseToYamlNode(ruleData.yamlInputUnknownKey) + + assertThrows { + parseRule(yamlMap, defaultMethodNameParts) + } + } + } + } + + @Nested + @DisplayName("parseSources") + inner class ParseSourcesTest { + + private val parseRulesData = listOf( + SourcesData to TaintRuleParser::parseSources, + PassesData to TaintRuleParser::parsePasses, + CleanersData to TaintRuleParser::parseCleaners, + SinksData to TaintRuleParser::parseSinks + ) + + @TestFactory + fun `should parse yaml list with rules`() = parseRulesData.map { (rulesData, parseRules) -> + DynamicTest.dynamicTest(parseRules.name) { + val yamlMap = Yaml.default.parseToYamlNode(rulesData.yamlInput) + val expectedRules = rulesData.yamlInputParsed + + val actualRules = parseRules(yamlMap) + assertEquals(expectedRules, actualRules) + } + } + + @TestFactory + fun `should parse yaml list with rules specified using nested names`() = + parseRulesData.map { (rulesData, parseRules) -> + DynamicTest.dynamicTest(parseRules.name) { + val yamlMap = Yaml.default.parseToYamlNode(rulesData.yamlInputNestedNames) + val expectedRules = rulesData.yamlInputNestedNamesParsed + + val actualRules = parseRules(yamlMap) + assertEquals(expectedRules, actualRules) + } + } + + @TestFactory + fun `should fail on invalid yaml structure`() = parseRulesData.map { (rulesData, parseRules) -> + DynamicTest.dynamicTest(parseRules.name) { + val yamlMap = Yaml.default.parseToYamlNode(rulesData.yamlInputInvalid) + + assertThrows { + parseRules(yamlMap) + } + } + } + } + + // test data + + private val defaultMethodNameParts = listOf("org.example.Server.start") + private val defaultMethodFqn = YamlMethodFqn(listOf("org", "example"), "Server", "start") + + // one rule + + interface RuleData { + val yamlInput: String + val yamlInputAdvanced: String + val yamlInputInvalid: String + val yamlInputUnknownKey: String + + fun yamlInputParsed(methodFqn: YamlMethodFqn): Rule + fun yamlInputAdvancedParsed(methodFqn: YamlMethodFqn): Rule + } + + private object SourceData : RuleData { + override val yamlInput = "{ $k_addTo: $k_return, $k_marks: [] }" + override val yamlInputAdvanced = "{ $k_signature: [], $k_conditions: {}, $k_addTo: $k_return, $k_marks: [] }" + override val yamlInputInvalid = "{ invalid-key: $k_return, $k_marks: [] }" + override val yamlInputUnknownKey = "{ $k_addTo: $k_return, $k_marks: [], unknown-key: [] }" + + override fun yamlInputParsed(methodFqn: YamlMethodFqn) = + YamlTaintSource(methodFqn, YamlTaintEntitiesSet(setOf(YamlTaintEntityReturn)), YamlTaintMarksAll) + + override fun yamlInputAdvancedParsed(methodFqn: YamlMethodFqn) = + YamlTaintSource( + methodFqn, + YamlTaintEntitiesSet(setOf(YamlTaintEntityReturn)), + YamlTaintMarksAll, + YamlTaintSignatureList(listOf()), + YamlNoTaintConditions + ) + } + + private object PassData : RuleData { + override val yamlInput = "{ $k_getFrom: $k_this, $k_addTo: $k_return, $k_marks: [] }" + override val yamlInputAdvanced = + "{ $k_signature: [], $k_conditions: {}, $k_getFrom: $k_this, $k_addTo: $k_return, $k_marks: [] }" + override val yamlInputInvalid = "{ $k_getFrom: $k_this, $k_addTo: $k_return, invalid-key: [] }" + override val yamlInputUnknownKey = "{ $k_getFrom: $k_this, $k_addTo: $k_return, $k_marks: [], unknown-key: [] }" + + override fun yamlInputParsed(methodFqn: YamlMethodFqn) = + YamlTaintPass( + methodFqn, YamlTaintEntitiesSet(setOf(YamlTaintEntityThis)), YamlTaintEntitiesSet( + setOf( + YamlTaintEntityReturn + ) + ), YamlTaintMarksAll + ) + + override fun yamlInputAdvancedParsed(methodFqn: YamlMethodFqn) = + YamlTaintPass( + methodFqn, YamlTaintEntitiesSet(setOf(YamlTaintEntityThis)), YamlTaintEntitiesSet( + setOf( + YamlTaintEntityReturn + ) + ), YamlTaintMarksAll, YamlTaintSignatureList(listOf()), YamlNoTaintConditions + ) + } + + private object CleanerData : RuleData { + override val yamlInput = "{ $k_removeFrom: $k_this, $k_marks: [] }" + override val yamlInputAdvanced = "{ $k_signature: [], $k_conditions: {}, $k_removeFrom: $k_this, $k_marks: [] }" + override val yamlInputInvalid = "{ $k_removeFrom: $k_this, invalid-key: [] }" + override val yamlInputUnknownKey = "{ $k_removeFrom: $k_this, $k_marks: [], unknown-key: [] }" + + override fun yamlInputParsed(methodFqn: YamlMethodFqn) = + YamlTaintCleaner(methodFqn, YamlTaintEntitiesSet(setOf(YamlTaintEntityThis)), YamlTaintMarksAll) + + override fun yamlInputAdvancedParsed(methodFqn: YamlMethodFqn) = + YamlTaintCleaner( + methodFqn, + YamlTaintEntitiesSet(setOf(YamlTaintEntityThis)), + YamlTaintMarksAll, + YamlTaintSignatureList(listOf()), + YamlNoTaintConditions + ) + } + + private object SinkData : RuleData { + override val yamlInput = "{ $k_check: $k_this, $k_marks: [] }" + override val yamlInputAdvanced = "{ $k_signature: [], $k_conditions: {}, $k_check: $k_this, $k_marks: [] }" + override val yamlInputInvalid = "{ $k_check: $k_this, invalid-key: [] }" + override val yamlInputUnknownKey = "{ $k_check: $k_this, $k_marks: [], unknown-key: [] }" + + override fun yamlInputParsed(methodFqn: YamlMethodFqn) = + YamlTaintSink(methodFqn, YamlTaintEntitiesSet(setOf(YamlTaintEntityThis)), YamlTaintMarksAll) + + override fun yamlInputAdvancedParsed(methodFqn: YamlMethodFqn) = + YamlTaintSink( + methodFqn, + YamlTaintEntitiesSet(setOf(YamlTaintEntityThis)), + YamlTaintMarksAll, + YamlTaintSignatureList(listOf()), + YamlNoTaintConditions + ) + } + + // combined rules + + private object SourcesData : RulesData(SourceData) + private object PassesData : RulesData(PassData) + private object CleanersData : RulesData(CleanerData) + private object SinksData : RulesData(SinkData) + + private open class RulesData(ruleData: RuleData) { + val yamlInput = """ + - a.b.c.m: ${ruleData.yamlInput} + - d.e.f.m: ${ruleData.yamlInputAdvanced} + """.trimIndent() + + val yamlInputNestedNames = """ + - a: + - b.c: + - m: ${ruleData.yamlInput} + - m: ${ruleData.yamlInputAdvanced} + - d: + - e.m1: ${ruleData.yamlInput} + - e.m2: ${ruleData.yamlInputAdvanced} + """.trimIndent() + + val yamlInputInvalid = """ + - a: + - b.c: + m: ${ruleData.yamlInput} + """.trimIndent() + + val yamlInputParsed = listOf( + ruleData.yamlInputParsed(YamlMethodFqn(listOf("a", "b"), "c", "m")), + ruleData.yamlInputAdvancedParsed(YamlMethodFqn(listOf("d", "e"), "f", "m")) + ) + + val yamlInputNestedNamesParsed = listOf( + ruleData.yamlInputParsed(YamlMethodFqn(listOf("a", "b"), "c", "m")), + ruleData.yamlInputAdvancedParsed(YamlMethodFqn(listOf("a", "b"), "c", "m")), + ruleData.yamlInputParsed(YamlMethodFqn(listOf("a", "d"), "e", "m1")), + ruleData.yamlInputAdvancedParsed(YamlMethodFqn(listOf("a", "d"), "e", "m2")) + ) + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintSignatureParserTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintSignatureParserTest.kt new file mode 100644 index 0000000000..a1b0513ea2 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintSignatureParserTest.kt @@ -0,0 +1,81 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.Yaml +import com.charleskorn.kaml.yamlMap +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 + +class TaintSignatureParserTest { + + @Nested + @DisplayName("parseSignatureKey") + inner class ParseSignatureKeyTest { + @Test + fun `should parse yaml list of the argument types`() { + val yamlList = Yaml.default.parseToYamlNode("[ ${k_lt}int$k_gt, $k__, ${k_lt}java.lang.String$k_gt ]") + val expectedSignature = YamlTaintSignatureList( + listOf( + YamlArgumentTypeString("int"), + YamlArgumentTypeAny, + YamlArgumentTypeString("java.lang.String") + ) + ) + + val actualSignature = TaintSignatureParser.parseSignature(yamlList) + assertEquals(expectedSignature, actualSignature) + } + + @Test + fun `should parse empty yaml list`() { + val yamlList = Yaml.default.parseToYamlNode("[]") + val expectedSignature = YamlTaintSignatureList(listOf()) + + val actualSignature = TaintSignatureParser.parseSignature(yamlList) + assertEquals(expectedSignature, actualSignature) + } + + @Test + fun `should fail on incorrect signature`() { + val yamlList = Yaml.default.parseToYamlNode("[ 0, $k__, 2 ]") + + assertThrows { + TaintSignatureParser.parseSignature(yamlList) + } + } + + @Test + fun `should fail on another yaml type`() { + val yamlMap = Yaml.default.parseToYamlNode("$k_signature: []") + + assertThrows { + TaintSignatureParser.parseSignature(yamlMap) + } + } + } + + @Nested + @DisplayName("parseSignature") + inner class ParseSignatureTest { + @Test + fun `should parse yaml map with a key 'signature'`() { + val yamlList = Yaml.default.parseToYamlNode("$k_signature: [ $k__, $k__, ${k_lt}int$k_gt ]").yamlMap + val expectedSignature = + YamlTaintSignatureList(listOf(YamlArgumentTypeAny, YamlArgumentTypeAny, YamlArgumentTypeString("int"))) + + val actualSignature = TaintSignatureParser.parseSignatureKey(yamlList) + assertEquals(expectedSignature, actualSignature) + } + + @Test + fun `should parse yaml map without a key 'signature' as AnySignature`() { + val yamlMap = Yaml.default.parseToYamlNode("$k_marks: []").yamlMap + val expectedSignature = YamlTaintSignatureAny + + val actualSignature = TaintSignatureParser.parseSignatureKey(yamlMap) + assertEquals(expectedSignature, actualSignature) + } + } +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintYamlParserTest.kt b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintYamlParserTest.kt new file mode 100644 index 0000000000..b49d3c8b51 --- /dev/null +++ b/utbot-framework-test/src/test/kotlin/org/utbot/taint/parser/yaml/TaintYamlParserTest.kt @@ -0,0 +1,148 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.YamlException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class TaintYamlParserTest { + + @Test + fun `parse should parse correct yaml`() { + val actualConfiguration = TaintYamlParser.parse(yamlInput) + assertEquals(expectedConfiguration, actualConfiguration) + } + + @Test + fun `parse should throw exception on malformed yaml`() { + val malformedYamlInput = yamlInput.replace("{", "") + assertThrows { + TaintYamlParser.parse(malformedYamlInput) + } + } + + @Test + fun `parse should throw exception on incorrect yaml`() { + val incorrectYamlInput = yamlInput.replace(k_not, "net") + assertThrows { + TaintYamlParser.parse(incorrectYamlInput) + } + } + + // test data + + private val yamlInput = """ + $k_sources: + - java.lang.System.getenv: + $k_signature: [ ${k_lt}java.lang.String$k_gt ] + $k_addTo: $k_return + $k_marks: environment + + $k_passes: + - java.lang.String: + - concat: + $k_conditions: + $k_this: { $k_not: "" } + $k_getFrom: $k_this + $k_addTo: $k_return + $k_marks: sensitive-data + - concat: + $k_conditions: + ${k_arg}1: { $k_not: "" } + $k_getFrom: ${k_arg}1 + $k_addTo: $k_return + $k_marks: sensitive-data + + $k_cleaners: + - java.lang.String.isEmpty: + $k_conditions: + $k_return: true + $k_removeFrom: $k_this + $k_marks: [ sql-injection, xss ] + + $k_sinks: + - org.example.util.unsafe: + $k_signature: [ $k__, ${k_lt}java.lang.Integer$k_gt ] + $k_conditions: + ${k_arg}2: 0 + $k_check: ${k_arg}2 + $k_marks: environment + """.trimIndent() + + private val expectedConfiguration = YamlTaintConfiguration( + sources = listOf( + YamlTaintSource( + methodFqn = YamlMethodFqn(listOf("java", "lang"), "System", "getenv"), + addTo = YamlTaintEntitiesSet(setOf(YamlTaintEntityReturn)), + marks = YamlTaintMarksSet(setOf(YamlTaintMark("environment"))), + signature = YamlTaintSignatureList(listOf(YamlArgumentTypeString("java.lang.String"))), + conditions = YamlNoTaintConditions + ) + ), + passes = listOf( + YamlTaintPass( + methodFqn = YamlMethodFqn(listOf("java", "lang"), "String", "concat"), + getFrom = YamlTaintEntitiesSet(setOf(YamlTaintEntityThis)), + addTo = YamlTaintEntitiesSet(setOf(YamlTaintEntityReturn)), + marks = YamlTaintMarksSet(setOf(YamlTaintMark("sensitive-data"))), + signature = YamlTaintSignatureAny, + conditions = YamlTaintConditionsMap( + mapOf( + YamlTaintEntityThis to YamlTaintConditionNot( + YamlTaintConditionEqualValue(YamlArgumentValueString("")) + ) + ) + ) + ), + YamlTaintPass( + methodFqn = YamlMethodFqn(listOf("java", "lang"), "String", "concat"), + getFrom = YamlTaintEntitiesSet(setOf(YamlTaintEntityArgument(1u))), + addTo = YamlTaintEntitiesSet(setOf(YamlTaintEntityReturn)), + marks = YamlTaintMarksSet(setOf(YamlTaintMark("sensitive-data"))), + signature = YamlTaintSignatureAny, + conditions = YamlTaintConditionsMap( + mapOf( + YamlTaintEntityArgument(1u) to YamlTaintConditionNot( + YamlTaintConditionEqualValue(YamlArgumentValueString("")) + ) + ) + ) + ) + ), + cleaners = listOf( + YamlTaintCleaner( + methodFqn = YamlMethodFqn(listOf("java", "lang"), "String", "isEmpty"), + removeFrom = YamlTaintEntitiesSet(setOf(YamlTaintEntityThis)), + marks = YamlTaintMarksSet(setOf(YamlTaintMark("sql-injection"), YamlTaintMark("xss"))), + signature = YamlTaintSignatureAny, + conditions = YamlTaintConditionsMap( + mapOf( + YamlTaintEntityReturn to YamlTaintConditionEqualValue( + YamlArgumentValueBoolean(true) + ) + ) + ) + ) + ), + sinks = listOf( + YamlTaintSink( + methodFqn = YamlMethodFqn(listOf("org", "example"), "util", "unsafe"), + check = YamlTaintEntitiesSet(setOf(YamlTaintEntityArgument(2u))), + marks = YamlTaintMarksSet(setOf(YamlTaintMark("environment"))), + signature = YamlTaintSignatureList( + argumentTypes = listOf( + YamlArgumentTypeAny, + YamlArgumentTypeString("java.lang.Integer") + ) + ), + conditions = YamlTaintConditionsMap( + mapOf( + YamlTaintEntityArgument(2u) to YamlTaintConditionEqualValue( + YamlArgumentValueLong(0L) + ) + ) + ) + ) + ) + ) +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/resources/log4j2.xml b/utbot-framework-test/src/test/resources/log4j2.xml index 11a2d0701c..ac3d2f2abf 100644 --- a/utbot-framework-test/src/test/resources/log4j2.xml +++ b/utbot-framework-test/src/test/resources/log4j2.xml @@ -7,12 +7,12 @@ filePattern="logs/framework-%d{MM-dd-yyyy}.log.gz" ignoreExceptions="false"> - + - + diff --git a/utbot-framework/build.gradle b/utbot-framework/build.gradle index c22f996332..c471f88cef 100644 --- a/utbot-framework/build.gradle +++ b/utbot-framework/build.gradle @@ -1,53 +1,56 @@ -repositories { - flatDir { - dirs 'dist' - } +plugins { + id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.20' } configurations { - z3native + fetchInstrumentationJar } dependencies { - api project(':utbot-fuzzers') + api project(':utbot-java-fuzzing') api project(':utbot-instrumentation') api project(':utbot-summary') api project(':utbot-framework-api') api project(':utbot-rd') + api project(':utbot-modificators-analyzer') - implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: '2022.3.1' - implementation group: 'com.jetbrains.rd', name: 'rd-core', version: '2022.3.1' + implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: rdVersion + implementation group: 'com.jetbrains.rd', name: 'rd-core', version: rdVersion - implementation "com.github.UnitTestBot:soot:${sootCommitHash}" + implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { + exclude group:'com.google.guava', module:'guava' + } + implementation group: 'com.google.guava', name: 'guava', version: guavaVersion implementation group: 'com.esotericsoftware.kryo', name: 'kryo5', version: kryoVersion // this is necessary for serialization of some collections implementation group: 'de.javakaffee', name: 'kryo-serializers', version: kryoSerializersVersion + implementation group: 'com.charleskorn.kaml', name: 'kaml', version: kamlVersion implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: jacksonVersion - implementation group: 'org.sosy-lab', name: 'javasmt-solver-z3', version: javasmtSolverZ3Version + implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-serialization-cbor', version: kotlinxSerializationVersion implementation group: 'com.github.curious-odd-man', name: 'rgxgen', version: rgxgenVersion implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: log4j2Version implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion implementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion implementation group: 'org.apache.commons', name: 'commons-text', version: apacheCommonsTextVersion // we need this for construction mocks from composite models - implementation group: 'org.mockito', name: 'mockito-core', version: '4.2.0' + implementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion // To use JUnit4, comment out JUnit5 and uncomment JUnit4 dependencies here. Please also check "test" section //implementation group: 'junit', name: 'junit', version: '4.13.1' implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.8.1' implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.1' - z3native group: 'com.microsoft.z3', name: 'z3-native-win64', version: z3Version, ext: 'zip' - z3native group: 'com.microsoft.z3', name: 'z3-native-linux64', version: z3Version, ext: 'zip' - z3native group: 'com.microsoft.z3', name: 'z3-native-osx', version: z3Version, ext: 'zip' + // TODO sbft-usvm-merge: UtBot engine expects `com.github.UnitTestBot.ksmt` here + implementation group: 'io.ksmt', name: 'ksmt-core', version: ksmtVersion + implementation group: 'io.ksmt', name: 'ksmt-z3', version: ksmtVersion + + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration: 'instrumentationArchive') } processResources { - configurations.z3native.resolvedConfiguration.resolvedArtifacts.each { artifact -> - from(zipTree(artifact.getFile())) { - into "lib/x64" - } + from(configurations.fetchInstrumentationJar) { + into "lib" } } diff --git a/utbot-framework/dist/z3-native-linux64-4.8.9.1.zip b/utbot-framework/dist/z3-native-linux64-4.8.9.1.zip deleted file mode 100644 index a5a3ac9e4a..0000000000 Binary files a/utbot-framework/dist/z3-native-linux64-4.8.9.1.zip and /dev/null differ 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 deleted file mode 100644 index 9b42561fc9..0000000000 Binary files a/utbot-framework/dist/z3-native-osx-4.8.9.1.zip and /dev/null differ diff --git a/utbot-framework/dist/z3-native-win64-4.8.9.1.zip b/utbot-framework/dist/z3-native-win64-4.8.9.1.zip deleted file mode 100644 index 2024be077e..0000000000 Binary files a/utbot-framework/dist/z3-native-win64-4.8.9.1.zip and /dev/null differ diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/AccessController.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/AccessController.java new file mode 100644 index 0000000000..f2cb72f38f --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/AccessController.java @@ -0,0 +1,103 @@ +package org.utbot.engine.overrides; + +import org.utbot.api.annotation.UtClassMock; + +import java.security.AccessControlContext; +import java.security.Permission; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +@UtClassMock(target = java.security.AccessController.class, internalUsage = true) +public class AccessController { + public static T doPrivileged(PrivilegedAction action) { + return action.run(); + } + + public static T doPrivilegedWithCombiner(PrivilegedAction action) { + return action.run(); + } + + public static T doPrivileged(PrivilegedAction action, + AccessControlContext ignoredContext) { + return action.run(); + } + + public static T doPrivileged(PrivilegedAction action, + AccessControlContext ignoredContext, Permission... perms) { + if (perms == null) { + throw new NullPointerException(); + } + + for (Permission permission : perms) { + if (permission == null) { + throw new NullPointerException(); + } + } + + return action.run(); + } + + public static T doPrivilegedWithCombiner(PrivilegedAction action, + AccessControlContext context, Permission... perms) { + return doPrivileged(action, context, perms); + } + + public static T doPrivileged(PrivilegedExceptionAction action) throws PrivilegedActionException { + try { + return action.run(); + } catch (RuntimeException e) { + // If the action's run method throws an unchecked exception, it will propagate through this method. + throw e; + } catch (Exception e) { + // Exception is wrapped with the PrivilegedActionException ONLY if this exception is CHECKED + throw new PrivilegedActionException(e); + } + } + + public static T doPrivilegedWithCombiner(PrivilegedExceptionAction action) throws PrivilegedActionException { + return doPrivileged(action); + } + + public static T doPrivileged( + PrivilegedExceptionAction action, + AccessControlContext ignoredContext + ) throws PrivilegedActionException { + return doPrivileged(action); + } + + public static T doPrivileged( + PrivilegedExceptionAction action, + AccessControlContext ignoredContext, + Permission... perms + ) throws PrivilegedActionException { + if (perms == null) { + throw new NullPointerException(); + } + + for (Permission permission : perms) { + if (permission == null) { + throw new NullPointerException(); + } + } + + return doPrivileged(action); + } + + public static T doPrivilegedWithCombiner( + PrivilegedExceptionAction action, + AccessControlContext context, + Permission... perms + ) throws PrivilegedActionException { + return doPrivileged(action, context, perms); + } + + public static void checkPermission(Permission perm) { + if (perm == null) { + throw new NullPointerException(); + } + + // We cannot check real permission do determines whether we should throw an AccessControlException, + // so do nothing more. + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/AutoCloseable.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/AutoCloseable.java new file mode 100644 index 0000000000..6ba7913699 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/AutoCloseable.java @@ -0,0 +1,10 @@ +package org.utbot.engine.overrides; + +import org.utbot.api.annotation.UtClassMock; + +@UtClassMock(target = java.lang.AutoCloseable.class, internalUsage = true) +public interface AutoCloseable { + default void close() throws Exception { + // Do nothing + } +} 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 1189d16e6f..717e2c4f2b 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 @@ -1,10 +1,10 @@ package org.utbot.engine.overrides; import org.utbot.api.annotation.UtClassMock; -import org.utbot.engine.overrides.strings.UtNativeString; -import org.utbot.engine.overrides.strings.UtString; import org.utbot.engine.overrides.strings.UtStringBuilder; +import java.util.Arrays; + import static org.utbot.api.mock.UtMock.assume; import static org.utbot.engine.overrides.UtLogicMock.ite; import static org.utbot.api.mock.UtMock.assumeOrExecuteConcretely; @@ -48,17 +48,39 @@ public static byte parseByte(String s, int radix) { @SuppressWarnings("ConstantConditions") public static String toString(byte b) { - // condition = b < 0 - boolean condition = less(b, (byte) 0); + if (b == -128) { + return "-128"; + } + + if (b == 0) { + return "0"; + } + // assumes are placed here to limit search space of solver // and reduce time of solving queries with bv2int expressions - assume(b < 128); - assume(b >= -128); - // prefix = condition ? "-" : "" - String prefix = ite(condition, "-", ""); - UtStringBuilder sb = new UtStringBuilder(prefix); - // value = condition ? -i : i - int value = ite(condition, (byte) -b, b); - return sb.append(new UtString(new UtNativeString(value)).toStringImpl()).toString(); + assume(b <= 127); + assume(b > -128); + assume(b != 0); + + // isNegative = b < 0 + boolean isNegative = less(b, (byte) 0); + String prefix = ite(isNegative, "-", ""); + + int value = ite(isNegative, (byte) -b, b); + char[] reversed = new char[3]; + int offset = 0; + while (value > 0) { + reversed[offset] = (char) ('0' + (value % 10)); + value /= 10; + offset++; + } + + char[] buffer = new char[offset]; + int counter = 0; + while (offset > 0) { + offset--; + buffer[counter++] = reversed[offset]; + } + return prefix + new String(buffer); } } 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 32e2e48e28..34d936364c 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 @@ -1,10 +1,9 @@ package org.utbot.engine.overrides; import org.utbot.api.annotation.UtClassMock; -import org.utbot.engine.overrides.strings.UtNativeString; -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; @@ -71,21 +70,35 @@ public static int parseUnsignedInt(String s, int radix) { } } + @SuppressWarnings("ConstantConditions") public static String toString(int i) { - if (i == 0x80000000) { // java.lang.MIN_VALUE + if (i == 0x80000000) { // java.lang.Integer.MIN_VALUE return "-2147483648"; } - // assumes are placed here to limit search space of solver - // and reduce time of solving queries with bv2int expressions - assumeOrExecuteConcretely(i <= 0x8000); - assumeOrExecuteConcretely(i >= -0x8000); - // condition = i < 0 - boolean condition = less(i, 0); - // prefix = condition ? "-" : "" - String prefix = ite(condition, "-", ""); - UtStringBuilder sb = new UtStringBuilder(prefix); - // value = condition ? -i : i - int value = ite(condition, -i, i); - return sb.append(new UtString(new UtNativeString(value)).toStringImpl()).toString(); + + if (i == 0) { + return "0"; + } + + // isNegative = i < 0 + boolean isNegative = less(i, 0); + String prefix = ite(isNegative, "-", ""); + + int value = ite(isNegative, -i, i); + char[] reversed = new char[10]; + int offset = 0; + while (value > 0) { + reversed[offset] = (char) ('0' + (value % 10)); + value /= 10; + offset++; + } + + char[] buffer = new char[offset]; + int counter = 0; + while (offset > 0) { + offset--; + buffer[counter++] = reversed[offset]; + } + return prefix + new String(buffer); } } 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 22584bede5..2128115279 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 @@ -1,10 +1,9 @@ package org.utbot.engine.overrides; import org.utbot.api.annotation.UtClassMock; -import org.utbot.engine.overrides.strings.UtNativeString; -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; @@ -71,21 +70,35 @@ public static long parseUnsignedLong(String s, int radix) { } } + @SuppressWarnings("ConstantConditions") public static String toString(long l) { if (l == 0x8000000000000000L) { // java.lang.Long.MIN_VALUE return "-9223372036854775808"; } - // assumes are placed here to limit search space of solver - // and reduce time of solving queries with bv2int expressions - assumeOrExecuteConcretely(l <= 10000); - assumeOrExecuteConcretely(l >= -10000); - // condition = l < 0 - boolean condition = less(l, 0); - // prefix = condition ? "-" : "" - String prefix = ite(condition, "-", ""); - UtStringBuilder sb = new UtStringBuilder(prefix); - // value = condition ? -l : l - long value = ite(condition, -l, l); - return sb.append(new UtString(new UtNativeString(value)).toStringImpl()).toString(); + + if (l == 0) { + return "0"; + } + + // isNegative = i < 0 + boolean isNegative = less(l, 0); + String prefix = ite(isNegative, "-", ""); + + long value = ite(isNegative, -l, l); + char[] reversed = new char[19]; + int offset = 0; + while (value > 0) { + reversed[offset] = (char) ('0' + (value % 10)); + value /= 10; + offset++; + } + + char[] buffer = new char[offset]; + int counter = 0; + while (offset > 0) { + offset--; + buffer[counter++] = reversed[offset]; + } + return prefix + new String(buffer); } } 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 529b4a3829..d64533bb78 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 @@ -1,10 +1,9 @@ package org.utbot.engine.overrides; import org.utbot.api.annotation.UtClassMock; -import org.utbot.engine.overrides.strings.UtNativeString; -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; @@ -45,18 +44,41 @@ public static java.lang.Short parseShort(String s, int radix) { } } + @SuppressWarnings("ConstantConditions") public static String toString(short s) { - // condition = s < 0 - boolean condition = less(s, (short) 0); + if (s == -32768) { + return "-32768"; + } + + if (s == 0) { + return "0"; + } + // assumes are placed here to limit search space of solver // and reduce time of solving queries with bv2int expressions - assumeOrExecuteConcretely(s <= 10000); - assumeOrExecuteConcretely(s >= -10000); - // prefix = condition ? "-" : "" - String prefix = ite(condition, "-", ""); - UtStringBuilder sb = new UtStringBuilder(prefix); - // value = condition ? -i : i - int value = ite(condition, (short)-s, s); - return sb.append(new UtString(new UtNativeString(value)).toStringImpl()).toString(); + assume(s <= 32767); + assume(s > -32768); + assume(s != 0); + + // isNegative = s < 0 + boolean isNegative = less(s, (short) 0); + String prefix = ite(isNegative, "-", ""); + + int value = ite(isNegative, (short) -s, s); + char[] reversed = new char[5]; + int offset = 0; + while (value > 0) { + reversed[offset] = (char) ('0' + (value % 10)); + value /= 10; + offset++; + } + + char[] buffer = new char[offset]; + int counter = 0; + while (offset > 0) { + offset--; + buffer[counter++] = reversed[offset]; + } + return prefix + new String(buffer); } } diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/System.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/System.java index fb37ceed9e..f34ddd851b 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/System.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/System.java @@ -210,7 +210,7 @@ public static void arraycopy(Object src, int srcPos, Object dest, int destPos, i } UtArrayMock.arraycopy(srcArray, srcPos, destArray, destPos, length); - } else { + } else if (src instanceof Object[]) { if (!(dest instanceof Object[])) { throw new ArrayStoreException(); } @@ -223,6 +223,9 @@ public static void arraycopy(Object src, int srcPos, Object dest, int destPos, i } UtArrayMock.arraycopy(srcArray, srcPos, destArray, destPos, length); + } else { + // From docs: if the src argument refers to an object that is not an array, an ArrayStoreException will be thrown + throw new ArrayStoreException(); } } } diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/Throwable.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/Throwable.java new file mode 100644 index 0000000000..9c51eade7f --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/Throwable.java @@ -0,0 +1,31 @@ +package org.utbot.engine.overrides; + +import org.utbot.api.annotation.UtClassMock; +import org.utbot.api.mock.UtMock; + +@UtClassMock(target = java.lang.Throwable.class, internalUsage = true) +public class Throwable { + public void printStackTrace() { + // Do nothing + } + + public synchronized java.lang.Throwable fillInStackTrace() { + return UtMock.makeSymbolic(); + } + + public StackTraceElement[] getStackTrace() { + return UtMock.makeSymbolic(); + } + + public void setStackTrace(StackTraceElement[] stackTrace) { + // Do nothing + } + + public final synchronized void addSuppressed(java.lang.Throwable exception) { + // Do nothing + } + + public final synchronized java.lang.Throwable[] getSuppressed() { + return UtMock.makeSymbolic(); + } +} 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 73ce70af93..818526ab4d 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 @@ -322,7 +322,7 @@ public int lastIndexOf(Object o) { @Override public Iterator iterator() { preconditionCheck(); - return new UtArrayListIterator(0); + return new UtArrayListSimpleIterator(0); } @NotNull @@ -405,6 +405,43 @@ public List subList(int fromIndex, int toIndex) { return this.toList().subList(fromIndex, toIndex); } + public class UtArrayListSimpleIterator implements Iterator { + int index; + int prevIndex = -1; + + UtArrayListSimpleIterator(int index) { + rangeCheckForAdd(index); + this.index = index; + } + + @Override + public boolean hasNext() { + preconditionCheck(); + return index != elementData.end; + } + + @Override + public E next() { + preconditionCheck(); + if (index == elementData.end) { + throw new NoSuchElementException(); + } + prevIndex = index; + return elementData.get(index++); + } + + @Override + public void remove() { + preconditionCheck(); + if (prevIndex == -1) { + throw new IllegalStateException(); + } + elementData.end--; + elementData.remove(prevIndex); + prevIndex = -1; + } + } + public class UtArrayListIterator implements ListIterator { int index; int prevIndex = -1; 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 f3a9b22405..97efe821d9 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 @@ -102,6 +102,12 @@ void preconditionCheck() { parameter(keys); parameter(keys.storage); + // Following three instructions are required to avoid possible aliasing + // between nested arrays + assume(keys.storage != values.storage); + assume(keys.storage != values.touched); + assume(values.storage != values.touched); + assume(values.size == keys.end); assume(values.touched.length == keys.end); doesntThrow(); @@ -205,11 +211,18 @@ public V put(K key, V value) { if (index == -1) { oldValue = null; keys.set(keys.end++, key); + values.store(key, value); } else { - // newKey equals to oldKey so we can use it instead - oldValue = values.select(key); + K oldKey = keys.get(index); + oldValue = values.select(oldKey); + values.store(oldKey, value); } - values.store(key, value); + + // Avoid optimization here. Despite the fact that old key is equal + // to a new one in case of `index != -1`, it is important to have + // this connection between `index`, `key`, `oldKey` and `value`. + // So, do not rewrite it with `values.store(key, value)` extracted from the if instruction + return oldValue; } 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 577c08aa11..986b043262 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 @@ -435,6 +435,8 @@ public List subList(int fromIndex, int toIndex) { @Override public Iterator iterator() { preconditionCheck(); + + // Some implementations of `iterator` return an instance of ListIterator return new UtLinkedListIterator(elementData.begin); } @@ -449,7 +451,7 @@ public ListIterator listIterator() { @Override public Iterator descendingIterator() { preconditionCheck(); - return new ReverseIteratorWrapper(elementData.end); + return new UtReverseIterator(elementData.end); } @Override @@ -467,12 +469,12 @@ public Stream parallelStream() { return stream(); } - public class ReverseIteratorWrapper implements ListIterator { + public class UtReverseIterator implements ListIterator { int index; int prevIndex = -1; - ReverseIteratorWrapper(int index) { + UtReverseIterator(int index) { this.index = index; } diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtOptional.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtOptional.java index 35d8106070..7fca5dbf49 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtOptional.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtOptional.java @@ -28,7 +28,8 @@ public UtOptional() { } public UtOptional(T value) { - Objects.requireNonNull(value); + // In different Java versions Optional.EMPTY is created differently - using the empty constructor in Java 8 and 11, + // and with this constructor in Java 17. It means we cannot use `requireNonNull` here because it fails for Java 17. visit(this); this.value = value; } diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/security/UtSecurityManager.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/security/UtSecurityManager.java index 67c635945a..c703789a7c 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/security/UtSecurityManager.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/security/UtSecurityManager.java @@ -1,5 +1,7 @@ package org.utbot.engine.overrides.security; +import org.utbot.api.mock.UtMock; + import java.security.Permission; /** @@ -13,4 +15,8 @@ public void checkPermission(Permission perm) { public void checkPackageAccess(String pkg) { // Do nothing to allow everything } + + public ThreadGroup getThreadGroup() { + return new ThreadGroup(UtMock.makeSymbolic()); + } } 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 index 0935ad1925..90566e068c 100644 --- 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 @@ -21,7 +21,7 @@ public static Stream stream(T[] array, int startInclusive, int endExclusi return new UtStream<>(array, startInclusive, endExclusive); } - // from docs - array is assumed to be umnodified during use + // from docs - array is assumed to be unmodified during use public static IntStream stream(int[] array, int startInclusive, int endExclusive) { int size = array.length; @@ -37,7 +37,7 @@ public static IntStream stream(int[] array, int startInclusive, int endExclusive return new UtIntStream(data, startInclusive, endExclusive); } - // from docs - array is assumed to be umnodified during use + // from docs - array is assumed to be unmodified during use public static LongStream stream(long[] array, int startInclusive, int endExclusive) { int size = array.length; @@ -53,7 +53,7 @@ public static LongStream stream(long[] array, int startInclusive, int endExclusi return new UtLongStream(data, startInclusive, endExclusive); } - // from docs - array is assumed to be umnodified during use + // from docs - array is assumed to be unmodified during use public static DoubleStream stream(double[] array, int startInclusive, int endExclusive) { int size = array.length; @@ -75,6 +75,4 @@ 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/UtDoubleStream.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtDoubleStream.java index d1e9768c2f..82c081011b 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtDoubleStream.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtDoubleStream.java @@ -1,8 +1,10 @@ package org.utbot.engine.overrides.stream; import org.jetbrains.annotations.NotNull; +import org.utbot.engine.ResolverKt; import org.utbot.engine.overrides.collections.RangeModifiableUnlimitedArray; import org.utbot.engine.overrides.collections.UtGenericStorage; +import org.utbot.framework.plugin.api.visible.UtStreamConsumingException; import java.util.DoubleSummaryStatistics; import java.util.NoSuchElementException; @@ -26,7 +28,6 @@ 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; @@ -69,7 +70,7 @@ public UtDoubleStream(Double[] data, int startInclusive, int endExclusive) { * 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.size in 0..MAX_STREAM_SIZE.
  • *
  • elementData is marked as parameter
  • *
  • elementData.storage and it's elements are marked as parameters
  • */ @@ -90,7 +91,7 @@ void preconditionCheck() { assume(elementData.end >= 0); // we can create a stream for an array using Stream.of - assumeOrExecuteConcretely(elementData.end <= HARD_MAX_ARRAY_SIZE); + assumeOrExecuteConcretely(elementData.end <= ResolverKt.MAX_STREAM_SIZE); // As real primitive streams contain primitives, we cannot accept nulls. for (int i = 0; i < elementData.end; i++) { @@ -122,8 +123,13 @@ public DoubleStream filter(DoublePredicate predicate) { int j = 0; for (int i = 0; i < size; i++) { double element = elementData.get(i); - if (predicate.test(element)) { - filtered[j++] = element; + + try { + if (predicate.test(element)) { + filtered[j++] = element; + } + } catch (Exception e) { + throw new UtStreamConsumingException(e); } } @@ -137,7 +143,11 @@ public DoubleStream map(DoubleUnaryOperator mapper) { int size = elementData.end; Double[] mapped = new Double[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.applyAsDouble(elementData.get(i)); + try { + mapped[i] = mapper.applyAsDouble(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtDoubleStream(mapped, size); @@ -153,7 +163,11 @@ public Stream mapToObj(DoubleFunction mapper) { int size = elementData.end; Object[] mapped = new Object[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.apply(elementData.get(i)); + try { + mapped[i] = mapper.apply(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtStream<>((U[]) mapped, size); @@ -166,7 +180,11 @@ public IntStream mapToInt(DoubleToIntFunction mapper) { int size = elementData.end; Integer[] mapped = new Integer[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.applyAsInt(elementData.get(i)); + try { + mapped[i] = mapper.applyAsInt(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtIntStream(mapped, size); @@ -179,7 +197,11 @@ public LongStream mapToLong(DoubleToLongFunction mapper) { int size = elementData.end; Long[] mapped = new Long[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.applyAsLong(elementData.get(i)); + try { + mapped[i] = mapper.applyAsLong(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtLongStream(mapped, size); @@ -256,7 +278,11 @@ public DoubleStream peek(DoubleConsumer action) { int size = elementData.end; for (int i = 0; i < size; i++) { - action.accept(elementData.get(i)); + try { + action.accept(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } // returned stream should be opened, so we "reopen" this stream to return it @@ -315,13 +341,16 @@ public DoubleStream skip(long n) { @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void forEach(DoubleConsumer action) { - peek(action); + try { + peek(action); + } catch (UtStreamConsumingException e) { + // Since this is a terminal operation, we should throw an original exception + } } - @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void forEachOrdered(DoubleConsumer action) { - peek(action); + forEach(action); } @Override diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtIntStream.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtIntStream.java index 322859d6b4..0d1d530bbc 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtIntStream.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtIntStream.java @@ -1,8 +1,10 @@ package org.utbot.engine.overrides.stream; import org.jetbrains.annotations.NotNull; +import org.utbot.engine.ResolverKt; import org.utbot.engine.overrides.collections.RangeModifiableUnlimitedArray; import org.utbot.engine.overrides.collections.UtGenericStorage; +import org.utbot.framework.plugin.api.visible.UtStreamConsumingException; import java.util.IntSummaryStatistics; import java.util.NoSuchElementException; @@ -27,7 +29,6 @@ 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; @@ -91,7 +92,7 @@ void preconditionCheck() { assume(elementData.end >= 0); // we can create a stream for an array using Stream.of - assumeOrExecuteConcretely(elementData.end <= HARD_MAX_ARRAY_SIZE); + assumeOrExecuteConcretely(elementData.end <= ResolverKt.MAX_STREAM_SIZE); // As real primitive streams contain primitives, we cannot accept nulls. for (int i = 0; i < elementData.end; i++) { @@ -123,8 +124,13 @@ public IntStream filter(IntPredicate predicate) { int j = 0; for (int i = 0; i < size; i++) { int element = elementData.get(i); - if (predicate.test(element)) { - filtered[j++] = element; + + try { + if (predicate.test(element)) { + filtered[j++] = element; + } + } catch (Exception e) { + throw new UtStreamConsumingException(e); } } @@ -138,7 +144,11 @@ public IntStream map(IntUnaryOperator mapper) { int size = elementData.end; Integer[] mapped = new Integer[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.applyAsInt(elementData.get(i)); + try { + mapped[i] = mapper.applyAsInt(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtIntStream(mapped, size); @@ -154,7 +164,11 @@ public Stream mapToObj(IntFunction mapper) { int size = elementData.end; U[] mapped = (U[]) new Object[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.apply(elementData.get(i)); + try { + mapped[i] = mapper.apply(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtStream<>(mapped, size); @@ -167,7 +181,11 @@ public LongStream mapToLong(IntToLongFunction mapper) { int size = elementData.end; Long[] mapped = new Long[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.applyAsLong(elementData.get(i)); + try { + mapped[i] = mapper.applyAsLong(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtLongStream(mapped, size); @@ -180,7 +198,11 @@ public DoubleStream mapToDouble(IntToDoubleFunction mapper) { int size = elementData.end; Double[] mapped = new Double[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.applyAsDouble(elementData.get(i)); + try { + mapped[i] = mapper.applyAsDouble(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtDoubleStream(mapped, size); @@ -257,7 +279,11 @@ public IntStream peek(IntConsumer action) { int size = elementData.end; for (int i = 0; i < size; i++) { - action.accept(elementData.get(i)); + try { + action.accept(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } // returned stream should be opened, so we "reopen" this stream to return it @@ -316,13 +342,16 @@ public IntStream skip(long n) { @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void forEach(IntConsumer action) { - peek(action); + try { + peek(action); + } catch (UtStreamConsumingException e) { + // Since this is a terminal operation, we should throw an original exception + } } - @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void forEachOrdered(IntConsumer action) { - peek(action); + forEach(action); } @Override diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtLongStream.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtLongStream.java index a7fb04e385..74c4465a64 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtLongStream.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtLongStream.java @@ -1,8 +1,10 @@ package org.utbot.engine.overrides.stream; import org.jetbrains.annotations.NotNull; +import org.utbot.engine.ResolverKt; import org.utbot.engine.overrides.collections.RangeModifiableUnlimitedArray; import org.utbot.engine.overrides.collections.UtGenericStorage; +import org.utbot.framework.plugin.api.visible.UtStreamConsumingException; import java.util.LongSummaryStatistics; import java.util.NoSuchElementException; @@ -27,7 +29,6 @@ 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; @@ -91,7 +92,7 @@ void preconditionCheck() { assume(elementData.end >= 0); // we can create a stream for an array using Stream.of - assumeOrExecuteConcretely(elementData.end <= HARD_MAX_ARRAY_SIZE); + assumeOrExecuteConcretely(elementData.end <= ResolverKt.MAX_STREAM_SIZE); // As real primitive streams contain primitives, we cannot accept nulls. for (int i = 0; i < elementData.end; i++) { @@ -123,8 +124,13 @@ public LongStream filter(LongPredicate predicate) { int j = 0; for (int i = 0; i < size; i++) { long element = elementData.get(i); - if (predicate.test(element)) { - filtered[j++] = element; + + try { + if (predicate.test(element)) { + filtered[j++] = element; + } + } catch (Exception e) { + throw new UtStreamConsumingException(e); } } @@ -138,7 +144,11 @@ public LongStream map(LongUnaryOperator mapper) { int size = elementData.end; Long[] mapped = new Long[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.applyAsLong(elementData.get(i)); + try { + mapped[i] = mapper.applyAsLong(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtLongStream(mapped, size); @@ -154,7 +164,11 @@ public Stream mapToObj(LongFunction mapper) { int size = elementData.end; Object[] mapped = new Object[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.apply(elementData.get(i)); + try { + mapped[i] = mapper.apply(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtStream<>((U[]) mapped, size); @@ -167,7 +181,11 @@ public IntStream mapToInt(LongToIntFunction mapper) { int size = elementData.end; Integer[] mapped = new Integer[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.applyAsInt(elementData.get(i)); + try { + mapped[i] = mapper.applyAsInt(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtIntStream(mapped, size); @@ -180,7 +198,11 @@ public DoubleStream mapToDouble(LongToDoubleFunction mapper) { int size = elementData.end; Double[] mapped = new Double[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.applyAsDouble(elementData.get(i)); + try { + mapped[i] = mapper.applyAsDouble(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtDoubleStream(mapped, size); @@ -257,7 +279,11 @@ public LongStream peek(LongConsumer action) { int size = elementData.end; for (int i = 0; i < size; i++) { - action.accept(elementData.get(i)); + try { + action.accept(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } // returned stream should be opened, so we "reopen" this stream to return it @@ -316,13 +342,16 @@ public LongStream skip(long n) { @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void forEach(LongConsumer action) { - peek(action); + try { + peek(action); + } catch (UtStreamConsumingException e) { + // Since this is a terminal operation, we should throw an original exception + } } - @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void forEachOrdered(LongConsumer action) { - peek(action); + forEach(action); } @Override 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 index 132635a43e..37413f0725 100644 --- 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 @@ -1,9 +1,11 @@ package org.utbot.engine.overrides.stream; import org.jetbrains.annotations.NotNull; +import org.utbot.engine.ResolverKt; import org.utbot.engine.overrides.UtArrayMock; import org.utbot.engine.overrides.collections.RangeModifiableUnlimitedArray; import org.utbot.engine.overrides.collections.UtGenericStorage; +import org.utbot.framework.plugin.api.visible.UtStreamConsumingException; import java.util.Comparator; import java.util.Iterator; @@ -30,7 +32,6 @@ 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; @@ -94,7 +95,7 @@ void preconditionCheck() { assume(elementData.end >= 0); // we can create a stream for an array using Stream.of - assume(elementData.end <= HARD_MAX_ARRAY_SIZE); + assume(elementData.end <= ResolverKt.MAX_STREAM_SIZE); visit(this); } @@ -120,8 +121,13 @@ public Stream filter(Predicate predicate) { int j = 0; for (int i = 0; i < size; i++) { E element = elementData.get(i); - if (predicate.test(element)) { - filtered[j++] = element; + + try { + if (predicate.test(element)) { + filtered[j++] = element; + } + } catch (Exception e) { + throw new UtStreamConsumingException(e); } } @@ -136,7 +142,11 @@ public Stream map(Function mapper) { int size = elementData.end; Object[] mapped = new Object[size]; for (int i = 0; i < size; i++) { - mapped[i] = mapper.apply(elementData.get(i)); + try { + mapped[i] = mapper.apply(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtStream<>((R[]) mapped, size); @@ -149,7 +159,11 @@ public IntStream mapToInt(ToIntFunction mapper) { int size = elementData.end; Integer[] data = new Integer[size]; for (int i = 0; i < size; i++) { - data[i] = mapper.applyAsInt(elementData.getWithoutClassCastExceptionCheck(i)); + try { + data[i] = mapper.applyAsInt(elementData.getWithoutClassCastExceptionCheck(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtIntStream(data, size); @@ -162,7 +176,11 @@ public LongStream mapToLong(ToLongFunction mapper) { int size = elementData.end; Long[] data = new Long[size]; for (int i = 0; i < size; i++) { - data[i] = mapper.applyAsLong(elementData.getWithoutClassCastExceptionCheck(i)); + try { + data[i] = mapper.applyAsLong(elementData.getWithoutClassCastExceptionCheck(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtLongStream(data, size); @@ -175,7 +193,11 @@ public DoubleStream mapToDouble(ToDoubleFunction mapper) { int size = elementData.end; Double[] data = new Double[size]; for (int i = 0; i < size; i++) { - data[i] = mapper.applyAsDouble(elementData.getWithoutClassCastExceptionCheck(i)); + try { + data[i] = mapper.applyAsDouble(elementData.getWithoutClassCastExceptionCheck(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } return new UtDoubleStream(data, size); @@ -319,7 +341,11 @@ public Stream peek(Consumer action) { int size = elementData.end; for (int i = 0; i < size; i++) { - action.accept(elementData.get(i)); + try { + action.accept(elementData.get(i)); + } catch (Exception e) { + throw new UtStreamConsumingException(e); + } } // returned stream should be opened, so we "reopen" this stream to return it @@ -389,12 +415,16 @@ public Stream skip(long n) { @Override public void forEach(Consumer action) { - peek(action); + try { + peek(action); + } catch (UtStreamConsumingException e) { + // Since this is a terminal operation, we should throw an original exception + } } @Override public void forEachOrdered(Consumer action) { - peek(action); + forEach(action); } @NotNull diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtNativeString.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtNativeString.java deleted file mode 100644 index 5d75c6dd05..0000000000 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtNativeString.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.utbot.engine.overrides.strings; - -/** - * An auxiliary class without implementation, - * that specifies interface of interaction with - * smt string objects. - * @see org.utbot.engine.UtNativeStringWrapper - */ -@SuppressWarnings("unused") -public class UtNativeString { - /** - * Constructor, that creates a new symbolic string. - * Length and content can be arbitrary and is set - * via path constraints. - */ - public UtNativeString() {} - - /** - * Constructor, that creates a new string - * that is equal smt expression: int2string(i)> - * - * if i is greater or equal to 0, than - * constructed string will be equal to - * i.toString()>, otherwise, it is undefined - * @param i number, that needs to be converted to string - */ - public UtNativeString(int i) { } - - /** - * Constructor, that creates a new string - * that is equal smt expression: int2string(l) - *

    - * if i is greater or equal to 0, than - * constructed string will be equal to - * l.toString(), otherwise, it is undefined - * @param l number, that needs to be converted to string - */ - public UtNativeString(long l) {} - - /** - * Returned variable's expression is equal to - * mkInt2BV(str.length(this), Long.SIZE_BITS) - * @return the length of this string - */ - public int length() { - return 0; - } - - /** - * If string represent a decimal positive number, - * then returned value is equal to Integer.valueOf(this)> - * Otherwise, the result is equal to -1 - *

    - * Returned variable's expression is equal to - * mkInt2BV(string2int(this), Long.SIZE_BITS) - */ - public int toInteger() { return 0; } - - /** - * If string represent a decimal positive number, - * then returned value is equal to Long.valueOf(this)> - * Otherwise, the result is equal to -1L - *

    - * Returned variable's expression is equal to - * mkInt2BV(string2int(this), Long.SIZE_BITS)> - */ - public long toLong() { return 0; } - - /** - * If i in valid index range of string, then - * a returned value's expression is equal to - * mkSeqNth(this, i).cast(char) - * @param i the index of char value to be returned - * @return the specified char value - */ - public char charAt(int i) { - return '\0'; - } - - /** - * Returns a char array with the same content as this string, - * shifted by offset indexes to the left. - *

    - * The returned value's UtExpression is equal to - * UtStringToArray(this, offset). - * @param offset - the number of indexes to be shifted to the left - * @return array of the string chars with shifted indexes by specified offset. - * @see org.utbot.engine.pc.UtArrayToString - */ - public char[] toCharArray(int offset) { return null; } - - /** - * If i in valid index range of string, then - * a returned value's expression is equal to - * mkSeqNth(this, i).cast(int) - * @param i the index of codePoint value to be returned - * @return the specified codePoint value - */ - public int codePointAt(int i) { - return 0; - } -} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtString.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtString.java index 9db996fb52..2d95283ada 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtString.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtString.java @@ -13,31 +13,13 @@ import static org.utbot.engine.overrides.UtOverrideMock.visit; import static java.lang.Math.min; -@SuppressWarnings({"ConstantConditions", "unused"}) +@SuppressWarnings("unused") public class UtString implements java.io.Serializable, Comparable, CharSequence { char[] value; int length; private static final long serialVersionUID = -6849794470754667710L; - public UtString(UtNativeString str) { - visit(this); - length = str.length(); - value = str.toCharArray(0); - } - - public static UtNativeString toUtNativeString(String s, int offset) { - char[] value = s.toCharArray(); - int length = value.length; - UtNativeString nativeString = new UtNativeString(); - assume(nativeString.length() == length - offset); - assume(nativeString.length() <= 2); - for (int i = offset; i < length; i++) { - assume(nativeString.charAt(i - offset) == value[i]); - } - return nativeString; - } - public UtString() { visit(this); value = new char[0]; @@ -774,7 +756,7 @@ public String concat(String str) { } char[] newValue = new char[length + str.length()]; UtArrayMock.arraycopy(value, 0, newValue, 0, length); - UtArrayMock.arraycopy(otherVal, 0, newValue, length + 1, str.length()); + UtArrayMock.arraycopy(otherVal, 0, newValue, length, str.length()); return new String(newValue); } @@ -866,47 +848,22 @@ public String replace(CharSequence target, CharSequence replacement) { return toString().replace(target, replacement); } + private String[] splitWithLimitImpl(String regex, int limit) { + return toString().split(regex, limit); + } + public String[] split(String regex, int limit) { - preconditionCheck(); - if (regex == null) { - throw new NullPointerException(); - } - if (limit < 0) { - throw new IllegalArgumentException(); - } - if (regex.length() == 0) { - int size = limit == 0 ? length + 1 : min(limit, length + 1); - String[] strings = new String[size]; - strings[size] = substring(size - 1); - // TODO remove assume - assume(size < 10); - for (int i = 0; i < size - 1; i++) { - strings[i] = Character.toString(value[i]); - } - return strings; - } - assume(regex.length() < 10); + return splitWithLimitImpl(regex, limit); + } + + private String[] splitImpl(String regex) { executeConcretely(); - return toStringImpl().split(regex, limit); + return toString().split(regex); } public String[] split(String regex) { preconditionCheck(); - if (regex == null) { - throw new NullPointerException(); - } - if (regex.length() == 0) { - String[] strings = new String[length + 1]; - strings[length] = ""; - // TODO remove assume - assume(length <= 25); - for (int i = 0; i < length; i++) { - strings[i] = Character.toString(value[i]); - } - return strings; - } - executeConcretely(); - return toStringImpl().split(regex); + return splitImpl(regex); } public String toLowerCase(Locale locale) { diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtStringBuffer.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtStringBuffer.java index 7931fc166c..d778a49ce3 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtStringBuffer.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtStringBuffer.java @@ -450,7 +450,7 @@ public StringBuffer insert(int dstOffset, CharSequence s, int start, int end) { } public StringBuffer insert(int offset, boolean b) { - return insert(offset, String.valueOf(b)); + return insert(offset, Boolean.toString(b)); } public StringBuffer insert(int offset, char c) { @@ -463,19 +463,19 @@ public StringBuffer insert(int offset, char c) { } public StringBuffer insert(int offset, int i) { - return insert(offset, String.valueOf(i)); + return insert(offset, Integer.toString(i)); } public StringBuffer insert(int offset, long l) { - return insert(offset, String.valueOf(l)); + return insert(offset, Long.toString(l)); } public StringBuffer insert(int offset, float f) { - return insert(offset, String.valueOf(f)); + return insert(offset, Float.toString(f)); } public StringBuffer insert(int offset, double d) { - return insert(offset, String.valueOf(d)); + return insert(offset, Double.toString(d)); } public int indexOf(String str) { diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtStringBuilder.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtStringBuilder.java index 4e06aaf24d..b627fa2aa1 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtStringBuilder.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/strings/UtStringBuilder.java @@ -451,7 +451,7 @@ public StringBuilder insert(int dstOffset, CharSequence s, int start, int end) { } public StringBuilder insert(int offset, boolean b) { - return insert(offset, String.valueOf(b)); + return insert(offset, Boolean.toString(b)); } public StringBuilder insert(int offset, char c) { @@ -464,19 +464,19 @@ public StringBuilder insert(int offset, char c) { } public StringBuilder insert(int offset, int i) { - return insert(offset, String.valueOf(i)); + return insert(offset, Integer.toString(i)); } public StringBuilder insert(int offset, long l) { - return insert(offset, String.valueOf(l)); + return insert(offset, Long.toString(l)); } public StringBuilder insert(int offset, float f) { - return insert(offset, String.valueOf(f)); + return insert(offset, Float.toString(f)); } public StringBuilder insert(int offset, double d) { - return insert(offset, String.valueOf(d)); + return insert(offset, Double.toString(d)); } public int indexOf(String str) { diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/CompletableFuture.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/CompletableFuture.java new file mode 100644 index 0000000000..f1ea088412 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/CompletableFuture.java @@ -0,0 +1,30 @@ +package org.utbot.engine.overrides.threads; + +import org.utbot.api.annotation.UtClassMock; + +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +@UtClassMock(target = java.util.concurrent.CompletableFuture.class, internalUsage = true) +public class CompletableFuture { + public static java.util.concurrent.CompletableFuture runAsync(Runnable runnable) { + java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); + + return future.thenRun(runnable); + } + + @SuppressWarnings("unused") + public static java.util.concurrent.CompletableFuture runAsync(Runnable runnable, Executor ignoredExecutor) { + return runAsync(runnable); + } + + public static java.util.concurrent.CompletableFuture supplyAsync(Supplier supplier) { + try { + final U value = supplier.get(); + + return new UtCompletableFuture<>(value).toCompletableFuture(); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/Executors.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/Executors.java new file mode 100644 index 0000000000..f04c0661e5 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/Executors.java @@ -0,0 +1,120 @@ +package org.utbot.engine.overrides.threads; + +import org.utbot.api.annotation.UtClassMock; +import org.utbot.api.mock.UtMock; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; + +@UtClassMock(target = java.util.concurrent.Executors.class, internalUsage = true) +public class Executors { + public static ExecutorService newFixedThreadPool(int nThreads) { + if (nThreads <= 0) { + throw new IllegalArgumentException(); + } + + return new UtExecutorService(); + } + + public static ExecutorService newWorkStealingPool() { + return new UtExecutorService(); + } + + public static ExecutorService newWorkStealingPool(int parallelism) { + if (parallelism <= 0) { + throw new IllegalArgumentException(); + } + + return new UtExecutorService(); + } + + public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { + if (threadFactory == null) { + throw new NullPointerException(); + } + + return newFixedThreadPool(nThreads); + } + + public static ExecutorService newSingleThreadExecutor() { + return new UtExecutorService(); + } + + public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { + if (threadFactory == null) { + throw new NullPointerException(); + } + + return new UtExecutorService(); + } + + public static ExecutorService newCachedThreadPool() { + return new UtExecutorService(); + } + + public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { + if (threadFactory == null) { + throw new NullPointerException(); + } + + return new UtExecutorService(); + } + + public static ScheduledExecutorService newSingleThreadScheduledExecutor() { + return new UtExecutorService(); + } + + public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) { + if (threadFactory == null) { + throw new NullPointerException(); + } + + return new UtExecutorService(); + } + + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { + if (corePoolSize < 0) { + throw new IllegalArgumentException(); + } + + return new UtExecutorService(); + } + + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) { + if (corePoolSize < 0) { + throw new IllegalArgumentException(); + } + + if (threadFactory == null) { + throw new NullPointerException(); + } + + return new UtExecutorService(); + } + + public static ExecutorService unconfigurableExecutorService(ExecutorService executor) { + if (executor == null) { + throw new NullPointerException(); + } + + return new UtExecutorService(); + } + + public static ScheduledExecutorService unconfigurableScheduledExecutorService(ScheduledExecutorService executor) { + if (executor == null) { + throw new NullPointerException(); + } + + return new UtExecutorService(); + } + + public static ThreadFactory defaultThreadFactory() { + // TODO make a wrapper? + return UtMock.makeSymbolic(); + } + + public static ThreadFactory privilegedThreadFactory() { + return defaultThreadFactory(); + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/ThreadFactory.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/ThreadFactory.java new file mode 100644 index 0000000000..f143c7be49 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/ThreadFactory.java @@ -0,0 +1,12 @@ +package org.utbot.engine.overrides.threads; + +import org.jetbrains.annotations.NotNull; +import org.utbot.api.annotation.UtClassMock; + +@UtClassMock(target = ThreadFactory.class, internalUsage = true) +public class ThreadFactory implements java.util.concurrent.ThreadFactory { + @Override + public Thread newThread(@NotNull Runnable r) { + return new Thread(r); + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtCompletableFuture.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtCompletableFuture.java new file mode 100644 index 0000000000..c87f14e356 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtCompletableFuture.java @@ -0,0 +1,453 @@ +package org.utbot.engine.overrides.threads; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +public class UtCompletableFuture implements ScheduledFuture, CompletionStage { + T result; + + Throwable exception; + + public UtCompletableFuture(T result) { + this.result = result; + } + + public UtCompletableFuture() {} + + public UtCompletableFuture(Throwable exception) { + this.exception = exception; + } + + public UtCompletableFuture(UtCompletableFuture future) { + result = future.result; + exception = future.exception; + } + + public void eqGenericType(T ignoredValue) { + // Will be processed symbolically + } + + public void preconditionCheck() { + eqGenericType(result); + } + + @Override + public CompletableFuture thenApply(Function fn) { + preconditionCheck(); + + final U nextResult; + try { + nextResult = fn.apply(result); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture<>(nextResult).toCompletableFuture(); + } + + @Override + public CompletableFuture thenApplyAsync(Function fn) { + return thenApply(fn); + } + + @Override + public CompletableFuture thenApplyAsync(Function fn, Executor executor) { + return thenApply(fn); + } + + @Override + public CompletableFuture thenAccept(Consumer action) { + preconditionCheck(); + + try { + action.accept(result); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture().toCompletableFuture(); + } + + @Override + public CompletableFuture thenAcceptAsync(Consumer action) { + return thenAccept(action); + } + + @Override + public CompletableFuture thenAcceptAsync(Consumer action, Executor executor) { + return thenAccept(action); + } + + @Override + public CompletableFuture thenRun(Runnable action) { + preconditionCheck(); + + try { + action.run(); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture().toCompletableFuture(); + } + + @Override + public CompletableFuture thenRunAsync(Runnable action) { + return thenRun(action); + } + + @Override + public CompletableFuture thenRunAsync(Runnable action, Executor executor) { + return thenRun(action); + } + + @Override + public CompletableFuture thenCombine(CompletionStage other, BiFunction fn) { + preconditionCheck(); + + final CompletableFuture completableFuture = other.toCompletableFuture(); + if (fn == null || completableFuture == null) { + throw new NullPointerException(); + } + + U otherResult; + try { + otherResult = completableFuture.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + + final V nextResult; + try { + nextResult = fn.apply(result, otherResult); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture<>(nextResult).toCompletableFuture(); + } + + @Override + public CompletableFuture thenCombineAsync(CompletionStage other, BiFunction fn) { + return thenCombine(other, fn); + } + + @Override + public CompletableFuture thenCombineAsync(CompletionStage other, BiFunction fn, Executor executor) { + return thenCombine(other, fn); + } + + @Override + public CompletableFuture thenAcceptBoth(CompletionStage other, BiConsumer action) { + preconditionCheck(); + + final CompletableFuture completableFuture = other.toCompletableFuture(); + if (action == null || completableFuture == null) { + throw new NullPointerException(); + } + + U otherResult; + try { + otherResult = completableFuture.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + + try { + action.accept(result, otherResult); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture().toCompletableFuture(); + } + + @Override + public CompletableFuture thenAcceptBothAsync(CompletionStage other, BiConsumer action) { + return thenAcceptBoth(other, action); + } + + @Override + public CompletableFuture thenAcceptBothAsync(CompletionStage other, BiConsumer action, Executor executor) { + return thenAcceptBoth(other, action); + } + + @Override + public CompletableFuture runAfterBoth(CompletionStage other, Runnable action) { + preconditionCheck(); + + final CompletableFuture completableFuture = other.toCompletableFuture(); + if (action == null || completableFuture == null) { + throw new NullPointerException(); + } + + try { + action.run(); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture().toCompletableFuture(); + } + + @Override + public CompletableFuture runAfterBothAsync(CompletionStage other, Runnable action) { + return runAfterBoth(other, action); + } + + @Override + public CompletableFuture runAfterBothAsync(CompletionStage other, Runnable action, Executor executor) { + return runAfterBoth(other, action); + } + + @Override + public CompletableFuture applyToEither(CompletionStage other, Function fn) { + preconditionCheck(); + + final CompletableFuture completableFuture = other.toCompletableFuture(); + if (fn == null || completableFuture == null) { + throw new NullPointerException(); + } + + final T eitherResult; + try { + eitherResult = (result != null) ? result : completableFuture.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + + final U newResult; + try { + newResult = fn.apply(eitherResult); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture<>(newResult).toCompletableFuture(); + } + + @Override + public CompletableFuture applyToEitherAsync(CompletionStage other, Function fn) { + return applyToEither(other, fn); + } + + @Override + public CompletableFuture applyToEitherAsync(CompletionStage other, Function fn, Executor executor) { + return applyToEither(other, fn); + } + + @Override + public CompletableFuture acceptEither(CompletionStage other, Consumer action) { + preconditionCheck(); + + final CompletableFuture completableFuture = other.toCompletableFuture(); + if (action == null || completableFuture == null) { + throw new NullPointerException(); + } + + final T eitherResult; + try { + eitherResult = (result != null) ? result : completableFuture.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + + try { + action.accept(eitherResult); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture().toCompletableFuture(); + } + + @Override + public CompletableFuture acceptEitherAsync(CompletionStage other, Consumer action) { + return acceptEither(other, action); + } + + @Override + public CompletableFuture acceptEitherAsync(CompletionStage other, Consumer action, Executor executor) { + return acceptEither(other, action); + } + + @Override + public CompletableFuture runAfterEither(CompletionStage other, Runnable action) { + preconditionCheck(); + + try { + action.run(); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture().toCompletableFuture(); + } + + @Override + public CompletableFuture runAfterEitherAsync(CompletionStage other, Runnable action) { + return runAfterEither(other, action); + } + + @Override + public CompletableFuture runAfterEitherAsync(CompletionStage other, Runnable action, Executor executor) { + return runAfterEither(other, action); + } + + @Override + public CompletableFuture thenCompose(Function> fn) { + preconditionCheck(); + + return fn.apply(result).toCompletableFuture(); + } + + @Override + public CompletableFuture thenComposeAsync(Function> fn) { + return thenCompose(fn); + } + + @Override + public CompletableFuture thenComposeAsync(Function> fn, Executor executor) { + return thenCompose(fn); + } + + @Override + public CompletableFuture handle(BiFunction fn) { + preconditionCheck(); + + U newResult; + try { + newResult = fn.apply(result, exception); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return new UtCompletableFuture<>(newResult).toCompletableFuture(); + } + + @Override + public CompletableFuture handleAsync(BiFunction fn) { + return handle(fn); + } + + @Override + public CompletableFuture handleAsync(BiFunction fn, Executor executor) { + return handle(fn); + } + + @Override + public CompletableFuture whenComplete(BiConsumer action) { + preconditionCheck(); + + final UtCompletableFuture next = new UtCompletableFuture<>(this); + try { + action.accept(next.result, next.exception); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + + return next.toCompletableFuture(); + } + + @Override + public CompletableFuture whenCompleteAsync(BiConsumer action) { + return whenComplete(action); + } + + @Override + public CompletableFuture whenCompleteAsync(BiConsumer action, Executor executor) { + return whenComplete(action); + } + + @Override + public CompletableFuture exceptionally(Function fn) { + preconditionCheck(); + + if (fn == null) { + throw new NullPointerException(); + } + + if (exception != null) { + try { + final T exceptionalResult = fn.apply(exception); + return new UtCompletableFuture<>(exceptionalResult).toCompletableFuture(); + } catch (Throwable e) { + return new UtCompletableFuture(e).toCompletableFuture(); + } + } + + return new UtCompletableFuture<>(result).toCompletableFuture(); + } + + @Override + public CompletableFuture toCompletableFuture() { + // Will be processed symbolically + return null; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + preconditionCheck(); + // Tasks could not be canceled since they are supposed to be executed immediately + return false; + } + + @Override + public boolean isCancelled() { + preconditionCheck(); + // Tasks could not be canceled since they are supposed to be executed immediately + return false; + } + + @Override + public boolean isDone() { + preconditionCheck(); + + return true; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + preconditionCheck(); + + if (exception != null) { + throw new ExecutionException(exception); + } + + return result; + } + + @Override + public T get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return get(); + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return 0; + } + + @Override + public int compareTo(@NotNull Delayed o) { + if (o == this) { // compare zero if same object{ + return 0; + } + + long diff = getDelay(NANOSECONDS) - o.getDelay(NANOSECONDS); + return (diff < 0) ? -1 : (diff > 0) ? 1 : 0; + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtCountDownLatch.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtCountDownLatch.java new file mode 100644 index 0000000000..f46bea70f0 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtCountDownLatch.java @@ -0,0 +1,63 @@ +package org.utbot.engine.overrides.threads; + +import java.util.concurrent.TimeUnit; + +import static org.utbot.api.mock.UtMock.assume; +import static org.utbot.engine.overrides.UtOverrideMock.alreadyVisited; +import static org.utbot.engine.overrides.UtOverrideMock.visit; + +public class UtCountDownLatch { + private int count; + + public UtCountDownLatch(int count) { + if (count < 0) { + throw new IllegalArgumentException(); + } + + visit(this); + this.count = count; + } + + void preconditionCheck() { + if (alreadyVisited(this)) { + return; + } + + assume(count >= 0); + + visit(this); + } + + public void await() { + preconditionCheck(); + // Do nothing + } + + public boolean await(long ignoredTimeout, TimeUnit ignoredUnit) { + preconditionCheck(); + + return count == 0; + } + + public void countDown() { + preconditionCheck(); + + if (count != 0) { + count--; + } + } + + public long getCount() { + preconditionCheck(); + + return count; + } + + @Override + public String toString() { + preconditionCheck(); + // Actually, the real string representation also contains some meta-information about this class, + // but it looks redundant for this wrapper + return String.valueOf(count); + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtExecutorService.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtExecutorService.java new file mode 100644 index 0000000000..b5246654b9 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtExecutorService.java @@ -0,0 +1,160 @@ +package org.utbot.engine.overrides.threads; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class UtExecutorService implements ScheduledExecutorService { + private boolean isShutdown; + private boolean isTerminated; + + @Override + public void shutdown() { + isShutdown = true; + isTerminated = true; + } + + @NotNull + @Override + public List shutdownNow() { + shutdown(); + // Since all tasks are processed immediately, there are no waiting tasks + return new ArrayList<>(); + } + + @Override + public boolean isShutdown() { + return isShutdown; + } + + @Override + public boolean isTerminated() { + return isTerminated; + } + + @Override + public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) { + // No need to wait tasks + return true; + } + + @NotNull + @Override + public Future submit(@NotNull Callable task) { + try { + T result = task.call(); + return new UtCompletableFuture<>(result); + } catch (Exception e) { + return new UtCompletableFuture<>(e); + } + } + + @NotNull + @Override + public Future submit(@NotNull Runnable task, T result) { + try { + task.run(); + return new UtCompletableFuture<>(result); + } catch (Exception e) { + return new UtCompletableFuture<>(e); + } + } + + @NotNull + @Override + public Future submit(@NotNull Runnable task) { + return submit(task, null); + } + + @NotNull + @Override + public List> invokeAll(@NotNull Collection> tasks) { + List> results = new ArrayList<>(); + for (Callable task : tasks) { + results.add(submit(task)); + } + + return results; + } + + @NotNull + @Override + public List> invokeAll(@NotNull Collection> tasks, long timeout, @NotNull TimeUnit unit) { + return invokeAll(tasks); + } + + @NotNull + @Override + public T invokeAny(@NotNull Collection> tasks) throws ExecutionException { + for (Callable task : tasks) { + try { + return task.call(); + } catch (Exception e) { + // Do nothing + } + } + + // ExecutionException no-parameters constructor is protected + throw new ExecutionException(new RuntimeException()); + } + + @Override + public T invokeAny(@NotNull Collection> tasks, long timeout, @NotNull TimeUnit unit) throws ExecutionException { + return invokeAny(tasks); + } + + @Override + public void execute(@NotNull Runnable command) { + command.run(); + } + + @NotNull + @Override + public ScheduledFuture schedule(@NotNull Runnable command, long delay, @NotNull TimeUnit unit) { + try { + command.run(); + return new UtCompletableFuture<>(); + } catch (Exception e) { + return new UtCompletableFuture<>(e); + } + } + + @NotNull + @Override + public ScheduledFuture schedule(@NotNull Callable callable, long delay, @NotNull TimeUnit unit) { + try { + V result = callable.call(); + return new UtCompletableFuture<>(result); + } catch (Exception e) { + return new UtCompletableFuture<>(e); + } + } + + @NotNull + @Override + public ScheduledFuture scheduleAtFixedRate(@NotNull Runnable command, long initialDelay, long period, @NotNull TimeUnit unit) { + if (period <= 0) { + throw new IllegalArgumentException(); + } + + return schedule(command, initialDelay, unit); + } + + @NotNull + @Override + public ScheduledFuture scheduleWithFixedDelay(@NotNull Runnable command, long initialDelay, long delay, @NotNull TimeUnit unit) { + if (delay <= 0) { + throw new IllegalArgumentException(); + } + + return schedule(command, initialDelay, unit); + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtThread.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtThread.java new file mode 100644 index 0000000000..8a8b3560c6 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtThread.java @@ -0,0 +1,434 @@ +package org.utbot.engine.overrides.threads; + +import org.utbot.api.mock.UtMock; + +import java.security.AccessControlContext; +import java.util.Map; + +import static org.utbot.api.mock.UtMock.assume; +import static org.utbot.engine.overrides.UtOverrideMock.alreadyVisited; +import static org.utbot.engine.overrides.UtOverrideMock.visit; + +@SuppressWarnings("unused") +public class UtThread { + private String name; + private int priority; + + private boolean daemon; + + private final Runnable target; + + private final ThreadGroup group; + + /* The context ClassLoader for this thread */ + private ClassLoader contextClassLoader; + + /* For autonumbering anonymous threads. */ + private static int threadInitNumber = 0; + + private static int nextThreadNum() { + return threadInitNumber++; + } + + /* + * Thread ID + */ + private final long tid; + + /* For generating thread ID */ + private static long threadSeqNumber; + + private static long nextThreadID() { + return ++threadSeqNumber; + } + + private boolean isInterrupted; + + // This field is required by ThreadLocal class. The real type is a package-private type ThreadLocal.ThreadLocalMap + Object threadLocals; + + // This field is required by InheritableThreadLocal class. The real type is a package-private type ThreadLocal.ThreadLocalMap + Object inheritableThreadLocals; + + /** + * The minimum priority that a thread can have. + */ + public static final int MIN_PRIORITY = 1; + + /** + * The maximum priority that a thread can have. + */ + public static final int MAX_PRIORITY = 10; + + // null unless explicitly set + private volatile Thread.UncaughtExceptionHandler uncaughtExceptionHandler; + + // null unless explicitly set + private static volatile Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler; + + public static UtThread currentThread() { + return UtMock.makeSymbolic(); + } + + public static void yield() { + // Do nothing + } + + @SuppressWarnings("RedundantThrows") + public static void sleep(long ignoredMillis) throws InterruptedException { + // Do nothing + } + + @SuppressWarnings("RedundantThrows") + public static void sleep(long millis, int nanos) throws InterruptedException { + if (millis < 0) { + throw new IllegalArgumentException(); + } + + if (nanos < 0 || nanos > 999999) { + throw new IllegalArgumentException(); + } + } + + public UtThread() { + this(null, null, UtMock.makeSymbolic(), 0); + } + + public UtThread(Runnable target) { + this(null, target, UtMock.makeSymbolic(), 0); + } + + public UtThread(ThreadGroup group, Runnable target) { + this(group, target, UtMock.makeSymbolic(), 0); + } + + public UtThread(String name) { + this(null, null, name, 0); + } + + public UtThread(ThreadGroup group, String name) { + this(group, null, name, 0); + } + + public UtThread(Runnable target, String name) { + this(null, target, name, 0); + } + + public UtThread(ThreadGroup group, Runnable target, String name) { + this(group, target, name, 0); + } + + public UtThread(ThreadGroup group, Runnable target, String name, + long stackSize) { + this(group, target, name, stackSize, null, true); + } + + public UtThread(ThreadGroup group, Runnable target, String name, + long stackSize, boolean inheritUtThreadLocals) { + this(group, target, name, stackSize, null, inheritUtThreadLocals); + } + + private UtThread(ThreadGroup g, Runnable target, String name, + long stackSize, AccessControlContext ignoredAcc, + boolean ignoredInheritUtThreadLocals) { + visit(this); + + if (name == null) { + throw new NullPointerException(); + } + + this.name = name; + + if (g == null) { + g = UtMock.makeSymbolic(); + } + + this.group = g; + this.daemon = UtMock.makeSymbolic(); + + final Integer priority = UtMock.makeSymbolic(); + assume(priority >= MIN_PRIORITY); + assume(priority <= MIN_PRIORITY); + this.priority = priority; + setPriority(this.priority); + + this.contextClassLoader = UtMock.makeSymbolic(); + this.target = target; + + this.tid = nextThreadID(); + + // We need to make it possible to cast these fields to the ThreadLocal.ThreadLocalMap type + UtMock.disableClassCastExceptionCheck(threadLocals); + UtMock.disableClassCastExceptionCheck(inheritableThreadLocals); + } + + public void preconditionCheck() { + if (alreadyVisited(this)) { + return; + } + + assume(name != null); + + assume(priority >= MIN_PRIORITY); + assume(priority <= MIN_PRIORITY); + + assume(group != null); + assume(contextClassLoader != null); + + assume(threadInitNumber >= 0); + assume(tid >= 0); + assume(threadSeqNumber >= 0); + + visit(this); + } + + public void start() { + run(); + } + + public void run() { + preconditionCheck(); + + if (target != null) { + target.run(); + } + } + + public final void stop() { + preconditionCheck(); + // DO nothing + } + + public void interrupt() { + preconditionCheck(); + // Set interrupted status + isInterrupted = true; + } + + public static boolean interrupted() { + return UtMock.makeSymbolic(); + } + + public boolean isInterrupted() { + preconditionCheck(); + + return isInterrupted; + } + + private boolean isInterrupted(boolean clearInterrupted) { + preconditionCheck(); + + boolean result = isInterrupted; + + if (clearInterrupted) { + isInterrupted = false; + } + + return result; + } + + public final boolean isAlive() { + preconditionCheck(); + + return UtMock.makeSymbolic(); + } + + public final void suspend() { + preconditionCheck(); + // Do nothing + } + + public final void resume() { + preconditionCheck(); + // Do nothing + } + + public final void setPriority(int newPriority) { + preconditionCheck(); + + if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { + throw new IllegalArgumentException(); + } + + if (group != null) { + if (newPriority > group.getMaxPriority()) { + newPriority = group.getMaxPriority(); + } + + priority = newPriority; + } + } + + public final int getPriority() { + preconditionCheck(); + + return priority; + } + + public final void setName(String name) { + preconditionCheck(); + + if (name == null) { + throw new NullPointerException(); + } + + this.name = name; + } + + public final String getName() { + preconditionCheck(); + + return name; + } + + public final ThreadGroup getThreadGroup() { + preconditionCheck(); + + return group; + } + + public static int activeCount() { + final Integer result = UtMock.makeSymbolic(); + assume(result >= 0); + + return result; + } + + public static int enumerate(UtThread[] tarray) { + Integer length = UtMock.makeSymbolic(); + assume(length >= 0); + assume(length <= tarray.length); + + for (int i = 0; i < length; i++) { + tarray[i] = UtMock.makeSymbolic(); + } + + return length; + } + + public int countStackFrames() { + preconditionCheck(); + + return UtMock.makeSymbolic(); + } + + public final void join(long millis) throws InterruptedException { + preconditionCheck(); + + if (millis < 0) { + throw new IllegalArgumentException(); + } + + // Do nothing + } + + public final void join(long millis, int nanos) throws InterruptedException { + preconditionCheck(); + + if (millis < 0) { + throw new IllegalArgumentException(); + } + + if (nanos < 0 || nanos > 999999) { + throw new IllegalArgumentException(); + } + + // Do nothing + } + + public final void join() throws InterruptedException { + preconditionCheck(); + + join(0); + } + + public static void dumpStack() { + // Do nothing + } + + public final void setDaemon(boolean on) { + preconditionCheck(); + + daemon = on; + } + + public final boolean isDaemon() { + preconditionCheck(); + + return daemon; + } + + public String toString() { + preconditionCheck(); + + if (group != null) { + return "Thread[" + getName() + "," + getPriority() + "," + + group.getName() + "]"; + } else { + return "Thread[" + getName() + "," + getPriority() + "," + + "" + "]"; + } + } + + public ClassLoader getContextClassLoader() { + preconditionCheck(); + + return contextClassLoader; + } + + public void setContextClassLoader(ClassLoader cl) { + preconditionCheck(); + + contextClassLoader = cl; + } + + public static boolean holdsLock(Object obj) { + if (obj == null) { + throw new NullPointerException(); + } + + return UtMock.makeSymbolic(); + } + + public StackTraceElement[] getStackTrace() { + preconditionCheck(); + + return UtMock.makeSymbolic(); + } + + public static Map getAllStackTraces() { + return UtMock.makeSymbolic(); + } + + public long getId() { + preconditionCheck(); + + return tid; + } + + public Thread.State getState() { + preconditionCheck(); + + return UtMock.makeSymbolic(); + } + + public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) { + defaultUncaughtExceptionHandler = eh; + } + + public static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() { + return defaultUncaughtExceptionHandler; + } + + public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() { + preconditionCheck(); + + return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group; + } + + public void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) { + preconditionCheck(); + + uncaughtExceptionHandler = eh; + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtThreadGroup.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtThreadGroup.java new file mode 100644 index 0000000000..5450ed7706 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/threads/UtThreadGroup.java @@ -0,0 +1,327 @@ +package org.utbot.engine.overrides.threads; + +import org.utbot.api.mock.UtMock; + +import java.util.Arrays; + +import static org.utbot.api.mock.UtMock.assume; +import static org.utbot.engine.overrides.UtOverrideMock.alreadyVisited; +import static org.utbot.engine.overrides.UtOverrideMock.visit; + +@SuppressWarnings("unused") +public class UtThreadGroup implements Thread.UncaughtExceptionHandler { + private final ThreadGroup parent; + String name; + int maxPriority; + boolean destroyed; + boolean daemon; + + int nUnstartedThreads = 0; + int nthreads; + Thread[] threads; + + int ngroups; + ThreadGroup[] groups; + + public UtThreadGroup(String name) { + this(UtThread.currentThread().getThreadGroup(), name); + } + + public UtThreadGroup(ThreadGroup parent, String name) { + visit(this); + + this.name = name; + + final Integer maxPriority = UtMock.makeSymbolic(); + assume(maxPriority >= UtThread.MIN_PRIORITY); + assume(maxPriority <= UtThread.MAX_PRIORITY); + this.maxPriority = maxPriority; + + this.daemon = UtMock.makeSymbolic(); + this.parent = parent; + } + + public void preconditionCheck() { + if (alreadyVisited(this)) { + return; + } + + assume(parent != null); + assume(name != null); + + assume(maxPriority >= UtThread.MIN_PRIORITY); + assume(maxPriority <= UtThread.MAX_PRIORITY); + + assume(nUnstartedThreads >= 0); + assume(ngroups >= 0); + + visit(this); + } + + public final String getName() { + preconditionCheck(); + + return name; + } + + public final ThreadGroup getParent() { + preconditionCheck(); + + return parent; + } + + public final int getMaxPriority() { + preconditionCheck(); + + return maxPriority; + } + + public final boolean isDaemon() { + preconditionCheck(); + + return daemon; + } + + public boolean isDestroyed() { + preconditionCheck(); + + return destroyed; + } + + public final void setDaemon(boolean daemon) { + preconditionCheck(); + + this.daemon = daemon; + } + + public final void setMaxPriority(int pri) { + preconditionCheck(); + + if (pri < UtThread.MIN_PRIORITY || pri > UtThread.MAX_PRIORITY) { + return; + } + + for (int i = 0; i < ngroups; i++) { + groups[i].setMaxPriority(pri); + } + } + + public final boolean parentOf(ThreadGroup g) { + preconditionCheck(); + + return UtMock.makeSymbolic(); + } + + public final void checkAccess() { + preconditionCheck(); + // Do nothing + } + + public int activeCount() { + preconditionCheck(); + + if (destroyed) { + return 0; + } + + final Integer result = UtMock.makeSymbolic(); + assume(result >= 0); + return result; + } + + public int enumerate(Thread[] list) { + return enumerate(list, 0, true); + } + + public int enumerate(Thread[] list, boolean recurse) { + return enumerate(list, 0, recurse); + } + + @SuppressWarnings({"SameParameterValue", "ParameterCanBeLocal"}) + private int enumerate(Thread[] list, int ignoredN, boolean ignoredRecurse) { + preconditionCheck(); + + if (destroyed) { + return 0; + } + + list = UtMock.makeSymbolic(); + + final Integer result = UtMock.makeSymbolic(); + assume(result <= list.length); + assume(result >= 0); + + return result; + } + + public int activeGroupCount() { + preconditionCheck(); + + if (destroyed) { + return 0; + } + + final Integer result = UtMock.makeSymbolic(); + assume(result >= 0); + return result; + } + + public int enumerate(ThreadGroup[] list) { + return enumerate(list, 0, true); + } + + public int enumerate(ThreadGroup[] list, boolean recurse) { + return enumerate(list, 0, recurse); + } + + @SuppressWarnings({"SameParameterValue", "ParameterCanBeLocal"}) + private int enumerate(ThreadGroup[] list, int ignoredN, boolean ignoredRecurse) { + preconditionCheck(); + + if (destroyed) { + return 0; + } + + list = UtMock.makeSymbolic(); + + final Integer result = UtMock.makeSymbolic(); + assume(result <= list.length); + assume(result >= 0); + + return result; + } + + public final void stop() { + preconditionCheck(); + // Do nothing + } + + public final void interrupt() { + preconditionCheck(); + + for (int i = 0; i < nthreads; i++) { + threads[i].interrupt(); + } + + for (int i = 0; i < ngroups; i++) { + groups[i].interrupt(); + } + } + + public final void suspend() { + preconditionCheck(); + // Do nothing + } + + public final void resume() { + preconditionCheck(); + // Do nothing + } + + public final void destroy() { + preconditionCheck(); + + if (destroyed || nthreads > 0) { + throw new IllegalThreadStateException(); + } + + if (parent != null) { + destroyed = true; + ngroups = 0; + groups = null; + nthreads = 0; + threads = null; + } + for (int i = 0; i < ngroups; i += 1) { + groups[i].destroy(); + } + } + + private void add(ThreadGroup g) { + preconditionCheck(); + + if (destroyed) { + throw new IllegalThreadStateException(); + } + + if (groups == null) { + groups = new ThreadGroup[4]; + } else if (ngroups == groups.length) { + groups = Arrays.copyOf(groups, ngroups * 2); + } + groups[ngroups] = g; + + ngroups++; + } + + private void remove(ThreadGroup g) { + preconditionCheck(); + + if (destroyed) { + return; + } + + for (int i = 0; i < ngroups; i++) { + if (groups[i] == g) { + ngroups -= 1; + System.arraycopy(groups, i + 1, groups, i, ngroups - i); + groups[ngroups] = null; + break; + } + } + + if (daemon && nthreads == 0 && nUnstartedThreads == 0 && ngroups == 0) { + destroy(); + } + } + + void addUnstarted() { + preconditionCheck(); + + if (destroyed) { + throw new IllegalThreadStateException(); + } + + nUnstartedThreads++; + } + + void add(Thread t) { + preconditionCheck(); + + if (destroyed) { + throw new IllegalThreadStateException(); + } + + if (threads == null) { + threads = new Thread[4]; + } else if (nthreads == threads.length) { + threads = Arrays.copyOf(threads, nthreads * 2); + } + threads[nthreads] = t; + + nthreads++; + nUnstartedThreads--; + } + + public void list() { + preconditionCheck(); + // Do nothing + } + + public void uncaughtException(Thread t, Throwable e) { + preconditionCheck(); + // Do nothing + } + + public boolean allowThreadSuspension(boolean b) { + preconditionCheck(); + + return true; + } + + public String toString() { + preconditionCheck(); + + return getClass().getName() + "[name=" + getName() + ",maxpri=" + maxPriority + "]"; + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractor.kt b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractor.kt index c86cad1b54..e63ccdcc92 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractor.kt @@ -1,6 +1,6 @@ package org.utbot.analytics -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState /** * Class that encapsulates work with FeatureExtractor during symbolic execution. 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 5049d5d397..d09277a977 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ArrayObjectWrappers.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ArrayObjectWrappers.kt @@ -18,6 +18,9 @@ import org.utbot.engine.pc.mkInt import org.utbot.engine.pc.select import org.utbot.engine.pc.store import org.utbot.engine.symbolic.asHardConstraint +import org.utbot.engine.types.OBJECT_TYPE +import org.utbot.engine.types.TypeRegistry +import org.utbot.engine.types.TypeResolver import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtCompositeModel @@ -25,6 +28,8 @@ 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.getIdOrThrow +import org.utbot.framework.plugin.api.id +import org.utbot.framework.plugin.api.util.fieldId import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.objectArrayClassId import org.utbot.framework.plugin.api.util.objectClassId @@ -199,14 +204,15 @@ class RangeModifiableUnlimitedArrayWrapper : WrapperInterface { val addr = UtAddrExpression(value) // Try to retrieve manually set type if present - val valueType = typeRegistry - .getTypeStoragesForObjectTypeParameters(wrapper.addr) - ?.singleOrNull() + val valueType = extractSingleTypeParameterForRangeModifiableArray(wrapper.addr, memory) ?.leastCommonType ?: OBJECT_TYPE val resultObject = if (valueType is RefType) { - createObject(addr, valueType, useConcreteType = false) + val mockInfoGenerator = UtMockInfoGenerator { mockAddr -> + UtObjectMockInfo(valueType.id, mockAddr) + } + createObject(addr, valueType, useConcreteType = false, mockInfoGenerator) } else { require(valueType is ArrayType) { "Unexpected Primitive Type $valueType in generic parameter for RangeModifiableUnlimitedArray $wrapper" @@ -215,13 +221,17 @@ class RangeModifiableUnlimitedArrayWrapper : WrapperInterface { createArray(addr, valueType, useConcreteType = false) } - listOf( - MethodResult( - SymbolicSuccess(resultObject), - typeRegistry.typeConstraintToGenericTypeParameter(addr, wrapper.addr, i = TYPE_PARAMETER_INDEX) - .asHardConstraint() - ) - ) + val typeIndex = wrapper.asWrapperOrNull?.getOperationTypeIndex + ?: error("Wrapper was expected, got $wrapper") + val typeConstraint = typeRegistry.typeConstraintToGenericTypeParameter( + addr, + wrapper.addr, + i = typeIndex + ).asHardConstraint() + + val methodResult = MethodResult(SymbolicSuccess(resultObject), typeConstraint) + + listOf(methodResult) } @Suppress("UNUSED_PARAMETER") @@ -337,8 +347,8 @@ class RangeModifiableUnlimitedArrayWrapper : WrapperInterface { resolver.addConstructedModel(concreteAddr, resultModel) // try to retrieve type storage for the single type parameter - val typeStorage = - resolver.typeRegistry.getTypeStoragesForObjectTypeParameters(wrapper.addr)?.singleOrNull() ?: TypeRegistry.objectTypeStorage + val typeStorage = extractSingleTypeParameterForRangeModifiableArray(wrapper.addr, resolver.memory) + ?: TypeRegistry.objectTypeStorage (0 until sizeValue).associateWithTo(resultModel.stores) { i -> val addr = UtAddrExpression(arrayExpression.select(mkInt(i + firstValue))) @@ -355,6 +365,15 @@ class RangeModifiableUnlimitedArrayWrapper : WrapperInterface { return resultModel } + private fun extractSingleTypeParameterForRangeModifiableArray( + addr: UtAddrExpression, + memory: Memory + ): TypeStorage? = TypeResolver.extractTypeStorageForObjectWithSingleTypeParameter( + addr, + objectClassName = "Range modifiable array", + memory + ) + companion object { internal val rangeModifiableArrayClass: SootClass get() = Scene.v().getSootClass(rangeModifiableArrayId.name) @@ -416,18 +435,21 @@ class AssociativeArrayWrapper : WrapperInterface { with(traverser) { val value = getStorageArrayExpression(wrapper).select(parameters[0].addr) val addr = UtAddrExpression(value) - val resultObject = createObject(addr, OBJECT_TYPE, useConcreteType = false) + // TODO it is probably a bug, what mock generator should we provide here? + // Seems like we don't know anything about its type here + val resultObject = createObject(addr, OBJECT_TYPE, useConcreteType = false, mockInfoGenerator = null) + + val typeIndex = wrapper.asWrapperOrNull?.selectOperationTypeIndex + ?: error("Wrapper was expected, got $wrapper") + val hardConstraints = typeRegistry.typeConstraintToGenericTypeParameter( + addr, + wrapper.addr, + typeIndex + ).asHardConstraint() - listOf( - MethodResult( - SymbolicSuccess(resultObject), - typeRegistry.typeConstraintToGenericTypeParameter( - addr, - wrapper.addr, - TYPE_PARAMETER_INDEX - ).asHardConstraint() - ) - ) + val methodResult = MethodResult(SymbolicSuccess(resultObject), hardConstraints) + + listOf(methodResult) } @Suppress("UNUSED_PARAMETER") @@ -440,21 +462,31 @@ class AssociativeArrayWrapper : WrapperInterface { with(traverser) { val storageValue = getStorageArrayExpression(wrapper).store(parameters[0].addr, parameters[1].addr) val sizeValue = getIntFieldValue(wrapper, sizeField) + + // it is the reason why it's important to use an `oldKey` in `UtHashMap.put` method. + // We navigate in the associative array using only this old address, not a new one. val touchedValue = getTouchedArrayExpression(wrapper).store(sizeValue, parameters[0].addr) - listOf( - MethodResult( - SymbolicSuccess(voidValue), - memoryUpdates = arrayUpdateWithValue( - getStorageArrayField(wrapper.addr).addr, - OBJECT_TYPE.arrayType, - storageValue - ) + arrayUpdateWithValue( - getTouchedArrayField(wrapper.addr).addr, - OBJECT_TYPE.arrayType, - touchedValue, - ) + objectUpdate(wrapper, sizeField, Add(sizeValue.toIntValue(), 1.toPrimitiveValue())) - ) + val storageArrayAddr = getStorageArrayField(wrapper.addr).addr + val touchedArrayFieldAddr = getTouchedArrayField(wrapper.addr).addr + + val storageArrayUpdate = arrayUpdateWithValue( + storageArrayAddr, + OBJECT_TYPE.arrayType, + storageValue + ) + + val touchedArrayUpdate = arrayUpdateWithValue( + touchedArrayFieldAddr, + OBJECT_TYPE.arrayType, + touchedValue, ) + + val sizeUpdate = objectUpdate(wrapper, sizeField, Add(sizeValue.toIntValue(), 1.toPrimitiveValue())) + + val memoryUpdates = storageArrayUpdate + touchedArrayUpdate + sizeUpdate + val methodResult = MethodResult(SymbolicSuccess(voidValue), memoryUpdates = memoryUpdates) + + listOf(methodResult) } override val wrappedMethods: Map = mapOf( @@ -480,13 +512,13 @@ class AssociativeArrayWrapper : WrapperInterface { // construct model values of an array val touchedValues = UtArrayModel( resolver.holder.concreteAddr(UtAddrExpression(touchedArrayAddr)), - objectClassId, + objectArrayClassId, sizeValue, UtNullModel(objectClassId), stores = (0 until sizeValue).associateWithTo(mutableMapOf()) { i -> resolver.resolveModel( ObjectValue( - TypeStorage(OBJECT_TYPE), + TypeStorage.constructTypeStorageWithSingleType(OBJECT_TYPE), UtAddrExpression(touchedArrayExpression.select(mkInt(i))) ) ) @@ -504,7 +536,7 @@ class AssociativeArrayWrapper : WrapperInterface { val storageValues = UtArrayModel( resolver.holder.concreteAddr(UtAddrExpression(storageArrayAddr)), - objectClassId, + objectArrayClassId, sizeValue, UtNullModel(objectClassId), stores = (0 until sizeValue).associateTo(mutableMapOf()) { i -> @@ -512,16 +544,18 @@ class AssociativeArrayWrapper : WrapperInterface { val addr = model.getIdOrThrow() addr to resolver.resolveModel( ObjectValue( - TypeStorage(OBJECT_TYPE), + TypeStorage.constructTypeStorageWithSingleType(OBJECT_TYPE), UtAddrExpression(storageArrayExpression.select(mkInt(addr))) ) ) }) - val model = UtCompositeModel(resolver.holder.concreteAddr(wrapper.addr), associativeArrayId, false) + val model = UtCompositeModel(resolver.holder.concreteAddr(wrapper.addr), associativeArrayId, isMock = false) + model.fields[sizeField.fieldId] = UtPrimitiveModel(sizeValue) model.fields[touchedField.fieldId] = touchedValues model.fields[storageField.fieldId] = storageValues + return model } @@ -537,7 +571,7 @@ class AssociativeArrayWrapper : WrapperInterface { private fun Traverser.getStorageArrayExpression( wrapper: ObjectValue ): UtExpression = selectArrayExpressionFromMemory(getStorageArrayField(wrapper.addr)) -} -// Arrays and lists have the only type parameter with index zero -private const val TYPE_PARAMETER_INDEX = 0 + override val selectOperationTypeIndex: Int + get() = 1 +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/CollectionIteratorWrappers.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/CollectionIteratorWrappers.kt new file mode 100644 index 0000000000..9eae85413f --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/CollectionIteratorWrappers.kt @@ -0,0 +1,109 @@ +package org.utbot.engine + +import org.utbot.engine.overrides.collections.UtArrayList.UtArrayListIterator +import org.utbot.engine.overrides.collections.UtArrayList.UtArrayListSimpleIterator +import org.utbot.engine.overrides.collections.UtHashSet.UtHashSetIterator +import org.utbot.engine.overrides.collections.UtLinkedList.UtReverseIterator +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.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtReferenceModel +import org.utbot.framework.plugin.api.util.fieldId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.methodId +import soot.SootClass +import soot.SootMethod +import kotlin.reflect.KClass +import kotlin.reflect.jvm.jvmName + +/** + * Abstract wrapper for iterator of [java.util.Collection]. + */ +abstract class CollectionIteratorWrapper(overriddenClass: KClass<*>) : BaseOverriddenWrapper(overriddenClass.jvmName) { + protected abstract val modelName: String + protected abstract val javaCollectionClassId: ClassId + protected abstract val iteratorMethodId: MethodId + protected abstract val iteratorClassId: ClassId + + override fun Traverser.overrideInvoke( + wrapper: ObjectValue, + method: SootMethod, + parameters: List + ): List? = null + + override fun value(resolver: Resolver, wrapper: ObjectValue): UtModel = resolver.run { + val addr = holder.concreteAddr(wrapper.addr) + val fieldModels = collectFieldModels(wrapper.addr, overriddenClass.type) + + val containerFieldId = overriddenClass.enclosingClassField + val containerFieldModel = fieldModels[containerFieldId] as UtReferenceModel + + val instantiationCall = UtExecutableCallModel( + instance = containerFieldModel, + executable = iteratorMethodId, + params = emptyList() + ) + + UtAssembleModel(addr, iteratorClassId, modelName, instantiationCall) + } +} + +class IteratorOfListWrapper : CollectionIteratorWrapper(UtArrayListSimpleIterator::class) { + override val modelName: String = "iteratorOfList" + override val javaCollectionClassId: ClassId = java.util.List::class.id + override val iteratorClassId: ClassId = java.util.Iterator::class.id + override val iteratorMethodId: MethodId = methodId( + classId = javaCollectionClassId, + name = "iterator", + returnType = iteratorClassId, + arguments = emptyArray() + ) +} + +class ListIteratorOfListWrapper : CollectionIteratorWrapper(UtArrayListIterator::class) { + override val modelName: String = "listIteratorOfList" + override val javaCollectionClassId: ClassId = java.util.List::class.id + override val iteratorClassId: ClassId = java.util.ListIterator::class.id + override val iteratorMethodId: MethodId = methodId( + classId = javaCollectionClassId, + name = "listIterator", + returnType = iteratorClassId, + arguments = emptyArray() + ) +} + +class IteratorOfSetWrapper : CollectionIteratorWrapper(UtHashSetIterator::class) { + override val modelName: String = "iteratorOfSet" + override val javaCollectionClassId: ClassId = java.util.Set::class.id + override val iteratorClassId: ClassId = java.util.Iterator::class.id + override val iteratorMethodId: MethodId = methodId( + classId = javaCollectionClassId, + name = "iterator", + returnType = iteratorClassId, + arguments = emptyArray() + ) +} + +class ReverseIteratorWrapper :CollectionIteratorWrapper(UtReverseIterator::class) { + override val modelName: String = "reverseIterator" + override val javaCollectionClassId: ClassId = java.util.Deque::class.id + override val iteratorClassId: ClassId = java.util.Iterator::class.id + override val iteratorMethodId: MethodId = methodId( + classId = javaCollectionClassId, + name = "descendingIterator", + returnType = iteratorClassId, + arguments = emptyArray() + ) +} + +internal val SootClass.enclosingClassField: FieldId + get() { + require(isInnerClass) { + "Cannot get field for enclosing class of non-inner class $this" + } + + return getFieldByName("this$0").fieldId + } 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 56c7e32373..914a2edc21 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/CollectionWrappers.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/CollectionWrappers.kt @@ -12,7 +12,9 @@ import org.utbot.engine.overrides.collections.UtLinkedListWithNullableCheck import org.utbot.engine.pc.UtAddrExpression import org.utbot.engine.pc.UtExpression import org.utbot.engine.pc.select +import org.utbot.engine.symbolic.SymbolicStateUpdate import org.utbot.engine.symbolic.asHardConstraint +import org.utbot.engine.types.TypeResolver import org.utbot.engine.z3.intValue import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.FieldId @@ -29,6 +31,7 @@ import org.utbot.framework.util.graph import org.utbot.framework.plugin.api.id import org.utbot.framework.plugin.api.util.booleanClassId import org.utbot.framework.plugin.api.util.constructorId +import org.utbot.framework.plugin.api.util.fieldId import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.methodId import org.utbot.framework.plugin.api.util.objectClassId @@ -166,13 +169,17 @@ abstract class BaseGenericStorageBasedContainerWrapper(containerClassName: Strin ): List? = when (method.signature) { UT_GENERIC_STORAGE_SET_EQUAL_GENERIC_TYPE_SIGNATURE -> { - val equalGenericTypeConstraint = typeRegistry - .eqGenericSingleTypeParameterConstraint(parameters[0].addr, wrapper.addr) - .asHardConstraint() + val (equalGenericTypeConstraint, memoryUpdate) = TypeResolver + .eqGenericSingleTypeParameterConstraint( + parameters[0].addr, + wrapper.addr, + memory.getAllGenericTypeInfo() + ) val methodResult = MethodResult( SymbolicSuccess(voidValue), - equalGenericTypeConstraint + equalGenericTypeConstraint.asHardConstraint(), + memoryUpdates = memoryUpdate ) listOf(methodResult) @@ -180,9 +187,16 @@ abstract class BaseGenericStorageBasedContainerWrapper(containerClassName: Strin UT_GENERIC_STORAGE_SET_GENERIC_TYPE_TO_TYPE_OF_VALUE_SIGNATURE -> { val valueTypeStorage = parameters[1].typeStorage - typeRegistry.saveObjectParameterTypeStorages(parameters[0].addr, listOf(valueTypeStorage)) + val memoryUpdate = TypeResolver.createGenericTypeInfoUpdate( + parameters[0].addr, + listOf(valueTypeStorage), + memory.getAllGenericTypeInfo() + ) - val methodResult = MethodResult(SymbolicSuccess(voidValue)) + val methodResult = MethodResult( + SymbolicSuccess(voidValue), + SymbolicStateUpdate(memoryUpdates = memoryUpdate) + ) listOf(methodResult) } @@ -300,23 +314,39 @@ class MapWrapper : BaseContainerWrapper(UtHashMap::class.qualifiedName!!) { parameters: List ): List? = when (method.signature) { - UT_GENERIC_STORAGE_SET_EQUAL_GENERIC_TYPE_SIGNATURE -> listOf( - MethodResult( - SymbolicSuccess(voidValue), - typeRegistry.eqGenericSingleTypeParameterConstraint(parameters[0].addr, wrapper.addr) - .asHardConstraint() + UT_GENERIC_STORAGE_SET_EQUAL_GENERIC_TYPE_SIGNATURE -> { + val (eqGenericSingleTypeParameterConstraint, memoryUpdate) = + TypeResolver.eqGenericSingleTypeParameterConstraint( + parameters[0].addr, + wrapper.addr, + memory.getAllGenericTypeInfo() + ) + + listOf( + MethodResult( + SymbolicSuccess(voidValue), + eqGenericSingleTypeParameterConstraint.asHardConstraint(), + memoryUpdates = memoryUpdate + ) ) - ) - UT_GENERIC_ASSOCIATIVE_SET_EQUAL_GENERIC_TYPE_SIGNATURE -> listOf( - MethodResult( - SymbolicSuccess(voidValue), - typeRegistry.eqGenericTypeParametersConstraint( + } + UT_GENERIC_ASSOCIATIVE_SET_EQUAL_GENERIC_TYPE_SIGNATURE -> { + val (eqGenericTypeParametersConstraint, memoryUpdate) = + TypeResolver.eqGenericTypeParametersConstraint( parameters[0].addr, wrapper.addr, - parameterSize = 2 - ).asHardConstraint() + parameterSize = 2, + memory.getAllGenericTypeInfo() + ) + + listOf( + MethodResult( + SymbolicSuccess(voidValue), + eqGenericTypeParametersConstraint.asHardConstraint(), + memoryUpdates = memoryUpdate + ) ) - ) + } else -> null } @@ -418,6 +448,8 @@ val ARRAY_LIST_TYPE: RefType get() = Scene.v().getSootClass(java.util.ArrayList::class.java.canonicalName).type val LINKED_LIST_TYPE: RefType get() = Scene.v().getSootClass(java.util.LinkedList::class.java.canonicalName).type +val ARRAY_DEQUE_TYPE: RefType + get() = Scene.v().getSootClass(java.util.ArrayDeque::class.java.canonicalName).type val LINKED_HASH_SET_TYPE: RefType get() = Scene.v().getSootClass(java.util.LinkedHashSet::class.java.canonicalName).type @@ -429,6 +461,11 @@ val LINKED_HASH_MAP_TYPE: RefType val HASH_MAP_TYPE: RefType get() = Scene.v().getSootClass(java.util.HashMap::class.java.canonicalName).type +val ITERATOR_TYPE: RefType + get() = Scene.v().getSootClass(java.util.Iterator::class.java.canonicalName).type +val LIST_ITERATOR_TYPE: RefType + get() = Scene.v().getSootClass(java.util.ListIterator::class.java.canonicalName).type + val STREAM_TYPE: RefType get() = Scene.v().getSootClass(java.util.stream.Stream::class.java.canonicalName).type diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ConstructedSootMethods.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ConstructedSootMethods.kt index 1bc3f9aa5e..67a5e89e45 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ConstructedSootMethods.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ConstructedSootMethods.kt @@ -1,13 +1,18 @@ package org.utbot.engine +import org.utbot.engine.types.OBJECT_TYPE +import org.utbot.engine.types.STRING_TYPE import soot.ArrayType import soot.IntType import soot.PrimType import soot.RefType +import soot.Scene +import soot.SootClass import soot.SootMethod import soot.Type import soot.Unit import soot.VoidType +import soot.jimple.StringConstant import soot.jimple.internal.JArrayRef import soot.jimple.internal.JAssignStmt import soot.jimple.internal.JNewMultiArrayExpr @@ -169,4 +174,117 @@ fun unfoldMultiArrayExpr(assignStmt: JAssignStmt): ExceptionalUnitGraph { createSootMethod(methodName, jimpleLocalsForSizes.map { it.type }, multiArray.type, sootClass, graphBody) return ExceptionalUnitGraph(graphBody) +} + + +fun makeSootConcat(declaringClass: SootClass, recipe: String, paramTypes: List, constants: List): SootMethod { + val paramsHashcode = paramTypes.hashCode() + val constantsHashcode = constants.hashCode() + val name = "utbot\$concatenateStringWithRecipe<$recipe>AndParams<$paramsHashcode>AndConstants<$constantsHashcode>" + + + // check if it is already defined + val cachedMethod = declaringClass.getMethodByNameUnsafe(name) + if (cachedMethod != null) { + return cachedMethod + } + + var objectCounter = 0 + + val units = mutableListOf() + val locals = mutableSetOf() + + + // initialize parameter locals + val parameterLocals = paramTypes.map { + JimpleLocal("r${objectCounter++}", it) + } + locals += parameterLocals + + + val parameterRefs = paramTypes.mapIndexed { idx, type -> parameterRef(type, idx) } + val identityStmts = parameterLocals.zip(parameterRefs) { local, ref -> identityStmt(local, ref) } + units += identityStmts + + + // initialize string builder + val sbSootClass = Scene.v().getSootClass("java.lang.StringBuilder") + + + val sb = JimpleLocal("\$r${objectCounter++}", sbSootClass.type) + locals += sb + + val newSbExpr = newNewExpr(sbSootClass.type) + units += assignStmt(sb, newSbExpr) + val initSootMethod = sbSootClass.getMethod("", emptyList()) + val initSbExpr = initSootMethod.toSpecialInvokeExpr(sb) + units += initSbExpr.toInvokeStmt() + + + var paramPointer = 0 // pointer to dynamic parameter + var constantPointer = 0 // pointer to constant list + var delayedString = "" // string which is build of sequent values from [constants] + + val appendStringSootMethod = sbSootClass.getMethod("append", listOf(STRING_TYPE), sbSootClass.type) + + // helper function for appending constants to delayedString + fun appendDelayedStringIfNotEmpty() { + if (delayedString.isEmpty()) { + return + } + + val invokeStringBuilderAppendStmt = appendStringSootMethod.toVirtualInvokeExpr(sb, StringConstant.v(delayedString)) + units += invokeStringBuilderAppendStmt.toInvokeStmt() + + delayedString = "" + } + + // recipe parsing and building the result string + for (c in recipe) { + when (c) { + '\u0001' -> { + appendDelayedStringIfNotEmpty() + + val local = parameterLocals[paramPointer++] + val type = local.type + + val appendSootMethod = sbSootClass.getMethodUnsafe("append", listOf(type), sbSootClass.type) + ?: sbSootClass.getMethod("append", listOf(OBJECT_TYPE), sbSootClass.type) + + val invokeStringBuilderAppendStmt = appendSootMethod.toVirtualInvokeExpr(sb, local) + units += invokeStringBuilderAppendStmt.toInvokeStmt() + } + '\u0002' -> { + appendDelayedStringIfNotEmpty() + + val const = constants[constantPointer++] + + val stringBuilderAppendExpr = appendStringSootMethod.toVirtualInvokeExpr(sb, StringConstant.v(const)) + units += stringBuilderAppendExpr.toInvokeStmt() + } + else -> { + delayedString += c + } + } + } + appendDelayedStringIfNotEmpty() + + // receiving result + val toStringSootMethod = sbSootClass.getMethodByName("toString") + + val result = JimpleLocal("\$r${objectCounter++}", STRING_TYPE) + locals += result + + val stringBuilderToStringExpr = toStringSootMethod.toVirtualInvokeExpr(sb) + val assignStmt = assignStmt(result, stringBuilderToStringExpr) + units += assignStmt + + val returnStmt = returnStatement(result) + units += returnStmt + + + val body = units.toGraphBody() + body.locals.addAll(locals) + + return createSootMethod(name, paramTypes, STRING_TYPE, declaringClass, body) } \ No newline at end of file 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 b4e09996af..46e7a49792 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/DataClasses.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/DataClasses.kt @@ -1,16 +1,19 @@ package org.utbot.engine -import org.utbot.common.WorkaroundReason -import org.utbot.common.workaround -import org.utbot.engine.TypeRegistry.Companion.objectTypeStorage +import org.utbot.api.mock.UtMock +import org.utbot.engine.overrides.UtOverrideMock +import org.utbot.engine.types.TypeRegistry.Companion.objectTypeStorage import org.utbot.engine.pc.UtAddrExpression import org.utbot.engine.pc.UtBoolExpression +import org.utbot.engine.pc.UtFalse import org.utbot.engine.pc.UtInstanceOfExpression 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.state.ExecutionState import org.utbot.engine.symbolic.* +import org.utbot.engine.types.TypeResolver import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.UtInstrumentation import soot.RefType @@ -60,11 +63,11 @@ fun explicitThrown(exception: Throwable, addr: UtAddrExpression, inNestedMethod: /** * Implicitly thrown exception. There are no difference if it happens in nested call or not. */ -fun implicitThrown(exception: Throwable, addr: UtAddrExpression, inNestedMethod: Boolean) = - SymbolicFailure(symbolic(exception, addr), exception, explicit = false, inNestedMethod = inNestedMethod) +fun implicitThrown(throwable: Throwable, addr: UtAddrExpression, inNestedMethod: Boolean) = + SymbolicFailure(symbolic(throwable, addr), throwable, explicit = false, inNestedMethod = inNestedMethod) -private fun symbolic(exception: Throwable, addr: UtAddrExpression) = - objectValue(Scene.v().getRefType(exception.javaClass.canonicalName), addr, ThrowableWrapper(exception)) +private fun symbolic(throwable: Throwable, addr: UtAddrExpression) = + objectValue(Scene.v().getRefType(throwable.javaClass.canonicalName), addr, ThrowableWrapper(throwable)) private fun extractConcrete(symbolic: SymbolicValue) = (symbolic.asWrapperOrNull as? ThrowableWrapper)?.throwable @@ -83,22 +86,25 @@ inline fun SymbolicFailure.fold( data class Parameter(private val localVariable: LocalVariable, private val type: Type, val value: SymbolicValue) /** - * Keeps most common type and possible types, to resolve types in uncertain situations, like virtual invokes. + * Contains type information about some object: set of its [possibleConcreteTypes] and their [leastCommonType]. + * Note that in some situations [leastCommonType] will not present in [possibleConcreteTypes] set, and + * it should not be used to encode this type information into a solver. * * Note: [leastCommonType] might be an interface or abstract type in opposite to the [possibleConcreteTypes] * that **usually** contains only concrete types (so-called appropriate). The only way to create [TypeStorage] with - * inappropriate possibleType is to create it using constructor with the only type. + * inappropriate possibleType is to create it using static methods [constructTypeStorageUnsafe] and + * [constructTypeStorageWithSingleType]. + * + * The right way to create an instance of TypeStorage is to use methods provided by [TypeResolver], e.g., + * [TypeResolver.constructTypeStorage]. * * @see isAppropriate + * @see TypeResolver.constructTypeStorage */ -data class TypeStorage(val leastCommonType: Type, val possibleConcreteTypes: Set) { +class TypeStorage private constructor(val leastCommonType: Type, val possibleConcreteTypes: Set) { private val hashCode = Objects.hash(leastCommonType, possibleConcreteTypes) - /** - * Construct a type storage with some type. In this case [possibleConcreteTypes] might contains - * abstract class or interface. Usually it means such typeStorage represents wrapper object type. - */ - constructor(concreteType: Type) : this(concreteType, setOf(concreteType)) + private constructor(concreteType: Type) : this(concreteType, setOf(concreteType)) fun isObjectTypeStorage(): Boolean = possibleConcreteTypes.size == objectTypeStorage.possibleConcreteTypes.size @@ -121,6 +127,32 @@ data class TypeStorage(val leastCommonType: Type, val possibleConcreteTypes: Set } else { "(leastCommonType=$leastCommonType, ${possibleConcreteTypes.size} possibleTypes=${possibleConcreteTypes.take(10)})" } + + operator fun component1(): Type = leastCommonType + operator fun component2(): Set = possibleConcreteTypes + + companion object { + /** + * Constructs a type storage with particular leastCommonType and set of possibleConcreteTypes. + * This method doesn't give any guarantee on correctness of the constructed type storage and + * should not be used directly except the situations where you want to create abnormal type storage, + * for example, a one that contains interfaces in [possibleConcreteTypes]. + * + * In regular cases you should use [TypeResolver.constructTypeStorage] method instead. + */ + fun constructTypeStorageUnsafe( + leastCommonType: Type, + possibleConcreteTypes: Set + ): TypeStorage = TypeStorage(leastCommonType, possibleConcreteTypes) + + /** + * Constructs a type storage with some type. In this case [possibleConcreteTypes] might contain + * an abstract class or an interface. Usually it means such typeStorage represents wrapper object type. + */ + fun constructTypeStorageWithSingleType( + leastCommonType: Type + ): TypeStorage = TypeStorage(leastCommonType) + } } sealed class InvokeResult @@ -129,8 +161,6 @@ data class MethodResult( val symbolicResult: SymbolicResult, val symbolicStateUpdate: SymbolicStateUpdate = SymbolicStateUpdate() ) : InvokeResult() { - val memoryUpdates by symbolicStateUpdate::memoryUpdates - constructor( symbolicResult: SymbolicResult, hardConstraints: HardConstraint = emptyHardConstraint(), @@ -281,7 +311,16 @@ data class TypeConstraint( /** * Returns a conjunction of the [isConstraint] and [correctnessConstraint]. Suitable for an object creation. */ - fun all(): UtBoolExpression = mkAnd(isOrNullConstraint(), correctnessConstraint) + fun all(): UtBoolExpression { + // There is no need in constraint for UtMock and UtOverrideMock instances + val sootClass = (isConstraint.type as? RefType)?.sootClass + + if (sootClass == utMockClass || sootClass == utOverrideMockClass) { + return UtTrue + } + + return mkAnd(isOrNullConstraint(), correctnessConstraint) + } /** * Returns a condition that either the object is an instance of the types in [isConstraint], or it is null. @@ -293,7 +332,13 @@ data class TypeConstraint( * For example, it is suitable for instanceof checks or negation of equality with some types. * NOTE: for Object we always return UtTrue. */ - fun isConstraint(): UtBoolExpression = if (isConstraint.typeStorage.isObjectTypeStorage()) UtTrue else isConstraint + fun isConstraint(): UtBoolExpression { + if (isConstraint.typeStorage.possibleConcreteTypes.isEmpty()) { + return UtFalse + } + + return if (isConstraint.typeStorage.isObjectTypeStorage()) UtTrue else isConstraint + } override fun hashCode(): Int = this.hashcode @@ -318,3 +363,6 @@ data class TypeConstraint( * should be initialized. We don't need to initialize fields that are not accessed in the method being tested. */ data class InstanceFieldReadOperation(val addr: UtAddrExpression, val fieldId: FieldId) + +private val utMockClassName: String = UtMock::class.java.name +private val utOverrideMockClassName: String = UtOverrideMock::class.java.name \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/DynamicInvokeResolver.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/DynamicInvokeResolver.kt new file mode 100644 index 0000000000..4c07e8d485 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/DynamicInvokeResolver.kt @@ -0,0 +1,114 @@ +package org.utbot.engine + +import soot.SootClass +import soot.Type +import soot.jimple.StringConstant +import soot.jimple.internal.JDynamicInvokeExpr + + +/** + * Map of supported boostrap function names to their resolvers + */ +private val defaultProcessors = mapOf( + // referencing by a name instead of a signature, because we believe, that all bootstrap methods have unique names + StringMakeConcatResolver.MAKE_CONCAT_METHOD_NAME to StringMakeConcatResolver, + StringMakeConcatResolver.MAKE_CONCAT_WITH_CONSTANTS_NAME to StringMakeConcatResolver +) + +interface DynamicInvokeResolver { + /** + * @return a successfully resolved [Invocation] or `null`, if it can't be resolved with this [DynamicInvokeResolver] + */ + context(Traverser) + fun TraversalContext.resolveDynamicInvoke(invokeExpr: JDynamicInvokeExpr): Invocation? +} + +/** + * Composes several [DynamicInvokeResolver]s into a single [DynamicInvokeResolver] based on bootstrap method names. + */ +class DelegatingDynamicInvokeResolver( + private val bootstrapMethodToProcessor: Map = defaultProcessors +) : DynamicInvokeResolver { + context(Traverser) + override fun TraversalContext.resolveDynamicInvoke(invokeExpr: JDynamicInvokeExpr): Invocation? { + val processor = bootstrapMethodToProcessor[invokeExpr.bootstrapMethod.name] ?: return null + return with(processor) { resolveDynamicInvoke(invokeExpr) } + } + +} + +/** + * Implements the logic of [java.lang.invoke.StringConcatFactory]. + * + * This is useful when analyzing only Java 9+ bytecode. + */ +object StringMakeConcatResolver : DynamicInvokeResolver { + const val STRING_CONCAT_LIBRARY_NAME = "java.lang.invoke.StringConcatFactory" + const val MAKE_CONCAT_METHOD_NAME = "makeConcat" + const val MAKE_CONCAT_WITH_CONSTANTS_NAME = "makeConcatWithConstants" + + /** + * Implements the logic of [java.lang.invoke.StringConcatFactory.makeConcat] and + * [java.lang.invoke.StringConcatFactory.makeConcatWithConstants] in a symbolic way. + * + * - Generates [soot.SootMethod] performing string concatenation if it has not generated yet + * - Links it + * + * Check out [java.lang.invoke.StringConcatFactory] documentation for more details. + * + * @return [Invocation] with a single generated [soot.SootMethod], which represents a concatenating function. + */ + context(Traverser) + override fun TraversalContext.resolveDynamicInvoke(invokeExpr: JDynamicInvokeExpr): Invocation { + val bootstrapMethod = invokeExpr.bootstrapMethod + require(bootstrapMethod.declaringClass.name == STRING_CONCAT_LIBRARY_NAME) + + val bootstrapArguments = invokeExpr.bootstrapArgs.map { arg -> + requireNotNull((arg as? StringConstant)?.value) { "StringConstant expected, but got ${arg::class.java}"} + } + + val (recipe, constants) = when (bootstrapMethod.name) { + MAKE_CONCAT_METHOD_NAME -> { + "\u0001".repeat(invokeExpr.args.size) to emptyList() + } + MAKE_CONCAT_WITH_CONSTANTS_NAME -> { + val recipe = requireNotNull(bootstrapArguments.firstOrNull()) { "At least one bootstrap argument expected" } + recipe to bootstrapArguments.drop(1) + } + else -> error("Unknown bootstrap method for string concatenation!") + } + + val declaringClass = environment.method.declaringClass + val dynamicParameterTypes = invokeExpr.methodRef.parameterTypes + + val parameters = resolveParameters(invokeExpr.args, dynamicParameterTypes) + return makeInvocation(declaringClass, recipe, dynamicParameterTypes, constants, parameters) + } + + private fun makeInvocation( + declaringClass: SootClass, + recipe: String, + dynamicParameterTypes: MutableList, + constants: List, + parameters: List + ): Invocation { + val sootMethod = makeSootConcat( + declaringClass, + recipe, + dynamicParameterTypes, + constants + ) + + val invocationTarget = InvocationTarget( + instance = null, + sootMethod + ) + + return Invocation( + instance = null, + sootMethod, + parameters, + invocationTarget + ) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ExecutionState.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ExecutionState.kt deleted file mode 100644 index 998ab1bbb1..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ExecutionState.kt +++ /dev/null @@ -1,436 +0,0 @@ -package org.utbot.engine - -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.PersistentMap -import kotlinx.collections.immutable.PersistentSet -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 -import org.utbot.engine.symbolic.Assumption -import org.utbot.framework.plugin.api.UtSymbolicExecution - -const val RETURN_DECISION_NUM = -1 -const val CALL_DECISION_NUM = -2 - -data class Edge(val src: Stmt, val dst: Stmt, val decisionNum: Int) { - private val hashCode: Int = Objects.hash(src, dst, decisionNum) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Edge - - if (src != other.src) return false - if (dst != other.dst) return false - if (decisionNum != other.decisionNum) return false - - return true - } - - override fun hashCode(): Int = hashCode -} - -/** - * Possible state types. Engine matches on them and processes differently. - * - * [INTERMEDIATE] is a label for an intermediate state which is suitable for further symbolic analysis. - * - * [TERMINAL] is a label for a terminal state from which we might (or might not) execute concretely and construct - * [UtExecution]. This state represents the final state of the program execution, that is a throw or return from the outer - * method. - * - * [CONCRETE] is a label for a state which is not suitable for further symbolic analysis, and it is also not a terminal - * state. Such states are only suitable for a concrete execution and may result from [Assumption]s. - */ -enum class StateLabel { - INTERMEDIATE, - TERMINAL, - CONCRETE -} - -/** - * The stack element of the [ExecutionState]. - * Contains properties, that are suitable for specified method in call stack. - * - * @param doesntThrow if true, then engine should drop states with throwing exceptions. - * @param localVariableMemory the local memory associated with the current stack element. - */ -data class ExecutionStackElement( - val caller: Stmt?, - val localVariableMemory: LocalVariableMemory = LocalVariableMemory(), - val parameters: MutableList = mutableListOf(), - val inputArguments: ArrayDeque = ArrayDeque(), - val doesntThrow: Boolean = false, - val method: SootMethod, -) { - fun update(memoryUpdate: LocalMemoryUpdate, doesntThrow: Boolean = this.doesntThrow) = - 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 - * inside a cycle. To improve performance it is a persistent map using state's hashcode to imitate an identity hashmap. - * - * @param symbolicState the current symbolic state. - */ -data class ExecutionState( - val stmt: Stmt, - val symbolicState: SymbolicState, - val executionStack: PersistentList, - val path: PersistentList = persistentListOf(), - val visitedStatementsHashesToCountInPath: PersistentMap = persistentHashMapOf(), - val decisionPath: PersistentList = persistentListOf(0), - val edges: PersistentSet = persistentHashSetOf(), - val stmts: PersistentMap = persistentHashMapOf(), - val pathLength: Int = 0, - val lastEdge: Edge? = null, - val lastMethod: SootMethod? = null, - val methodResult: MethodResult? = null, - val exception: SymbolicFailure? = null, - val label: StateLabel = StateLabel.INTERMEDIATE, - 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 - get() = executionStack.last().localVariableMemory - - val inputArguments - get() = executionStack.last().inputArguments - - val parameters - get() = executionStack.last().parameters - - /** - * Retrieves MUT parameters. - */ - val methodUnderTestParameters - get() = executionStack.firstOrNull()?.parameters?.map { it.value } - ?: error("Cannot retrieve MUT parameters from empty execution stack") - - val isThrowException: Boolean - get() = (lastEdge?.decisionNum ?: 0) < CALL_DECISION_NUM - - val isInsideStaticInitializer - get() = executionStack.any { it.method.isStaticInitializer } - - fun createExceptionState( - exception: SymbolicFailure, - update: SymbolicStateUpdate - ): ExecutionState { - val last = executionStack.last() - // go to negative indexing below CALL_DECISION_NUM for exceptions - val edge = Edge(stmt, stmt, CALL_DECISION_NUM - (++outgoingEdges)) - return ExecutionState( - stmt = stmt, - symbolicState = symbolicState + update, - executionStack = executionStack.set(executionStack.lastIndex, last.update(update.localMemoryUpdates)), - path = path, - visitedStatementsHashesToCountInPath = visitedStatementsHashesToCountInPath, - decisionPath = decisionPath + edge.decisionNum, - edges = edges + edge, - stmts = stmts, - pathLength = pathLength + 1, - lastEdge = edge, - lastMethod = executionStack.last().method, - exception = exception, - label = label, - stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) - ) - } - - fun pop(methodResult: MethodResult): ExecutionState { - val caller = executionStack.last().caller!! - val edge = Edge(stmt, caller, RETURN_DECISION_NUM) - - val stmtHashcode = stmt.hashCode() - val stmtCountInPath = (visitedStatementsHashesToCountInPath[stmtHashcode] ?: 0) + 1 - - return ExecutionState( - stmt = caller, - symbolicState = symbolicState, - executionStack = executionStack.removeAt(executionStack.lastIndex), - path = path + stmt, - 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, - label = label, - stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) - ) - } - - fun push( - stmt: Stmt, - inputArguments: ArrayDeque, - update: SymbolicStateUpdate, - method: SootMethod - ): ExecutionState { - val edge = Edge(this.stmt, stmt, CALL_DECISION_NUM) - val stackElement = ExecutionStackElement( - this.stmt, - localVariableMemory = localVariableMemory.memoryForNestedMethod().update(update.localMemoryUpdates), - inputArguments = inputArguments, - method = method, - doesntThrow = executionStack.last().doesntThrow - ) - outgoingEdges++ - - val stmtHashCode = this.stmt.hashCode() - val stmtCountInPath = (visitedStatementsHashesToCountInPath[stmtHashCode] ?: 0) + 1 - - return ExecutionState( - stmt = stmt, - symbolicState = symbolicState.stateForNestedMethod() + update, - executionStack = executionStack + stackElement, - path = path + this.stmt, - 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, - label = label, - stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) - ) - } - - fun update( - stateUpdate: SymbolicStateUpdate - ): ExecutionState { - val last = executionStack.last() - val stackElement = last.update(stateUpdate.localMemoryUpdates) - return copy( - symbolicState = symbolicState + stateUpdate, - executionStack = executionStack.set(executionStack.lastIndex, stackElement) - ) - } - - fun update( - edge: Edge, - symbolicStateUpdate: SymbolicStateUpdate, - doesntThrow: Boolean, - ): ExecutionState { - val last = executionStack.last() - val stackElement = last.update( - symbolicStateUpdate.localMemoryUpdates, - last.doesntThrow || doesntThrow - ) - outgoingEdges++ - - val stmtHashCode = stmt.hashCode() - val stmtCountInPath = (visitedStatementsHashesToCountInPath[stmtHashCode] ?: 0) + 1 - - return ExecutionState( - stmt = edge.dst, - symbolicState = symbolicState + symbolicStateUpdate, - executionStack = executionStack.set(executionStack.lastIndex, stackElement), - path = path + stmt, - 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, - label = label, - stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) - ) - } - - /** - * Tell to solver that states with status [UtSolverStatusUNDEFINED] can be created from current state. - * - * Note: Solver optimize cloning respect this flag. - */ - fun expectUndefined() { - solver.expectUndefined = true - } - - fun withLabel(newLabel: StateLabel) = copy(label = newLabel) - - override fun close() { - solver.close() - } - - - /** - * Collects full statement path from method entry point to current statement, including current statement. - * - * Each step contains statement, call depth for nested calls (returns belong to called method) and decision. - * Decision for current statement is zero. - * - * Note: calculates depth wrongly for thrown exception, check SAT-811, SAT-812 - * TODO: fix SAT-811, SAT-812 - */ - fun fullPath(): List { - var depth = 0 - val path = path.zip( - decisionPath.subList(1, decisionPath.size) - ).map { (stmt, decision) -> - val stepDepth = when (decision) { - CALL_DECISION_NUM -> depth++ - RETURN_DECISION_NUM -> depth-- - else -> depth - } - Step(stmt, stepDepth, decision) - } - return path + Step(stmt, depth, 0) - } - - /** - * Prettifies full statement path for logging. - * - * Note: marks return statements with *depth-1* to pair with call statement. - */ - fun prettifiedPathLog(): String { - val path = fullPath() - 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, ' ') - } - "$prefix$stmt" - } - - 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/ExecutionStateListener.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ExecutionStateListener.kt new file mode 100644 index 0000000000..9b92984a1b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ExecutionStateListener.kt @@ -0,0 +1,10 @@ +package org.utbot.engine + +import org.utbot.engine.state.ExecutionState + +/** + * [UtBotSymbolicEngine] will fire an event every time it traverses new [ExecutionState]. + */ +fun interface ExecutionStateListener { + fun visit(graph: InterProceduralUnitGraph, state: ExecutionState) +} \ 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 3e29087cf6..6c54d304d9 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Extensions.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Extensions.kt @@ -14,7 +14,6 @@ import org.utbot.engine.pc.UtFp32Sort import org.utbot.engine.pc.UtFp64Sort import org.utbot.engine.pc.UtIntSort import org.utbot.engine.pc.UtLongSort -import org.utbot.engine.pc.UtSeqSort import org.utbot.engine.pc.UtShortSort import org.utbot.engine.pc.UtSolverStatusKind import org.utbot.engine.pc.UtSolverStatusSAT @@ -29,8 +28,8 @@ import org.utbot.engine.pc.mkFloat import org.utbot.engine.pc.mkInt 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.engine.state.ExecutionState import org.utbot.framework.UtSettings.checkNpeInNestedMethods import org.utbot.framework.UtSettings.checkNpeInNestedNotPrivateMethods import org.utbot.framework.plugin.api.FieldId @@ -46,11 +45,6 @@ import soot.SootField import soot.SootMethod import soot.Type import soot.Value -import soot.jimple.Expr -import soot.jimple.InvokeExpr -import soot.jimple.JimpleBody -import soot.jimple.StaticFieldRef -import soot.jimple.Stmt import soot.jimple.internal.JDynamicInvokeExpr import soot.jimple.internal.JIdentityStmt import soot.jimple.internal.JInterfaceInvokeExpr @@ -68,6 +62,9 @@ import java.util.Queue import java.util.concurrent.ConcurrentHashMap import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.persistentHashMapOf +import org.utbot.engine.types.OBJECT_TYPE +import org.utbot.framework.plugin.api.util.enumConstants +import soot.jimple.* val JIdentityStmt.lines: String get() = tags.joinToString { "$it" } @@ -84,6 +81,15 @@ fun Expr.isInvokeExpr() = this is JDynamicInvokeExpr || this is JVirtualInvokeExpr || this is JSpecialInvokeExpr +fun InvokeExpr.baseOrNull(): Value? = + when (this) { + is StaticInvokeExpr -> null + is SpecialInvokeExpr -> base + is VirtualInvokeExpr -> base + is InterfaceInvokeExpr -> base + else -> null + } + val SootMethod.pureJavaSignature get() = bytecodeSignature.substringAfter(' ').dropLast(1) @@ -177,10 +183,10 @@ val SootClass.isAppropriate get() = !isInappropriate /** - * Returns true if the class is abstract, interface, local or if the class has UtClassMock annotation, false otherwise. + * Returns true if the class is abstract, interface, or if the class has UtClassMock annotation, false otherwise. */ val SootClass.isInappropriate - get() = isAbstract || isInterface || isLocal || findMockAnnotationOrNull != null + get() = isAbstract || isInterface || findMockAnnotationOrNull != null private val isLocalRegex = ".*\\$\\d+[\\p{L}\\p{M}0-9][\\p{L}\\p{M}0-9]*".toRegex() @@ -284,8 +290,8 @@ val JimpleLocal.variable: LocalVariable val Type.defaultSymValue: UtExpression get() = toSort().defaultValue -val SootField.fieldId: FieldId - get() = FieldId(declaringClass.id, name) +val SootField.isEnumConstant: Boolean + get() = name in declaringClass.id.enumConstants.orEmpty().map { enum -> enum.name } val UtSort.defaultValue: UtExpression get() = when (this) { @@ -297,8 +303,6 @@ val UtSort.defaultValue: UtExpression UtFp32Sort -> mkFloat(0f) UtFp64Sort -> mkDouble(0.0) UtBoolSort -> mkBool(false) - // empty string because we want to have a default value of the same sort as the items stored in the strings array - UtSeqSort -> mkString("") is UtArraySort -> if (itemSort is UtArraySort) nullObjectAddr else mkArrayWithConst(this, itemSort.defaultValue) else -> nullObjectAddr } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/Hierarchy.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/Hierarchy.kt index c610ab822c..bc70375df3 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Hierarchy.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Hierarchy.kt @@ -1,5 +1,7 @@ package org.utbot.engine +import org.utbot.engine.types.OBJECT_TYPE +import org.utbot.engine.types.TypeRegistry import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.id import soot.RefType @@ -30,7 +32,19 @@ class Hierarchy(private val typeRegistry: TypeRegistry) { val realType = typeRegistry.findRealType(type) as RefType val realFieldDeclaringType = typeRegistry.findRealType(field.declaringClass.type) as RefType - if (realFieldDeclaringType.sootClass !in ancestors(realType.sootClass.id)) { + // java.lang.Thread class has package-private fields, that can be used outside the class. + // Since wrapper UtThread does not inherit java.lang.Thread, we cannot use this inheritance condition only. + // The possible presence of hidden field is not important here - we just need + // to know whether we have at least one such field. + + // NOTE: we cannot use `getFieldByNameUnsafe` here because of possible hidden fields presence, and we also + // cannot use `ClassId::hasField` here because it use classloader to check it but we cannot load our wrappers. + // Also, we cannot `getFieldUnsafe` by signature of field, because, for example, `threadLocals` field in `UtThread` + // is declared with `Object` type since its real type is package-private. + val realTypeHasFieldByName = realType.sootClass.fields.any { it.name == field.name } + val realTypeIsInheritor = realFieldDeclaringType.sootClass in ancestors(realType.sootClass.id) + + if (!realTypeIsInheritor && !realTypeHasFieldByName) { error("No such field ${field.subSignature} found in ${realType.sootClass.name}") } return ChunkId("$realFieldDeclaringType", field.name) @@ -53,14 +67,20 @@ class Hierarchy(private val typeRegistry: TypeRegistry) { private fun findAncestors(id: ClassId) = with(Scene.v().getSootClass(id.name)) { - if (this.isInterface) { - Scene.v().activeHierarchy.getSuperinterfacesOfIncluding(this) + val superClasses = mutableListOf() + val superInterfaces = mutableListOf() + + if (isInterface) { + superClasses += OBJECT_TYPE.sootClass + superInterfaces += Scene.v().activeHierarchy.getSuperinterfacesOfIncluding(this) } else { - Scene.v().activeHierarchy.getSuperclassesOfIncluding(this) + - this.interfaces.flatMap { - Scene.v().activeHierarchy.getSuperinterfacesOfIncluding(it) - } + superClasses += Scene.v().activeHierarchy.getSuperclassesOfIncluding(this) + superInterfaces += superClasses + .flatMap { it.interfaces } + .flatMap { Scene.v().activeHierarchy.getSuperinterfacesOfIncluding(it) } } + + superClasses + superInterfaces } private fun findInheritors(id: ClassId) = 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 ff533b1085..c732463d37 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/InterProceduralUnitGraph.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/InterProceduralUnitGraph.kt @@ -1,6 +1,9 @@ package org.utbot.engine import org.utbot.engine.selectors.strategies.TraverseGraphStatistics +import org.utbot.engine.state.CALL_DECISION_NUM +import org.utbot.engine.state.Edge +import org.utbot.engine.state.ExecutionState import soot.SootClass import soot.SootMethod import soot.jimple.Stmt diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/JimpleCreation.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/JimpleCreation.kt index 4c7887eab9..791d78f55d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/JimpleCreation.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/JimpleCreation.kt @@ -1,5 +1,8 @@ package org.utbot.engine +import soot.Local +import soot.Modifier +import soot.RefType import soot.SootClass import soot.SootMethod import soot.Type @@ -17,13 +20,20 @@ import soot.jimple.InvokeStmt import soot.jimple.Jimple import soot.jimple.JimpleBody import soot.jimple.NewArrayExpr +import soot.jimple.NewExpr import soot.jimple.ParameterRef import soot.jimple.ReturnStmt import soot.jimple.ReturnVoidStmt +import soot.jimple.SpecialInvokeExpr import soot.jimple.StaticInvokeExpr +import soot.jimple.VirtualInvokeExpr fun SootMethod.toStaticInvokeExpr(): StaticInvokeExpr = Jimple.v().newStaticInvokeExpr(this.makeRef()) +fun SootMethod.toVirtualInvokeExpr(local: Local, vararg values: Value): VirtualInvokeExpr = Jimple.v().newVirtualInvokeExpr(local, this.makeRef(), *values) + +fun SootMethod.toSpecialInvokeExpr(local: Local, vararg values: Value): SpecialInvokeExpr = Jimple.v().newSpecialInvokeExpr(local, this.makeRef(), *values) + fun InvokeExpr.toInvokeStmt(): InvokeStmt = Jimple.v().newInvokeStmt(this) fun returnVoidStatement(): ReturnVoidStmt = Jimple.v().newReturnVoidStmt() @@ -34,6 +44,8 @@ fun parameterRef(type: Type, number: Int): ParameterRef = Jimple.v().newParamete fun identityStmt(local: Value, identityRef: Value): IdentityStmt = Jimple.v().newIdentityStmt(local, identityRef) +fun newNewExpr(type: RefType): NewExpr = Jimple.v().newNewExpr(type) + fun newArrayExpr(type: Type, size: Value): NewArrayExpr = Jimple.v().newNewArrayExpr(type, size) fun assignStmt(variable: Value, rValue: Value): AssignStmt = Jimple.v().newAssignStmt(variable, rValue) @@ -60,11 +72,10 @@ fun createSootMethod( argsTypes: List, returnType: Type, declaringClass: SootClass, - graphBody: JimpleBody -) = SootMethod(name, argsTypes, returnType) + graphBody: JimpleBody, + isStatic: Boolean = true +) = SootMethod(name, argsTypes, returnType, if (isStatic) Modifier.STATIC else 0) .also { - it.declaringClass = declaringClass declaringClass.addMethod(it) - graphBody.method = it it.activeBody = graphBody } \ No newline at end of file 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 8edee10242..8aca0de6cf 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Memory.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Memory.kt @@ -1,50 +1,33 @@ package org.utbot.engine -import com.google.common.collect.HashBiMap -import org.utbot.common.WorkaroundReason.MAKE_SYMBOLIC -import org.utbot.common.workaround import org.utbot.engine.MemoryState.CURRENT import org.utbot.engine.MemoryState.INITIAL import org.utbot.engine.MemoryState.STATIC_INITIAL -import org.utbot.engine.overrides.strings.UtNativeString -import org.utbot.engine.pc.RewritingVisitor import org.utbot.engine.pc.UtAddrExpression import org.utbot.engine.pc.UtAddrSort import org.utbot.engine.pc.UtArrayExpressionBase import org.utbot.engine.pc.UtArraySelectExpression import org.utbot.engine.pc.UtArraySort -import org.utbot.engine.pc.UtBoolExpression import org.utbot.engine.pc.UtBoolSort import org.utbot.engine.pc.UtConstArrayExpression -import org.utbot.engine.pc.UtEqGenericTypeParametersExpression import org.utbot.engine.pc.UtExpression import org.utbot.engine.pc.UtFalse -import org.utbot.engine.pc.UtGenericExpression import org.utbot.engine.pc.UtInt32Sort import org.utbot.engine.pc.UtIntSort -import org.utbot.engine.pc.UtIsExpression -import org.utbot.engine.pc.UtIsGenericTypeExpression +import org.utbot.engine.pc.UtLongSort import org.utbot.engine.pc.UtMkArrayExpression import org.utbot.engine.pc.UtMkTermArrayExpression import org.utbot.engine.pc.UtSeqSort import org.utbot.engine.pc.UtSort import org.utbot.engine.pc.UtTrue -import org.utbot.engine.pc.mkAnd import org.utbot.engine.pc.mkArrayConst -import org.utbot.engine.pc.mkArrayWithConst -import org.utbot.engine.pc.mkEq import org.utbot.engine.pc.mkInt -import org.utbot.engine.pc.mkNot -import org.utbot.engine.pc.mkOr +import org.utbot.engine.pc.mkLong import org.utbot.engine.pc.select import org.utbot.engine.pc.store import org.utbot.engine.pc.toSort -import org.utbot.engine.symbolic.asHardConstraint import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.FieldId -import java.math.BigInteger -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicLong import kotlinx.collections.immutable.ImmutableCollection import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentMap @@ -56,48 +39,21 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentMap -import org.utbot.framework.plugin.api.classId +import org.utbot.engine.types.STRING_TYPE +import org.utbot.engine.types.SeqType +import org.utbot.engine.types.TypeResolver +import org.utbot.framework.plugin.api.id +import org.utbot.framework.plugin.api.util.fieldId +import org.utbot.framework.plugin.api.util.isEnum import soot.ArrayType -import soot.BooleanType -import soot.ByteType import soot.CharType -import soot.DoubleType -import soot.FloatType import soot.IntType -import soot.LongType import soot.RefLikeType -import soot.RefType import soot.Scene -import soot.ShortType -import soot.SootClass import soot.SootField -import soot.SootMethod import soot.Type -import soot.tagkit.AnnotationClassElem -/** - * Represents a memory associated with a certain method call. For now consists only of local variables mapping. - * TODO: think on other fields later - * - * @param [locals] represents a mapping from [LocalVariable]s of a specific method call to [SymbolicValue]s. - */ -data class LocalVariableMemory( - private val locals: PersistentMap = persistentHashMapOf() -) { - fun memoryForNestedMethod(): LocalVariableMemory = this.copy(locals = persistentHashMapOf()) - - fun update(update: LocalMemoryUpdate): LocalVariableMemory = this.copy(locals = locals.update(update.locals)) - - /** - * Returns local variable value. - */ - fun local(variable: LocalVariable): SymbolicValue? = locals[variable] - - val localValues: Set - get() = locals.values.toSet() -} - /** * Local memory implementation based on arrays. * @@ -114,6 +70,11 @@ data class LocalVariableMemory( * * Note: [staticInitial] contains mapping from [FieldId] to the memory state at the moment of the field initialization. * + * [fieldValues] stores symbolic values for specified fields of the concrete object instances. + * We need to associate field of a concrete instance with the created symbolic value to not lose an information about its type. + * For example, if field's declared type is Runnable but at the current state it is a specific lambda, + * we have to save this lambda as a type of this field to be able to retrieve it in the future. + * * @see memoryForNestedMethod * @see FieldStates */ @@ -127,7 +88,9 @@ data class Memory( // TODO: split purely symbolic memory and information about s private val initializedStaticFields: PersistentSet = persistentHashSetOf(), private val staticFieldsStates: PersistentMap = persistentHashMapOf(), private val meaningfulStaticFields: PersistentSet = persistentHashSetOf(), + private val fieldValues: PersistentMap> = persistentHashMapOf(), private val addrToArrayType: PersistentMap = persistentHashMapOf(), + private val genericTypeStorageByAddr: PersistentMap> = persistentHashMapOf(), private val addrToMockInfo: PersistentMap = persistentHashMapOf(), private val updates: MemoryUpdate = MemoryUpdate(), // TODO: refactor this later. Now we use it only for statics substitution private val visitedValues: UtArrayExpressionBase = UtConstArrayExpression( @@ -149,6 +112,12 @@ data class Memory( // TODO: split purely symbolic memory and information about s UtFalse, UtArraySort(UtAddrSort, UtBoolSort) ), + // Const array here is because the initial values of all taint bit-vectors are 0. + // If you want to mark some symbolic variable, you should do it manually. + private var taintArray: UtArrayExpressionBase = UtConstArrayExpression( + mkLong(value = 0L), + UtArraySort(indexSort = UtAddrSort, itemSort = UtLongSort) + ), private val symbolicEnumValues: PersistentList = persistentListOf() ) { val chunkIds: Set @@ -160,6 +129,25 @@ data class Memory( // TODO: split purely symbolic memory and information about s fun staticFields(): Map = staticFieldsStates.filterKeys { it in meaningfulStaticFields } + /** + * Retrieves parameter type storages of an object with the given [addr] if present, or null otherwise. + */ + fun getTypeStoragesForObjectTypeParameters( + addr: UtAddrExpression + ): List? = genericTypeStorageByAddr[addr] + + /** + * Returns all collected information about addresses and corresponding generic types. + */ + fun getAllGenericTypeInfo(): Map> = genericTypeStorageByAddr + + /** + * Returns a symbolic value, associated with the specified [field] of the object with the specified [instanceAddr], + * if present, and null otherwise. + */ + fun fieldValue(field: SootField, instanceAddr: UtAddrExpression): SymbolicValue? = + fieldValues[field]?.get(instanceAddr) + /** * Construct the mapping from addresses to sets of fields whose values are read during the code execution * and therefore should be initialized in a constructed model. @@ -184,6 +172,8 @@ data class Memory( // TODO: split purely symbolic memory and information about s */ fun isSpeculativelyNotNull(addr: UtAddrExpression): UtArraySelectExpression = speculativelyNotNullAddresses.select(addr) + fun taintVector(addr: UtAddrExpression): UtArraySelectExpression = taintArray.select(addr) + /** * @return ImmutableCollection of the initial values for all the arrays we touched during the execution */ @@ -238,6 +228,7 @@ data class Memory( // TODO: split purely symbolic memory and information about s .mapValues { (_, values) -> values.first().value to values.last().value } val previousMemoryStates = staticFieldsStates.toMutableMap() + val previousFieldValues = fieldValues.toMutableMap() /** @@ -251,6 +242,11 @@ data class Memory( // TODO: split purely symbolic memory and information about s update.classIdToClearStatics?.let { classId -> Scene.v().getSootClass(classId.name).fields.forEach { sootField -> previousMemoryStates.remove(sootField.fieldId) + + if (sootField.isStatic) { + // Only statics should be cleared here + previousFieldValues.remove(sootField) + } } } @@ -283,6 +279,36 @@ data class Memory( // TODO: split purely symbolic memory and information about s acc.store(addr, UtTrue) } + // TODO: Fold inside a fold is not the best choice, it might cause + // significant growth of size of the taint bit-vec values + val updTaintArray = update.taintArrayUpdate + .groupBy { (addr, _) -> + addr + }.map { (addr, listUpdates) -> + addr to listUpdates.fold(mkLong(0).toLongValue()) { acc, (_, taintVector) -> + Or(acc, taintVector.toLongValue()).toLongValue() + } + }.fold(taintArray) { acc, (addr, taintVector) -> + acc.store(addr, taintVector.expr) + } + + // We have a list with updates for generic type info, and we want to apply + // them in such way that only updates with more precise type information + // should be applied. + val currentGenericsMap = update + .genericTypeStorageByAddr + // go over type generic updates and apply them to already existed info + .fold(genericTypeStorageByAddr.toMutableMap()) { acc, value -> + // If we have more type information, a new type storage will be returned. + // Otherwise, we will have the same info taken from the memory. + val (addr, typeStorages) = + TypeResolver.createGenericTypeInfoUpdate(value.first, value.second, acc) + .genericTypeStorageByAddr + .single() + acc[addr] = typeStorages + acc + } + return this.copy( initial = updInitial, current = updCurrent, @@ -293,13 +319,16 @@ data class Memory( // TODO: split purely symbolic memory and information about s initializedStaticFields = initializedStaticFields.addAll(update.initializedStaticFields), staticFieldsStates = previousMemoryStates.toPersistentMap().putAll(updatedStaticFields), meaningfulStaticFields = meaningfulStaticFields.addAll(update.meaningfulStaticFields), + fieldValues = previousFieldValues.toPersistentMap().putAll(update.fieldValues), addrToArrayType = addrToArrayType.putAll(update.addrToArrayType), + genericTypeStorageByAddr = currentGenericsMap.toPersistentMap(), addrToMockInfo = addrToMockInfo.putAll(update.addrToMockInfo), updates = updates + update, visitedValues = updVisitedValues, touchedAddresses = updTouchedAddresses, instanceFieldReadOperations = instanceFieldReadOperations.addAll(update.instanceFieldReads), speculativelyNotNullAddresses = updSpeculativelyNotNullAddresses, + taintArray = updTaintArray, symbolicEnumValues = symbolicEnumValues.addAll(update.symbolicEnumValues) ) } @@ -316,7 +345,8 @@ data class Memory( // TODO: split purely symbolic memory and information about s */ fun queuedStaticMemoryUpdates(): MemoryUpdate = MemoryUpdate( staticInstanceStorage = updates.staticInstanceStorage, - staticFieldsUpdates = updates.staticFieldsUpdates + staticFieldsUpdates = updates.staticFieldsUpdates, + fieldValues = updates.fieldValues.filter { it.key.isStatic }.toPersistentMap() ) /** @@ -354,581 +384,7 @@ data class Memory( // TODO: split purely symbolic memory and information about s fun findTypeForArrayOrNull(addr: UtAddrExpression): ArrayType? = addrToArrayType[addr] fun getSymbolicEnumValues(classId: ClassId): List = - symbolicEnumValues.filter { it.type.classId == classId } -} - -/** - * Types/classes registry. - * - * Registers and keeps two mappings: - * - Type <-> unique type id (int) - * - Object address -> to type id - */ -class TypeRegistry { - init { - // initializes type storage for OBJECT_TYPE from current scene - objectTypeStorage = TypeStorage(OBJECT_TYPE, Scene.v().classes.mapTo(mutableSetOf()) { it.type }) - } - - private val typeIdBiMap = HashBiMap.create() - - // A cache for strings representing bit-vectors for some collection of types. - private val typesToBitVecString = mutableMapOf, String>() - private val typeToRating = mutableMapOf() - 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() - - /** - * Contains mapping from a class to the class containing substitutions for its methods. - */ - private val targetToSubstitution: Map by lazy { - val classesWithTargets = Scene.v().classes.mapNotNull { clazz -> - val annotation = clazz.findMockAnnotationOrNull - ?.elems - ?.singleOrNull { it.name == "target" } as? AnnotationClassElem - - val classNameFromSignature = classBytecodeSignatureToClassNameOrNull(annotation?.desc) - - if (classNameFromSignature == null) { - null - } else { - val target = Scene.v().getSootClass(classNameFromSignature) - target to clazz - } - } - classesWithTargets.toMap() - } - - /** - * Contains mapping from a class with substitutions of the methods of the target class to the target class itself. - */ - private val substitutionToTarget: Map by lazy { - targetToSubstitution.entries.associate { (k, v) -> v to k } - } - - private val typeToFields = mutableMapOf>() - - /** - * An array containing information about whether the object with particular addr could throw a [ClassCastException]. - * - * Note: all objects can throw it by default. - * @see disableCastClassExceptionCheck - */ - private var isClassCastExceptionAllowed: UtArrayExpressionBase = - mkArrayWithConst(UtArraySort(UtAddrSort, UtBoolSort), UtTrue) - - - /** - * Contains information about types for ReferenceValues. - * An element on some position k contains information about type for an object with address == k - * Each element in addrToTypeId is in range [1..numberOfTypes] - */ - private val addrToTypeId: UtArrayExpressionBase by lazy { - mkArrayConst( - "addrToTypeId", - UtAddrSort, - UtInt32Sort - ) - } - - private val genericAddrToTypeArrays = mutableMapOf() - - private fun genericAddrToType(i: Int) = genericAddrToTypeArrays.getOrPut(i) { - mkArrayConst( - "genericAddrToTypeId_$i", - UtAddrSort, - UtInt32Sort - ) - } - - /** - * Contains information about number of dimensions for ReferenceValues. - */ - private val addrToNumDimensions: UtArrayExpressionBase by lazy { - mkArrayConst( - "addrToNumDimensions", - UtAddrSort, - UtInt32Sort - ) - } - - private val genericAddrToNumDimensionsArrays = mutableMapOf() - - private fun genericAddrToNumDimensions(i: Int) = genericAddrToNumDimensionsArrays.getOrPut(i) { - mkArrayConst( - "genericAddrToNumDimensions_$i", - UtAddrSort, - UtInt32Sort - ) - } - - /** - * Contains information about whether the object with some addr is a mock or not. - */ - private val isMockArray: UtArrayExpressionBase by lazy { - mkArrayConst( - "isMock", - UtAddrSort, - UtBoolSort - ) - } - - /** - * Takes information about whether the object with [addr] is mock or not. - * - * @see isMockArray - */ - fun isMock(addr: UtAddrExpression) = isMockArray.select(addr) - - /** - * Makes the numbers of dimensions for every object in the program equal to zero by default - */ - fun softZeroNumDimensions() = UtMkTermArrayExpression(addrToNumDimensions) - - /** - * addrToTypeId is created as const array of the emptyType. If such type occurs anywhere in the program, it means - * we haven't touched the element that this type belongs to - * @see emptyTypeId - */ - fun softEmptyTypes() = UtMkTermArrayExpression(addrToTypeId, mkInt(emptyTypeId)) - - /** - * Calculates type 'rating' for a particular type. Used for ordering ObjectValue's possible types. - * The type with a higher rating is more likely than the one with a lower rating. - */ - fun findRating(type: RefType) = typeToRating.getOrPut(type) { - var finalCost = 0 - - val sootClass = type.sootClass - - // TODO: let's have "preferred types" - if (sootClass.name == "java.util.ArrayList") finalCost += 4096 - if (sootClass.name == "java.util.LinkedList") finalCost += 2048 - if (sootClass.name == "java.util.HashMap") finalCost += 4096 - if (sootClass.name == "java.util.TreeMap") finalCost += 2048 - if (sootClass.name == "java.util.HashSet") finalCost += 2048 - if (sootClass.name == "java.lang.Integer") finalCost += 8192 - if (sootClass.name == "java.lang.Character") finalCost += 8192 - if (sootClass.name == "java.lang.Double") finalCost += 8192 - if (sootClass.name == "java.lang.Long") finalCost += 8192 - - if (sootClass.packageName.startsWith("java.lang")) finalCost += 1024 - - if (sootClass.packageName.startsWith("java.util")) finalCost += 512 - - if (sootClass.packageName.startsWith("java")) finalCost += 128 - - if (sootClass.isPublic) finalCost += 16 - - if (sootClass.isPrivate) finalCost += -16 - - if ("blocking" in sootClass.name.toLowerCase()) finalCost -= 32 - - if (sootClass.type.isJavaLangObject()) finalCost += -32 - - if (sootClass.isAnonymous) finalCost += -128 - - if (sootClass.name.contains("$")) finalCost += -4096 - - if (sootClass.type.sootClass.isInappropriate) finalCost += -8192 - - finalCost - } - - private val classRefCounter = AtomicInteger(classRefAddrsInitialValue) - private fun nextClassRefAddr() = UtAddrExpression(classRefCounter.getAndIncrement()) - - private val symbolicReturnNameCounter = AtomicLong(symbolicReturnNameCounterInitialValue) - fun findNewSymbolicReturnValueName() = - workaround(MAKE_SYMBOLIC) { "symbolicReturnValue\$${symbolicReturnNameCounter.incrementAndGet()}" } - - private val typeCounter = AtomicInteger(typeCounterInitialValue) - private fun nextTypeId() = typeCounter.getAndIncrement() - - /** - * Returns unique typeId for the given type - */ - fun findTypeId(type: Type): Int = typeIdBiMap.getOrPut(type) { nextTypeId() } - - /** - * Returns type for the given typeId - * - * @return If there is such typeId in the program, returns the corresponding type, otherwise returns null - */ - fun typeByIdOrNull(typeId: Int): Type? = typeIdBiMap.getByValue(typeId) - - /** - * Returns symbolic representation for a typeId corresponding to the given address - */ - fun symTypeId(addr: UtAddrExpression) = addrToTypeId.select(addr) - - /** - * Returns a symbolic representation for an [i]th type parameter - * corresponding to the given address - */ - fun genericTypeId(addr: UtAddrExpression, i: Int) = genericAddrToType(i).select(addr) - - /** - * Returns symbolic representation for a number of dimensions corresponding to the given address - */ - 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 - */ - fun zeroDimensionConstraint(addr: UtAddrExpression) = mkEq(symNumDimensions(addr), mkInt(objectNumDimensions)) - - /** - * Constructs a binary bit-vector by the given types with length 'numberOfTypes'. Each position - * corresponding to one of the typeId. - * - * @param types the collection of possible type - * @return decimal string representing the binary bit-vector - */ - fun constructBitVecString(types: Collection) = typesToBitVecString.getOrPut(types) { - val initialValue = BigInteger(ByteArray(numberOfTypes) { 0 }) - - return types.fold(initialValue) { acc, type -> - val typeId = if (type is ArrayType) findTypeId(type.baseType) else findTypeId(type) - acc.setBit(typeId) - }.toString() - } - - /** - * Creates class reference, i.e. Class<Integer> - * - * Note: Uses type id as an address to have the one and the same class reference for all objects of one class - */ - fun createClassRef(baseType: Type, numDimensions: Int = 0): MethodResult { - val addr = classRefBiMap.getOrPut(baseType) { nextClassRefAddr() } - - val objectValue = ObjectValue(TypeStorage(CLASS_REF_TYPE), addr) - - val typeConstraint = typeConstraint(addr, TypeStorage(CLASS_REF_TYPE)).all() - - val typeId = mkInt(findTypeId(baseType)) - val symNumDimensions = mkInt(numDimensions) - - val stores = persistentListOf( - simplifiedNamedStore(CLASS_REF_TYPE_DESCRIPTOR, addr, typeId), - simplifiedNamedStore(CLASS_REF_NUM_DIMENSIONS_DESCRIPTOR, addr, symNumDimensions) - ) - - val touchedDescriptors = persistentSetOf(CLASS_REF_TYPE_DESCRIPTOR, CLASS_REF_NUM_DIMENSIONS_DESCRIPTOR) - - val memoryUpdate = MemoryUpdate(stores = stores, touchedChunkDescriptors = touchedDescriptors) - - return MethodResult(objectValue, typeConstraint.asHardConstraint(), memoryUpdates = memoryUpdate) - } - - /** - * Returns a list of inheritors for the given [type], including itself. - */ - fun findInheritorsIncludingTypes(type: RefType, defaultValue: () -> Set) = - typeToInheritorsTypes.getOrPut(type, defaultValue) - - /** - * Returns a list of ancestors for the given [type], including itself. - */ - fun findAncestorsIncludingTypes(type: RefType, defaultValue: () -> Set) = - typeToAncestorsTypes.getOrPut(type, defaultValue) - - fun findFields(type: RefType, defaultValue: () -> List) = - typeToFields.getOrPut(type, defaultValue) - - /** - * Returns a [TypeConstraint] instance for the given [addr] and [typeStorage]. - */ - fun typeConstraint(addr: UtAddrExpression, typeStorage: TypeStorage): TypeConstraint = - TypeConstraint( - constructIsExpression(addr, typeStorage), - mkEq(addr, nullObjectAddr), - constructCorrectnessConstraint(addr, typeStorage) - ) - - private fun constructIsExpression(addr: UtAddrExpression, typeStorage: TypeStorage): UtIsExpression = - UtIsExpression(addr, typeStorage, numberOfTypes) - - /** - * Returns a conjunction of the constraints responsible for the type construction: - * * typeId must be in range [[emptyTypeId]..[numberOfTypes]]; - * * numDimensions must be in range [0..[MAX_NUM_DIMENSIONS]]; - * * if the baseType for [TypeStorage.leastCommonType] is a [java.lang.Object], - * should be added constraints for primitive arrays to prevent - * impossible resolved types: Object[] must be at least primType[][]. - */ - private fun constructCorrectnessConstraint(addr: UtAddrExpression, typeStorage: TypeStorage): UtBoolExpression { - val symType = symTypeId(addr) - val symNumDimensions = symNumDimensions(addr) - val type = typeStorage.leastCommonType - - val constraints = mutableListOf() - - // add constraints for typeId, it must be in 0..numberOfTypes - constraints += Ge(symType.toIntValue(), emptyTypeId.toPrimitiveValue()) - constraints += Le(symType.toIntValue(), numberOfTypes.toPrimitiveValue()) - - // add constraints for number of dimensions, it must be in 0..MAX_NUM_DIMENSIONS - constraints += Ge(symNumDimensions.toIntValue(), 0.toPrimitiveValue()) - constraints += Le(symNumDimensions.toIntValue(), MAX_NUM_DIMENSIONS.toPrimitiveValue()) - - // add constraints for object and arrays of primitives - if (type.baseType.isJavaLangObject()) { - primTypes.forEach { - val typesAreEqual = mkEq(symType, mkInt(findTypeId(it))) - val numDimensions = Gt(symNumDimensions.toIntValue(), type.numDimensions.toPrimitiveValue()) - constraints += mkOr(mkNot(typesAreEqual), numDimensions) - } - } - - // there are no arrays of anonymous classes - typeStorage.possibleConcreteTypes - .mapNotNull { (it.baseType as? RefType) } - .filter { it.sootClass.isAnonymous } - .forEach { - val typesAreEqual = mkEq(symType, mkInt(findTypeId(it))) - val numDimensions = mkEq(symNumDimensions.toIntValue(), mkInt(objectNumDimensions).toIntValue()) - constraints += mkOr(mkNot(typesAreEqual), numDimensions) - } - - return mkAnd(constraints) - } - - /** - * returns constraint representing, that object with address [addr] is parametrized by [types] type parameters. - * @see UtGenericExpression - */ - fun genericTypeParameterConstraint(addr: UtAddrExpression, types: List) = - UtGenericExpression(addr, types, numberOfTypes) - - /** - * returns constraint representing that type parameters of an object with address [firstAddr] are equal to - * type parameters of an object with address [secondAddr], corresponding to [indexInjection] - * @see UtEqGenericTypeParametersExpression - */ - @Suppress("unused") - fun eqGenericTypeParametersConstraint( - firstAddr: UtAddrExpression, - secondAddr: UtAddrExpression, - vararg indexInjection: Pair - ): 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 - * the corresponding type parameters of an object with address [secondAddr] - * @see UtEqGenericTypeParametersExpression - */ - fun eqGenericTypeParametersConstraint( - firstAddr: UtAddrExpression, - secondAddr: UtAddrExpression, - parameterSize: Int - ) : 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 - * the first type parameter of an object with address [secondAddr] - * @see UtEqGenericTypeParametersExpression - */ - fun eqGenericSingleTypeParameterConstraint(firstAddr: UtAddrExpression, secondAddr: UtAddrExpression) = - 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 - * with index [i] of an object with address [baseAddr]. - * - * For a SomeCollection the type parameters are [A, B], where A and B are type variables - * with indices zero and one respectively. To connect some element of the collection with its generic type - * add to the constraints `typeConstraintToGenericTypeParameter(elementAddr, collectionAddr, typeParamIndex)`. - * - * @see UtIsGenericTypeExpression - */ - fun typeConstraintToGenericTypeParameter( - addr: UtAddrExpression, - baseAddr: UtAddrExpression, - i: Int - ): UtIsGenericTypeExpression = UtIsGenericTypeExpression(addr, baseAddr, i) - - /** - * Looks for a substitution for the given [method]. - * - * @param method a method to be substituted. - * @return substituted method if the given [method] has substitution, null otherwise. - * - * Note: all the methods in the class with substitutions will be returned instead of methods of the target class - * with the same name and parameters' types without any additional annotations. The only exception is `` - * method, substitutions will be returned only for constructors marked by [org.utbot.api.annotation.UtConstructorMock] - * annotation. - */ - fun findSubstitutionOrNull(method: SootMethod): SootMethod? { - val declaringClass = method.declaringClass - val classWithSubstitutions = targetToSubstitution[declaringClass] - - val substitutedMethod = classWithSubstitutions - ?.methods - ?.singleOrNull { it.name == method.name && it.parameterTypes == method.parameterTypes } - // Note: subSignature is not used in order to support `this` as method's return value. - // Otherwise we'd have to check for wrong `this` type in the subSignature - - if (method.isConstructor) { - // if the constructor doesn't have the mock annotation do not substitute it - substitutedMethod?.findMockAnnotationOrNull ?: return null - } - return substitutedMethod - } - - /** - * Returns a class containing substitutions for the methods belong to the target class, null if there is not such class. - */ - @Suppress("unused") - fun findSubstitutionByTargetOrNull(targetClass: SootClass): SootClass? = targetToSubstitution[targetClass] - - /** - * Returns a target class by given class with methods substitutions. - */ - @Suppress("MemberVisibilityCanBePrivate") - fun findTargetBySubstitutionOrNull(classWithSubstitutions: SootClass): SootClass? = - substitutionToTarget[classWithSubstitutions] - - /** - * Looks for 'real' type. - * - * For example, we have two classes: A and B, B contains substitutions for A's methods. - * `findRealType(a.type)` will return `a.type`, but `findRealType(b.type)` will return `a.type` as well. - * - * Returns: - * * [type] if it is not a RefType; - * * [type] if it is a RefType and it doesn't have a target class to substitute; - * * otherwise a type of the target class, which methods should be substituted. - */ - fun findRealType(type: Type): Type = - if (type !is RefType) type else findTargetBySubstitutionOrNull(type.sootClass)?.type ?: type - - /** - * Returns a select expression containing information about whether [ClassCastException] is allowed or not - * for an object with the given [addr]. - * - * True means that [ClassCastException] might be thrown, false will restrict it. - */ - fun isClassCastExceptionAllowed(addr: UtAddrExpression) = isClassCastExceptionAllowed.select(addr) - - /** - * Modify [isClassCastExceptionAllowed] to make impossible for a [ClassCastException] to be thrown for an object - * with the given [addr]. - */ - fun disableCastClassExceptionCheck(addr: UtAddrExpression) { - isClassCastExceptionAllowed = isClassCastExceptionAllowed.store(addr, UtFalse) - } - - /** - * Returns chunkId for the given [arrayType]. - * - * Examples: - * * Object[] -> RefValues_Arrays - * * int[] -> intArrays - * * int[][] -> MultiArrays - */ - fun arrayChunkId(arrayType: ArrayType) = when (arrayType.numDimensions) { - 1 -> if (arrayType.baseType is RefType) { - ChunkId("RefValues", "Arrays") - } else { - ChunkId("${findRealType(arrayType.baseType)}", "Arrays") - } - else -> ChunkId("Multi", "Arrays") - } - - companion object { - // we use different shifts to distinguish easily types from objects in z3 listings - const val objectCounterInitialValue = 0x00000001 // 0x00000000 is reserved for NULL - - // we want to reserve addresses for every ClassRef in the program starting from this one - // Note: the number had been chosen randomly and can be changes without any consequences - const val classRefAddrsInitialValue = -16777216 // -(2 ^ 24) - - // since we use typeId as addr for ConstRef, we can not use 0x00000000 because of NULL value - const val typeCounterInitialValue = 0x00000001 - const val symbolicReturnNameCounterInitialValue = 0x80000000 - const val objectNumDimensions = 0 - const val emptyTypeId = 0 - private const val primitivesNumber = 8 - - internal val primTypes - get() = listOf( - ByteType.v(), - ShortType.v(), - IntType.v(), - LongType.v(), - FloatType.v(), - DoubleType.v(), - BooleanType.v(), - CharType.v() - ) - - val numberOfTypes get() = Scene.v().classes.size + primitivesNumber + typeCounterInitialValue - - /** - * Stores [TypeStorage] for [OBJECT_TYPE]. As it should be changed when Soot scene changes, - * it is loaded each time when [TypeRegistry] is created in init section. - */ - lateinit var objectTypeStorage: TypeStorage - } + extractSymbolicEnumValues(symbolicEnumValues, classId) } private fun initialArray(descriptor: MemoryChunkDescriptor) = @@ -966,13 +422,16 @@ data class MemoryUpdate( val initializedStaticFields: PersistentSet = persistentHashSetOf(), val staticFieldsUpdates: PersistentList = persistentListOf(), val meaningfulStaticFields: PersistentSet = persistentHashSetOf(), + val fieldValues: PersistentMap> = persistentHashMapOf(), val addrToArrayType: PersistentMap = persistentHashMapOf(), + val genericTypeStorageByAddr: PersistentList>> = persistentListOf(), val addrToMockInfo: PersistentMap = persistentHashMapOf(), val visitedValues: PersistentList = persistentListOf(), val touchedAddresses: PersistentList = persistentListOf(), val classIdToClearStatics: ClassId? = null, val instanceFieldReads: PersistentSet = persistentHashSetOf(), val speculativelyNotNullAddresses: PersistentList = persistentListOf(), + val taintArrayUpdate: PersistentList> = persistentListOf(), val symbolicEnumValues: PersistentList = persistentListOf() ) { operator fun plus(other: MemoryUpdate) = @@ -985,18 +444,21 @@ data class MemoryUpdate( initializedStaticFields = initializedStaticFields.addAll(other.initializedStaticFields), staticFieldsUpdates = staticFieldsUpdates.addAll(other.staticFieldsUpdates), meaningfulStaticFields = meaningfulStaticFields.addAll(other.meaningfulStaticFields), + fieldValues = fieldValues.putAll(other.fieldValues), addrToArrayType = addrToArrayType.putAll(other.addrToArrayType), + genericTypeStorageByAddr = genericTypeStorageByAddr.addAll(other.genericTypeStorageByAddr), addrToMockInfo = addrToMockInfo.putAll(other.addrToMockInfo), visitedValues = visitedValues.addAll(other.visitedValues), touchedAddresses = touchedAddresses.addAll(other.touchedAddresses), classIdToClearStatics = other.classIdToClearStatics, instanceFieldReads = instanceFieldReads.addAll(other.instanceFieldReads), speculativelyNotNullAddresses = speculativelyNotNullAddresses.addAll(other.speculativelyNotNullAddresses), + taintArrayUpdate = taintArrayUpdate.addAll(other.taintArrayUpdate), symbolicEnumValues = symbolicEnumValues.addAll(other.symbolicEnumValues), ) fun getSymbolicEnumValues(classId: ClassId): List = - symbolicEnumValues.filter { it.type.classId == classId } + extractSymbolicEnumValues(symbolicEnumValues, classId) } // array - Java Array @@ -1040,20 +502,20 @@ data class UtNamedStore( ) /** - * Create [UtNamedStore] with simplified [index] and [value] expressions. + * Create [UtNamedStore] with unsimplified [index] and [value] expressions. * - * @see RewritingVisitor + * @note simplifications occur explicitly in [Traverser] */ -fun simplifiedNamedStore( +fun namedStore( chunkDescriptor: MemoryChunkDescriptor, index: UtExpression, value: UtExpression -) = RewritingVisitor().let { visitor -> UtNamedStore(chunkDescriptor, index.accept(visitor), value.accept(visitor)) } +) = UtNamedStore(chunkDescriptor, index, value) /** * Updates persistent map where value = null in update means deletion of original key-value */ -private fun PersistentMap.update(update: Map): PersistentMap { +fun PersistentMap.update(update: Map): PersistentMap { if (update.isEmpty()) return this val deletions = mutableListOf() val updates = mutableMapOf() @@ -1077,8 +539,6 @@ fun localMemoryUpdate(vararg updates: Pair) = private val STRING_INTERNAL = ChunkId(java.lang.String::class.qualifiedName!!, "internal") -private val NATIVE_STRING_VALUE = ChunkId(UtNativeString::class.qualifiedName!!, "value") - internal val STRING_LENGTH get() = utStringClass.getField("length", IntType.v()) internal val STRING_VALUE @@ -1091,15 +551,6 @@ internal val STRING_INTERNAL_DESCRIPTOR: MemoryChunkDescriptor get() = MemoryChunkDescriptor(STRING_INTERNAL, STRING_TYPE, SeqType) -internal val NATIVE_STRING_VALUE_DESCRIPTOR: MemoryChunkDescriptor - get() = MemoryChunkDescriptor(NATIVE_STRING_VALUE, utNativeStringClass.type, SeqType) - -/** - * Returns internal string representation by String object address, addr -> String - */ -fun Memory.nativeStringValue(addr: UtAddrExpression) = - PrimitiveValue(SeqType, findArray(NATIVE_STRING_VALUE_DESCRIPTOR).select(addr)).expr - private const val STRING_INTERN_MAP_LABEL = "java.lang.String_intern_map" /** @@ -1148,3 +599,21 @@ private operator fun MockInfoEnriched.plus(update: MockInfoEnriched?): MockInfoE private fun MutableMap>.mergeValues(other: Map>): Map> = apply { other.forEach { (key, values) -> merge(key, values) { v1, v2 -> v1 + v2 } } } + +private fun extractSymbolicEnumValues( + symbolicEnumValuesSource: PersistentList, + classId: ClassId +): List = symbolicEnumValuesSource.filter { + val symbolicValueClassId = it.type.id + + // If symbolicValueClassId is not an enum in accordance with java.lang.Class.isEnum + // function, we have to take results for its superclass (a direct inheritor of java.lang.Enum). + // Otherwise, we should get results by its own classId. + val enumClass = if (symbolicValueClassId.isEnum) { + symbolicValueClassId + } else { + it.type.sootClass.superClassOrNull()?.id + } + + enumClass == classId +} \ No newline at end of file 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 63c32a753f..9faf511ab6 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Mocks.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Mocks.kt @@ -11,21 +11,21 @@ import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.id import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.isRefType import org.utbot.framework.util.executableId import java.util.concurrent.atomic.AtomicInteger import kotlin.reflect.KFunction2 import kotlin.reflect.KFunction5 import kotlinx.collections.immutable.persistentListOf import org.utbot.common.nameOfPackage +import org.utbot.engine.types.OBJECT_TYPE import org.utbot.engine.util.mockListeners.MockListenerController -import org.utbot.framework.util.isInaccessibleViaReflection +import org.utbot.framework.context.MockerContext +import org.utbot.framework.plugin.api.util.isInaccessibleViaReflection import soot.BooleanType import soot.RefType import soot.Scene import soot.SootClass import soot.SootMethod -import kotlin.reflect.KFunction4 /** * Generates mock with address provided. @@ -129,6 +129,36 @@ data class UtStaticMethodMockInfo( val methodId: MethodId ) : UtMockInfo(methodId.classId, addr) +/** + * A wrapper for [ObjectValue] to store additional info. + */ +sealed class MockedObjectInfo { + abstract val value: ObjectValue? +} + +object NoMock: MockedObjectInfo() { + override val value: ObjectValue? = null +} + +/** + * Represents a mock that occurs when mock strategy allows it + * or when an object type requires always requires mocking. + * + * See [Mocker.mockAlways] for more details. + */ +class ExpectedMock(objectValue: ObjectValue): MockedObjectInfo() { + override val value: ObjectValue = objectValue +} + +/** + * Represents a mock that occurs when it is not allowed. + * E.g. mock framework is not installed or + * mock strategy is [MockStrategy.NO_MOCKS] and class is not in [Mocker.mockAlways] set. + */ +class UnexpectedMock(objectValue: ObjectValue): MockedObjectInfo() { + override val value: ObjectValue = objectValue +} + /** * Service to mock things. Knows mock strategy, class under test and class hierarchy. */ @@ -138,21 +168,30 @@ class Mocker( private val hierarchy: Hierarchy, chosenClassesToMockAlways: Set, internal val mockListenerController: MockListenerController? = null, + private val mockerContext: MockerContext, ) { + private val mocksAreDesired: Boolean = strategy != MockStrategy.NO_MOCKS + /** - * Creates mocked instance of the [type] using mock info if it should be mocked by the mocker, - * otherwise returns null. + * Creates mocked instance (if it should be mocked by the mocker) of the [type] using [mockInfo] + * otherwise returns [NoMock]. * * @see shouldMock */ - fun mock(type: RefType, mockInfo: UtMockInfo): ObjectValue? = - if (shouldMock(type, mockInfo)) createMockObject(type, mockInfo) else null + fun mock(type: RefType, mockInfo: UtMockInfo): MockedObjectInfo { + val objectValue = if (shouldMock(type, mockInfo)) createMockObject(type, mockInfo) else null + return construct(objectValue, mockInfo) + } /** - * Creates mocked instance of the [type] using mock info. Unlike to [mock], it does not - * check anything and always returns the constructed mock. + * Unlike to [mock], unconditionally creates a mocked instance of the [type] using [mockInfo]. */ - fun forceMock(type: RefType, mockInfo: UtMockInfo): ObjectValue = createMockObject(type, mockInfo) + fun forceMock(type: RefType, mockInfo: UtMockInfo): MockedObjectInfo { + mockListenerController?.onShouldMock(strategy, mockInfo) + + val objectValue = createMockObject(type, mockInfo) + return construct(objectValue, mockInfo) + } /** * Checks if Engine should mock objects of particular type with current mock strategy and mock type. @@ -178,6 +217,30 @@ class Mocker( } } + /** + * Constructs [MockedObjectInfo]: enriches given [mockedValue] with an information if mocking is expected or not. + */ + private fun construct(mockedValue: ObjectValue?, mockInfo: UtMockInfo): MockedObjectInfo { + if (mockedValue == null) { + return NoMock + } + + val mockingIsPossible = when (mockInfo) { + is UtFieldMockInfo, + is UtObjectMockInfo -> mockerContext.mockFrameworkInstalled + is UtNewInstanceMockInfo, + is UtStaticMethodMockInfo, + is UtStaticObjectMockInfo -> mockerContext.staticsMockingIsConfigured + } + val mockingIsForcedAndPossible = mockAlways(mockedValue.type) && mockingIsPossible + + return if (mocksAreDesired || mockingIsForcedAndPossible) { + ExpectedMock(mockedValue) + } else { + UnexpectedMock(mockedValue) + } + } + private fun checkIfShouldMock( type: RefType, mockInfo: UtMockInfo @@ -188,12 +251,28 @@ class Mocker( if (isOverriddenClass(type)) return false // never mock overriden classes if (type.isInaccessibleViaReflection) return false // never mock classes that we can't process with reflection 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 - 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) + + val sootClass = type.sootClass + + if (sootClass.isArtificialEntity) return false // never mock artificial types, i.e. Maps$lambda_computeValue_1__7 + + if (!isEngineClass(type)) { + // there is no reason (and maybe no possibility) to mock such classes + if (sootClass.isInnerClass || sootClass.isLocal || sootClass.isAnonymous) { + return false + } + + // could not mock private classes (even if it is in mock always list) + if (sootClass.isPrivate) { + return false + } + } + if (mockAlways(type)) return true // always mock randoms and loggers + if (mockInfo is UtFieldMockInfo) { - val declaringClass = mockInfo.fieldId.declaringClass + val fieldId = mockInfo.fieldId + val declaringClass = fieldId.declaringClass val sootDeclaringClass = Scene.v().getSootClass(declaringClass.name) if (sootDeclaringClass.isArtificialEntity || sootDeclaringClass.isOverridden) { @@ -203,12 +282,22 @@ class Mocker( return false } - return when { - 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 + val sootField = sootDeclaringClass + .fields + .firstOrNull { it.name == fieldId.name && it.declaringClass.name == sootDeclaringClass.name } + ?: error("Unexpected $fieldId is provided into shouldMock function") + + val sootFieldType = sootField.type + + if (sootFieldType !is RefType) { + return false } + + return strategy.eligibleToMock(sootFieldType.id, classUnderTest) } + + // Note that eligibleToMock can use information retrieved from jClass + // Therefore, such classes should be already processed at this point return strategy.eligibleToMock(type.id, classUnderTest) // strategy to decide } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ObjectCounter.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ObjectCounter.kt new file mode 100644 index 0000000000..53dd3d320a --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ObjectCounter.kt @@ -0,0 +1,12 @@ +package org.utbot.engine + +import java.util.concurrent.atomic.AtomicInteger + +/** + * Counts new objects during execution. Used to give new addresses for objects in [Traverser] and [Resolver]. + */ +data class ObjectCounter(val initialValue: Int) { + private val internalCounter = AtomicInteger(initialValue) + + fun createNewAddr(): Int = internalCounter.getAndIncrement() +} 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 936ca1a137..2dd16eda3b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ObjectWrappers.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ObjectWrappers.kt @@ -15,20 +15,54 @@ import org.utbot.engine.UtStreamClass.UT_LONG_STREAM 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.UtArrayList +import org.utbot.engine.overrides.collections.UtArrayList.UtArrayListIterator +import org.utbot.engine.overrides.collections.UtArrayList.UtArrayListSimpleIterator import org.utbot.engine.overrides.collections.UtHashMap import org.utbot.engine.overrides.collections.UtHashSet +import org.utbot.engine.overrides.collections.UtHashSet.UtHashSetIterator +import org.utbot.engine.overrides.collections.UtLinkedList +import org.utbot.engine.overrides.collections.UtLinkedList.UtLinkedListIterator +import org.utbot.engine.overrides.collections.UtLinkedList.UtReverseIterator +import org.utbot.engine.overrides.collections.UtLinkedListWithNullableCheck +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.security.UtSecurityManager -import org.utbot.engine.overrides.strings.UtNativeString +import org.utbot.engine.overrides.stream.UtDoubleStream +import org.utbot.engine.overrides.stream.UtIntStream +import org.utbot.engine.overrides.stream.UtLongStream +import org.utbot.engine.overrides.stream.UtStream import org.utbot.engine.overrides.strings.UtString import org.utbot.engine.overrides.strings.UtStringBuffer import org.utbot.engine.overrides.strings.UtStringBuilder +import org.utbot.engine.overrides.threads.UtCompletableFuture +import org.utbot.engine.overrides.threads.UtCountDownLatch +import org.utbot.engine.overrides.threads.UtExecutorService +import org.utbot.engine.overrides.threads.UtThread +import org.utbot.engine.overrides.threads.UtThreadGroup import org.utbot.engine.pc.UtAddrExpression +import org.utbot.engine.types.COMPLETABLE_FUTURE_TYPE +import org.utbot.engine.types.COUNT_DOWN_LATCH_TYPE +import org.utbot.engine.types.EXECUTOR_SERVICE_TYPE +import org.utbot.engine.types.OPTIONAL_DOUBLE_TYPE +import org.utbot.engine.types.OPTIONAL_INT_TYPE +import org.utbot.engine.types.OPTIONAL_LONG_TYPE +import org.utbot.engine.types.OPTIONAL_TYPE +import org.utbot.engine.types.SECURITY_MANAGER_TYPE +import org.utbot.engine.types.STRING_BUFFER_TYPE +import org.utbot.engine.types.STRING_BUILDER_TYPE +import org.utbot.engine.types.STRING_TYPE +import org.utbot.engine.types.THREAD_GROUP_TYPE +import org.utbot.engine.types.THREAD_TYPE +import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtExecutableCallModel import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.UtStatementModel import org.utbot.framework.plugin.api.id +import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.constructorId import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.stringClassId @@ -37,13 +71,8 @@ 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 kotlin.reflect.KFunction4 +import kotlin.reflect.jvm.jvmName typealias TypeToBeWrapped = RefType typealias WrapperType = RefType @@ -55,25 +84,31 @@ typealias WrapperType = RefType val classToWrapper: MutableMap = mutableMapOf().apply { putSootClass(java.lang.StringBuilder::class, utStringBuilderClass) - putSootClass(UtStringBuilder::class, utStringBuilderClass) putSootClass(java.lang.StringBuffer::class, utStringBufferClass) - putSootClass(UtStringBuffer::class, utStringBufferClass) - putSootClass(UtNativeString::class, utNativeStringClass) putSootClass(java.lang.CharSequence::class, utStringClass) putSootClass(java.lang.String::class, utStringClass) - putSootClass(UtString::class, utStringClass) - putSootClass(Optional::class, UT_OPTIONAL.className) - putSootClass(OptionalInt::class, UT_OPTIONAL_INT.className) - putSootClass(OptionalLong::class, UT_OPTIONAL_LONG.className) - putSootClass(OptionalDouble::class, UT_OPTIONAL_DOUBLE.className) - - putSootClass(RangeModifiableUnlimitedArray::class, RangeModifiableUnlimitedArrayWrapper::class) - putSootClass(AssociativeArray::class, AssociativeArrayWrapper::class) + putSootClass(java.util.Optional::class, UT_OPTIONAL.className) + putSootClass(java.util.OptionalInt::class, UT_OPTIONAL_INT.className) + putSootClass(java.util.OptionalLong::class, UT_OPTIONAL_LONG.className) + putSootClass(java.util.OptionalDouble::class, UT_OPTIONAL_DOUBLE.className) + + // threads + putSootClass(java.lang.Thread::class, utThreadClass) + putSootClass(java.lang.ThreadGroup::class, utThreadGroupClass) + + // executors, futures and latches + putSootClass(java.util.concurrent.ExecutorService::class, utExecutorServiceClass) + putSootClass(java.util.concurrent.ThreadPoolExecutor::class, utExecutorServiceClass) + putSootClass(java.util.concurrent.ForkJoinPool::class, utExecutorServiceClass) + putSootClass(java.util.concurrent.ScheduledThreadPoolExecutor::class, utExecutorServiceClass) + putSootClass(java.util.concurrent.CountDownLatch::class, utCountDownLatchClass) + putSootClass(java.util.concurrent.CompletableFuture::class, utCompletableFutureClass) + putSootClass(java.util.concurrent.CompletionStage::class, utCompletableFutureClass) putSootClass(java.util.List::class, UT_ARRAY_LIST.className) putSootClass(java.util.AbstractList::class, UT_ARRAY_LIST.className) putSootClass(java.util.ArrayList::class, UT_ARRAY_LIST.className) - putSootClass(CopyOnWriteArrayList::class, UT_ARRAY_LIST.className) + putSootClass(java.util.concurrent.CopyOnWriteArrayList::class, UT_ARRAY_LIST.className) putSootClass(java.util.LinkedList::class, UT_LINKED_LIST.className) putSootClass(java.util.AbstractSequentialList::class, UT_LINKED_LIST.className) @@ -94,6 +129,13 @@ val classToWrapper: MutableMap = putSootClass(java.util.HashMap::class, UtHashMap::class) putSootClass(java.util.concurrent.ConcurrentHashMap::class, UtHashMap::class) + // Iterators + putSootClass(java.util.Iterator::class, UtArrayListSimpleIterator::class) + putSootClass(java.util.ListIterator::class, UtArrayListIterator::class) + putSootClass(UtLinkedListIterator::class, UtLinkedListIterator::class) + putSootClass(UtReverseIterator::class, UtReverseIterator::class) + putSootClass(UtHashSetIterator::class, UtHashSetIterator::class) + putSootClass(java.util.stream.BaseStream::class, UT_STREAM.className) putSootClass(java.util.stream.Stream::class, UT_STREAM.className) putSootClass(java.util.stream.IntStream::class, UT_INT_STREAM.className) @@ -101,6 +143,17 @@ val classToWrapper: MutableMap = putSootClass(java.util.stream.DoubleStream::class, UT_DOUBLE_STREAM.className) putSootClass(java.lang.SecurityManager::class, UtSecurityManager::class) + + putSootClass(RangeModifiableUnlimitedArray::class, RangeModifiableUnlimitedArrayWrapper::class) + putSootClass(AssociativeArray::class, AssociativeArray::class) + }.apply { + // Each wrapper has to wrap itself to make possible to create it but with the underlying type in UtMocks or in wrappers. + // We take this particular classloader because current classloader cannot load our classes + val applicationClassLoader = UtContext::class.java.classLoader + values.distinct().forEach { + val kClass = applicationClassLoader.loadClass(it.className).kotlin + putSootClass(kClass, it) + } } /** @@ -118,7 +171,7 @@ val wrapperToClass: Map> = private fun MutableMap.putSootClass( key: KClass<*>, value: KClass<*> -) = putSootClass(key, Scene.v().getSootClass(value.java.canonicalName).type) +) = putSootClass(key, Scene.v().getSootClass(value.jvmName).type) // It is important to use `jvmName` because `canonicalName` replaces `$` for nested classes to `.` private fun MutableMap.putSootClass( key: KClass<*>, @@ -133,36 +186,34 @@ private fun MutableMap.putSootClass( private fun MutableMap.putSootClass( key: KClass<*>, value: RefType -) = put(Scene.v().getSootClass(key.java.canonicalName).type, value) +) = put(Scene.v().getSootClass(key.jvmName).type, value) // It is important to use `jvmName` because `canonicalName` replaces `$` for nested classes to `.` -private val wrappers = mapOf( +private val wrappers: Map ObjectValue> = mutableMapOf( wrap(java.lang.StringBuilder::class) { type, addr -> objectValue(type, addr, UtStringBuilderWrapper()) }, - wrap(UtStringBuilder::class) { type, addr -> objectValue(type, addr, UtStringBuilderWrapper()) }, wrap(java.lang.StringBuffer::class) { type, addr -> objectValue(type, addr, UtStringBufferWrapper()) }, - wrap(UtStringBuffer::class) { type, addr -> objectValue(type, addr, UtStringBufferWrapper()) }, - wrap(UtNativeString::class) { type, addr -> objectValue(type, addr, UtNativeStringWrapper()) }, wrap(java.lang.CharSequence::class) { type, addr -> objectValue(type, addr, StringWrapper()) }, wrap(java.lang.String::class) { type, addr -> objectValue(type, addr, StringWrapper()) }, - wrap(UtString::class) { type, addr -> objectValue(type, addr, StringWrapper()) }, - wrap(Optional::class) { type, addr -> objectValue(type, addr, OptionalWrapper(UT_OPTIONAL)) }, - wrap(OptionalInt::class) { type, addr -> objectValue(type, addr, OptionalWrapper(UT_OPTIONAL_INT)) }, - wrap(OptionalLong::class) { type, addr -> objectValue(type, addr, OptionalWrapper(UT_OPTIONAL_LONG)) }, - wrap(OptionalDouble::class) { type, addr -> objectValue(type, addr, OptionalWrapper(UT_OPTIONAL_DOUBLE)) }, - - wrap(RangeModifiableUnlimitedArray::class) { type, addr -> - objectValue(type, addr, RangeModifiableUnlimitedArrayWrapper()) - }, - wrap(AssociativeArray::class) { type, addr -> - objectValue(type, addr, AssociativeArrayWrapper()) - }, + wrap(java.util.Optional::class) { type, addr -> objectValue(type, addr, OptionalWrapper(UT_OPTIONAL)) }, + wrap(java.util.OptionalInt::class) { type, addr -> objectValue(type, addr, OptionalWrapper(UT_OPTIONAL_INT)) }, + wrap(java.util.OptionalLong::class) { type, addr -> objectValue(type, addr, OptionalWrapper(UT_OPTIONAL_LONG)) }, + wrap(java.util.OptionalDouble::class) { type, addr -> objectValue(type, addr, OptionalWrapper(UT_OPTIONAL_DOUBLE)) }, + + // threads + wrap(java.lang.Thread::class) { type, addr -> objectValue(type, addr, ThreadWrapper()) }, + wrap(java.lang.ThreadGroup::class) { type, addr -> objectValue(type, addr, ThreadGroupWrapper()) }, + wrap(java.util.concurrent.ExecutorService::class) { type, addr -> objectValue(type, addr, ExecutorServiceWrapper()) }, + wrap(java.util.concurrent.ThreadPoolExecutor::class) { type, addr -> objectValue(type, addr, ExecutorServiceWrapper()) }, + wrap(java.util.concurrent.ForkJoinPool::class) { type, addr -> objectValue(type, addr, ExecutorServiceWrapper()) }, + wrap(java.util.concurrent.ScheduledThreadPoolExecutor::class) { type, addr -> objectValue(type, addr, ExecutorServiceWrapper()) }, + wrap(java.util.concurrent.CountDownLatch::class) { type, addr -> objectValue(type, addr, CountDownLatchWrapper()) }, + wrap(java.util.concurrent.CompletableFuture::class) { type, addr -> objectValue(type, addr, CompletableFutureWrapper()) }, + wrap(java.util.concurrent.CompletionStage::class) { type, addr -> objectValue(type, addr, CompletableFutureWrapper()) }, // list wrappers wrap(java.util.List::class) { _, addr -> objectValue(ARRAY_LIST_TYPE, addr, ListWrapper(UT_ARRAY_LIST)) }, wrap(java.util.AbstractList::class) { _, addr -> objectValue(ARRAY_LIST_TYPE, addr, ListWrapper(UT_ARRAY_LIST)) }, wrap(java.util.ArrayList::class) { _, addr -> objectValue(ARRAY_LIST_TYPE, addr, ListWrapper(UT_ARRAY_LIST)) }, - - - wrap(CopyOnWriteArrayList::class) { type, addr -> objectValue(type, addr, ListWrapper(UT_ARRAY_LIST)) }, + wrap(java.util.concurrent.CopyOnWriteArrayList::class) { type, addr -> objectValue(type, addr, ListWrapper(UT_ARRAY_LIST)) }, wrap(java.util.LinkedList::class) { _, addr -> objectValue(LINKED_LIST_TYPE, addr, ListWrapper(UT_LINKED_LIST)) }, wrap(java.util.AbstractSequentialList::class) { _, addr -> objectValue(LINKED_LIST_TYPE, addr, ListWrapper(UT_LINKED_LIST)) }, @@ -197,6 +248,10 @@ private val wrappers = mapOf( wrap(java.util.HashMap::class) { _, addr -> objectValue(HASH_MAP_TYPE, addr, MapWrapper()) }, wrap(java.util.concurrent.ConcurrentHashMap::class) { _, addr -> objectValue(HASH_MAP_TYPE, addr, MapWrapper()) }, + // iterator wrappers + wrap(java.util.Iterator::class) { _, addr -> objectValue(ITERATOR_TYPE, addr, IteratorOfListWrapper()) }, + wrap(java.util.ListIterator::class) { _, addr -> objectValue(LIST_ITERATOR_TYPE, addr, ListIteratorOfListWrapper()) }, + // 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()) }, @@ -205,12 +260,64 @@ private val wrappers = mapOf( wrap(java.util.stream.DoubleStream::class) { _, addr -> objectValue(DOUBLE_STREAM_TYPE, addr, DoubleStreamWrapper()) }, // Security-related wrappers - wrap(SecurityManager::class) { type, addr -> objectValue(type, addr, SecurityManagerWrapper()) }, -).also { + wrap(java.lang.SecurityManager::class) { type, addr -> objectValue(type, addr, SecurityManagerWrapper()) }, +).apply { + // Each wrapper has to wrap itself to make possible to create it but with the underlying type in UtMocks or in wrappers + arrayOf( + wrap(UtStringBuilder::class) { _, addr -> objectValue(STRING_BUILDER_TYPE, addr, UtStringBuilderWrapper()) }, + wrap(UtStringBuffer::class) { _, addr -> objectValue(STRING_BUFFER_TYPE, addr, UtStringBufferWrapper()) }, + wrap(UtString::class) { _, addr -> objectValue(STRING_TYPE, addr, StringWrapper()) }, + + wrap(UtOptional::class) { _, addr -> objectValue(OPTIONAL_TYPE, addr, OptionalWrapper(UT_OPTIONAL)) }, + wrap(UtOptionalInt::class) { _, addr -> objectValue(OPTIONAL_INT_TYPE, addr, OptionalWrapper(UT_OPTIONAL_INT)) }, + wrap(UtOptionalLong::class) { _, addr -> objectValue(OPTIONAL_LONG_TYPE, addr, OptionalWrapper(UT_OPTIONAL_LONG)) }, + wrap(UtOptionalDouble::class) { _, addr -> objectValue(OPTIONAL_DOUBLE_TYPE, addr, OptionalWrapper(UT_OPTIONAL_DOUBLE)) }, + + wrap(UtThread::class) { _, addr -> objectValue(THREAD_TYPE, addr, ThreadWrapper()) }, + wrap(UtThreadGroup::class) { _, addr -> objectValue(THREAD_GROUP_TYPE, addr, ThreadGroupWrapper()) }, + wrap(UtExecutorService::class) { _, addr -> objectValue(EXECUTOR_SERVICE_TYPE, addr, ExecutorServiceWrapper()) }, + wrap(UtCountDownLatch::class) { _, addr -> objectValue(COUNT_DOWN_LATCH_TYPE, addr, CountDownLatchWrapper()) }, + wrap(UtCompletableFuture::class) { _, addr -> objectValue(COMPLETABLE_FUTURE_TYPE, addr, CompletableFutureWrapper()) }, + + wrap(UtArrayList::class) { _, addr -> objectValue(ARRAY_LIST_TYPE, addr, ListWrapper(UT_ARRAY_LIST)) }, + wrap(UtLinkedList::class) { _, addr -> objectValue(LINKED_LIST_TYPE, addr, ListWrapper(UT_LINKED_LIST)) }, + wrap(UtLinkedListWithNullableCheck::class) { _, addr -> + objectValue(ARRAY_DEQUE_TYPE, addr, ListWrapper(UT_LINKED_LIST_WITH_NULLABLE_CHECK)) + }, + + wrap(UtHashSet::class) { _, addr -> objectValue(HASH_SET_TYPE, addr, SetWrapper()) }, + + wrap(UtHashMap::class) { _, addr -> objectValue(HASH_MAP_TYPE, addr, MapWrapper()) }, + + wrap(UtArrayListSimpleIterator::class) { _, addr -> objectValue(ITERATOR_TYPE, addr, IteratorOfListWrapper()) }, + wrap(UtArrayListIterator::class) { _, addr -> objectValue(LIST_ITERATOR_TYPE, addr, ListIteratorOfListWrapper()) }, + wrap(UtLinkedListIterator::class) { _, addr -> objectValue(LIST_ITERATOR_TYPE, addr, ListIteratorOfListWrapper()) }, // use ListIterator instead of simple Iterator because java.util.LinkedList may return ListIterator for `iterator` + wrap(UtReverseIterator::class) { _, addr -> objectValue(ITERATOR_TYPE, addr, ReverseIteratorWrapper()) }, + wrap(UtHashSetIterator::class) { _, addr -> objectValue(ITERATOR_TYPE, addr, IteratorOfSetWrapper()) }, + + wrap(UtStream::class) { _, addr -> objectValue(STREAM_TYPE, addr, CommonStreamWrapper()) }, + wrap(UtIntStream::class) { _, addr -> objectValue(INT_STREAM_TYPE, addr, IntStreamWrapper()) }, + wrap(UtLongStream::class) { _, addr -> objectValue(LONG_STREAM_TYPE, addr, LongStreamWrapper()) }, + wrap(UtDoubleStream::class) { _, addr -> objectValue(DOUBLE_STREAM_TYPE, addr, DoubleStreamWrapper()) }, + + wrap(UtSecurityManager::class) { _, addr -> objectValue(SECURITY_MANAGER_TYPE, addr, SecurityManagerWrapper()) }, + + wrap(RangeModifiableUnlimitedArray::class) { type, addr -> + objectValue(type, addr, RangeModifiableUnlimitedArrayWrapper()) + }, + wrap(AssociativeArray::class) { type, addr -> + objectValue(type, addr, AssociativeArrayWrapper()) + }, + ).let { putAll(it) } +}.also { // check every `wrapped` class has a corresponding value in [classToWrapper] - it.keys.all { key -> + val missedWrappers = it.keys.filterNot { key -> Scene.v().getSootClass(key.name).type in classToWrapper.keys } + + require(missedWrappers.isEmpty()) { + "Missed wrappers for classes [${missedWrappers.joinToString(", ")}]" + } } private fun wrap(kClass: KClass<*>, implementation: (RefType, UtAddrExpression) -> ObjectValue) = @@ -247,6 +354,21 @@ interface WrapperInterface { } fun value(resolver: Resolver, wrapper: ObjectValue): UtModel + + /** + * It is an index for type parameter corresponding to the result + * value of `select` operation. For example, for arrays and lists it's zero, + * for associative array it's one. + */ + val selectOperationTypeIndex: Int + get() = 0 + + /** + * Similar to [selectOperationTypeIndex], it is responsible for type index + * of the returning value from `get` operation + */ + val getOperationTypeIndex: Int + get() = 0 } // TODO: perhaps we have to have wrapper around concrete value here @@ -259,7 +381,12 @@ data class ThrowableWrapper(val throwable: Throwable) : WrapperInterface { workaround(MAKE_SYMBOLIC) { listOf( MethodResult( - createConst(method.returnType, typeRegistry.findNewSymbolicReturnValueName()) + createConst( + method.returnType, + typeRegistry.findNewSymbolicReturnValueName(), + // we don't want to mock anything returned from a throwable instance's methods + mockInfoGenerator = null + ) ) ) } 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 7fc3930ba5..7b0dd6966f 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/OptionalWrapper.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/OptionalWrapper.kt @@ -16,7 +16,6 @@ import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtExecutableCallModel import org.utbot.framework.plugin.api.UtNullModel import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.UtStatementModel import org.utbot.framework.plugin.api.classId import org.utbot.framework.plugin.api.id import org.utbot.framework.plugin.api.util.defaultValueModel @@ -65,7 +64,11 @@ class OptionalWrapper(private val utOptionalClass: UtOptionalClass) : BaseOverri ): List? { when (method.signature) { AS_OPTIONAL_METHOD_SIGNATURE -> { - return listOf(MethodResult(wrapper.copy(typeStorage = TypeStorage(method.returnType)))) + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(method.returnType) + val resultingWrapper = wrapper.copy(typeStorage = typeStorage) + val methodResult = MethodResult(resultingWrapper) + + return listOf(methodResult) } UT_OPTIONAL_EQ_GENERIC_TYPE_SIGNATURE -> { return listOf( 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 af7450b98d..809da5c78b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt @@ -7,7 +7,7 @@ import org.utbot.common.workaround import org.utbot.engine.MemoryState.CURRENT import org.utbot.engine.MemoryState.INITIAL import org.utbot.engine.MemoryState.STATIC_INITIAL -import org.utbot.engine.TypeRegistry.Companion.objectNumDimensions +import org.utbot.engine.types.TypeRegistry.Companion.objectNumDimensions import org.utbot.engine.pc.UtAddrExpression import org.utbot.engine.pc.UtArrayExpressionBase import org.utbot.engine.pc.UtArraySort @@ -55,6 +55,7 @@ import org.utbot.framework.plugin.api.UtOverflowFailure import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.framework.plugin.api.UtSandboxFailure import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation +import org.utbot.framework.plugin.api.UtTaintAnalysisFailure import org.utbot.framework.plugin.api.UtVoidModel import org.utbot.framework.plugin.api.classId import org.utbot.framework.plugin.api.id @@ -90,12 +91,32 @@ import kotlin.math.max import kotlin.math.min import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf +import org.utbot.framework.plugin.api.OverflowDetectionError +import org.utbot.framework.plugin.api.TaintAnalysisError +import org.utbot.engine.types.CLASS_REF_CLASSNAME +import org.utbot.engine.types.CLASS_REF_CLASS_ID +import org.utbot.engine.types.CLASS_REF_NUM_DIMENSIONS_DESCRIPTOR +import org.utbot.engine.types.CLASS_REF_TYPE_DESCRIPTOR +import org.utbot.engine.types.ENUM_ORDINAL +import org.utbot.engine.types.OBJECT_TYPE +import org.utbot.engine.types.STRING_TYPE +import org.utbot.engine.types.TypeRegistry +import org.utbot.engine.types.TypeResolver +import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.api.visible.UtStreamConsumingException +import org.utbot.framework.plugin.api.UtStreamConsumingFailure +import org.utbot.framework.plugin.api.util.constructor.ValueConstructor +import org.utbot.framework.plugin.api.util.fieldId +import org.utbot.framework.plugin.api.util.isStatic // hack +// IMPORTANT, if these values are used in code for analysis (e.g., in the wrappers), +// they must be a compilation time constant to avoid extra analysis. const val MAX_LIST_SIZE = 10 const val MAX_RESOLVE_LIST_SIZE = 256 const val MAX_STRING_SIZE = 40 -internal const val HARD_MAX_ARRAY_SIZE = 256 +internal const val MAX_STREAM_SIZE = 256 +internal val hardMaxArraySize = UtSettings.maxArraySize internal const val PREFERRED_ARRAY_SIZE = 2 internal const val MIN_PREFERRED_INTEGER = -256 @@ -114,12 +135,13 @@ typealias Address = Int */ class Resolver( val hierarchy: Hierarchy, - private val memory: Memory, + val memory: Memory, val typeRegistry: TypeRegistry, private val typeResolver: TypeResolver, val holder: UtSolverStatusSAT, methodUnderTest: ExecutableId, - private val softMaxArraySize: Int + private val softMaxArraySize: Int, + private val objectCounter: ObjectCounter ) { private val classLoader: ClassLoader @@ -272,8 +294,11 @@ class Resolver( // Associates mock infos by concrete addresses // Sometimes we might have two mocks with the same concrete address, and here we group their mockInfos // Then we should merge executables for mocks with the same concrete addresses with respect to the calls order - val mocks = memory - .mocks() + val memoryMocks = memory.mocks() + // TODO add a comment about why is it impossible to have nulls here + .filter { holder.concreteAddr(it.mockInfo.addr) != SYMBOLIC_NULL_ADDR } + + val mocks = memoryMocks .groupBy { enriched -> holder.concreteAddr(enriched.mockInfo.addr) } .map { (address, mockInfos) -> address to mockInfos.mergeExecutables() } @@ -283,7 +308,7 @@ class Resolver( val staticMethodMocks = mutableMapOf>() // Enriches mock info with information from callsToMocks - memory.mocks().forEach { (mockInfo, executables) -> + memoryMocks.forEach { (mockInfo, executables) -> when (mockInfo) { // Collects static field mocks differently is UtFieldMockInfo -> if (mockInfo.ownerAddr == null) { @@ -299,7 +324,7 @@ class Resolver( } // Collects instrumentation - val newInstancesInstrumentation = memory.mocks() + val newInstancesInstrumentation = memoryMocks .map { it.mockInfo } .filterIsInstance() .groupBy { it.classId } @@ -370,13 +395,19 @@ class Resolver( */ private fun SymbolicFailure.resolve(): UtExecutionFailure { val exception = concreteException() + + if (exception is UtStreamConsumingException) { + // This exception is artificial and is not really thrown + return UtStreamConsumingFailure(exception) + } + return if (explicit) { UtExplicitlyThrownException(exception, inNestedMethod) } else { - when { - // TODO SAT-1561 - exception is ArithmeticException && exception.message?.contains("overflow") == true -> UtOverflowFailure(exception) - exception is AccessControlException -> UtSandboxFailure(exception) + when (exception) { + is OverflowDetectionError -> UtOverflowFailure(exception) + is AccessControlException -> UtSandboxFailure(exception) + is TaintAnalysisError -> UtTaintAnalysisFailure(exception) else -> UtImplicitlyThrownException(exception, inNestedMethod) } } @@ -427,7 +458,7 @@ class Resolver( // if the value is Object, we have to construct array or an object depending on the number of dimensions // it is possible if we had an object and we casted it into array val constructedType = holder.constructTypeOrNull(value.addr, value.type) ?: return UtNullModel(value.type.id) - val typeStorage = TypeStorage(constructedType) + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(constructedType) return if (constructedType is ArrayType) { constructArrayModel(ArrayValue(typeStorage, value.addr)) @@ -513,16 +544,51 @@ class Resolver( if (sootClass.isLambda) { return constructLambda(concreteAddr, sootClass).also { lambda -> - lambda.capturedValues += collectFieldModels(addr, actualType).values + val collectedFieldModels = collectFieldModels(addr, actualType).toMutableMap() + + if (!lambda.lambdaMethodId.isStatic) { + val thisInstanceField = FieldId(declaringClass = sootClass.id, name = "cap0") + + if (thisInstanceField !in collectedFieldModels || collectedFieldModels[thisInstanceField] is UtNullModel) { + // Non-static lambda has to have `this` instance captured as `cap0` field that cannot be null, + // so if we do not have it as field or it is null (for example, an exception was thrown before initializing lambda), + // we need to construct `this` instance by ourselves. + // Since we do not know its fields, we create an empty object of the corresponding type that will be + // constructed in codegen using reflection. + val thisInstanceClassId = sootClass.name.substringBeforeLast("\$lambda").let { + Scene.v().getSootClass(it) + }.id + val thisInstanceModel = + UtCompositeModel(objectCounter.createNewAddr(), thisInstanceClassId, isMock = false) + + collectedFieldModels[thisInstanceField] = thisInstanceModel + } + } + + lambda.capturedValues += collectedFieldModels.values } } val clazz = classLoader.loadClass(sootClass.name) - + // If we have an actual type as an Enum instance (a direct inheritor of java.lang.Enum), + // we should construct it using information about corresponding ordinal. + // The right enum constant will be constructed due to constraints added to the solver. if (clazz.isEnum) { return constructEnum(concreteAddr, actualType, clazz) } + // But, if we have the actualType that is not a direct inheritor of java.lang.Enum, + // we have to check its ancestor instead, because we might be in a situation when + // we worked with an enum constant as a parameter, and now we have correctly calculated actual type. + // Since actualType for enums represents its instances and enum constants are not actually + // enum instances (in accordance to java.lang.Class#isEnum), we have to check + // the defaultType below instead on the actual one whether is it an Enum or not. + val defaultClazz = classLoader.loadClass(defaultType.sootClass.name) + + if (defaultClazz.isEnum) { + return constructEnum(concreteAddr, actualType, defaultClazz) + } + // check if we have mock with this address // Unfortunately we cannot do it in constructModel only cause constructTypeOrNull returns null for mocks val mockInfo = mockInfos[concreteAddr] @@ -620,7 +686,7 @@ class Resolver( val modeledNumDimensions = holder.eval(numDimensionsArray.select(addrExpression)).intValue() val classRef = classRefByName(modeledType, modeledNumDimensions) - val model = UtClassRefModel(addr, CLASS_REF_CLASS_ID, classRef) + val model = UtClassRefModel(addr, CLASS_REF_CLASS_ID, classRef.id) addConstructedModel(addr, model) return model @@ -662,12 +728,15 @@ class Resolver( builder.toString() } - return UtLambdaModel( + val lambdaModel = UtLambdaModel( id = addr, samType = samType, declaringClass = declaringClass.id, lambdaName = lambdaName ) + addConstructedModel(addr, lambdaModel) + + return lambdaModel } private fun constructEnum(addr: Address, type: RefType, clazz: Class<*>): UtEnumConstantModel { @@ -714,11 +783,16 @@ class Resolver( * Constructs a type for the addr. * * There are three options here: + * * * it successfully constructs a type suitable with defaultType and returns it; + * * * it constructs a type that cannot be assigned in a variable with the [defaultType] and we `touched` - * the [addr] during the analysis. In such case the method returns [defaultType] as a result - * if the constructed type is not an array, and in case of array it returns the [defaultType] if it has the same - * dimensions as the constructed type and its ancestors includes the constructed type, or null otherwise; + * the [addr] during the analysis. In such case we have only two options: either this object was aliased with + * an object of another type (by solver's decision) and we didn't touch it in fact, or it happened because + * of missed connection between an array type and types of its elements. For example, we might case + * array to a succ type after we `touched` it's element. In the first scenario null will be returns, in the + * second one -- a default type (that will be a subtype of actualType). + * * * it constructs a type that cannot be assigned in a variable with the [defaultType] and we did **not** `touched` * the [addr] during the analysis. It means we can create [UtNullModel] to represent such element. In such case * the method returns null as the result. @@ -813,19 +887,62 @@ class Resolver( // as const or store model. if (defaultBaseType is PrimType) return null - require(!defaultType.isJavaLangObject()) { + // In case when we have `actualType` equal to `byte` and defaultType is `java.lang.Object` + if (defaultType.isJavaLangObject() && actualType is PrimType) { + return defaultType + } + + // There is no way you have java.lang.Object as a defaultType here, since it'd mean that + // some actualType is not an inheritor of it + require(!defaultType.isJavaLangObject() || actualType is PrimType) { "Object type $defaultType is unexpected in fallback to default type" } - if (defaultType.numDimensions == 0) { + // It might happen only if we have a wrong aliasing here: some object has not been touched + // during the execution, and solver returned for him an address of already existed object + // with another type. In such case `UtNullModel` should be constructed. + if (defaultBaseType.isJavaLangObject() && actualType.numDimensions < defaultType.numDimensions) { + return null + } + + val baseTypeIsAnonymous = (actualType.baseType as? RefType)?.sootClass?.isAnonymous == true + val actualTypeIsArrayOfAnonymous = actualType is ArrayType && baseTypeIsAnonymous + + // There must be no arrays of anonymous classes + if (defaultType.isJavaLangObject() && actualTypeIsArrayOfAnonymous) { return defaultType } + // All cases with `java.lang.Object` as default base type should have been already processed + require(!defaultBaseType.isJavaLangObject()) { + "Unexpected `java.lang.Object` as a default base type" + } + val actualBaseType = actualType.baseType + // It is a tricky case: we might have a method with a parameter like `List`. + // Inside we will transform it into a wrapper with internal field `RangeModifiableArray` + // with Objects inside. Since we do not support generics for nested fields, it won't + // have type information and there will be no type constraints for its elements (because + // it contains java.lang.Objects inside). But, during the resolving, we will use type information + // from org.utbot.engine.types.TypeRegistry.getTypeStoragesForObjectTypeParameters. + // Therefore, we will encounter an object that will try to be resolved as an instance of `Integer`, + // but there will be no type constraints for it. Therefore, it might get `actualType` equal to some + // primitive type. In this case, we will return the default type (actually, it is not clear whether + // we should return null or defaultType, and maybe here some inconsistency exists). + if (actualType is PrimType && defaultType !is PrimType) { + return defaultType + } + require(actualBaseType is RefType) { "Expected RefType, but $actualBaseType found" } require(defaultBaseType is RefType) { "Expected RefType, but $defaultBaseType found" } + + // The same idea about fake aliasing. It might happen only if there have been an aliasing + // because of the solver's decision. In fact an object for which we construct type has not been + // touched during analysis. + if (actualType.numDimensions != defaultType.numDimensions) return null + val ancestors = typeResolver.findOrConstructAncestorsIncludingTypes(defaultBaseType) // This is intended to fix a specific problem. We have code: @@ -837,7 +954,27 @@ class Resolver( // when the array is ColoredPoint[], but the first element of it got type Point from the solver. // In such case here we'll have ColoredPoint as defaultType and Point as actualType. It is obvious from the example // that we can construct ColoredPoint instance instead of it with randomly filled colored-specific fields. - return defaultType.takeIf { actualBaseType in ancestors && actualType.numDimensions == defaultType.numDimensions } + // Note that it won't solve a problem when this `array[0]` has already been constructed somewhere above, + // since a model for it is already presented in cache and will be taken by addr from it. + // TODO corresponding issue https://github.com/UnitTestBot/UTBotJava/issues/1232 + if (actualBaseType in ancestors) return defaultType + + val inheritors = typeResolver.findOrConstructInheritorsIncludingTypes(defaultBaseType) + + // If we have an actual type that is not a subclass of defaultBaseType and isTouched is true, + // it means that we have encountered unexpected aliasing between an object for which we resolve its type + // and some object in the system. The reason is `isTouched` means that we processed this object + // during the analysis, therefore we create correct type constraints for it. Since we have + // inappropriate actualType here, these constraints were supposed to define type for another object, + // and, in fact, we didn't touch our object. + // For example, we have an array of two elements: Integer[] = new Integer[2], and this instance: ThisClass. + // During the execution we `touched` only the first element of the array, and we know its type. + // During resolving we found that second element of the array has set in true `isTouched` field, + // and its actualType is `ThisClass`. So, we have wrong aliasing here and can return `null` to construct + // UtNullModel instead. + if (actualBaseType !in inheritors) return null + + return null } /** @@ -990,7 +1127,9 @@ class Resolver( val constructedType = holder.constructTypeOrNull(addr, defaultType) ?: return UtNullModel(defaultType.id) if (defaultType.isJavaLangObject() && constructedType is ArrayType) { - return constructArrayModel(ArrayValue(TypeStorage(constructedType), addr)) + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(constructedType) + val arrayValue = ArrayValue(typeStorage, addr) + return constructArrayModel(arrayValue) } else { val concreteType = typeResolver.findAnyConcreteInheritorIncludingOrDefault( constructedType as RefType, @@ -1086,7 +1225,8 @@ fun Traverser.toMethodResult(value: Any?, sootType: Type): MethodResult { return when (value) { null -> asMethodResult { if (sootType is RefType) { - createObject(nullObjectAddr, sootType, useConcreteType = true) + // We don't want to mock values we took from a concrete environment + createObject(nullObjectAddr, sootType, useConcreteType = true, mockInfoGenerator = null) } else { createArray(nullObjectAddr, sootType as ArrayType, useConcreteType = true) } @@ -1122,7 +1262,15 @@ fun Traverser.toMethodResult(value: Any?, sootType: Type): MethodResult { val createdElement = if (elementType is RefType) { val className = value[it]!!.javaClass.id.name - createObject(addr, Scene.v().getRefType(className), useConcreteType = true) + // Try to take an instance of class we find in Runtime + // If it is impossible, take the default `elementType` without a concrete type instead. + val (type, useConcreteType) = + Scene.v().getRefTypeUnsafe(className) + ?.let { type -> type to true } + ?: (elementType to false) + + // We don't want to mock values we took from a concrete environment + createObject(addr, type, useConcreteType, mockInfoGenerator = null) } else { require(elementType is ArrayType) // We cannot use concrete types since we do not receive @@ -1136,19 +1284,28 @@ fun Traverser.toMethodResult(value: Any?, sootType: Type): MethodResult { else -> { workaround(RUN_CONCRETE) { val className = value.javaClass.id.name - val superclassName = value.javaClass.superclass.name + val superClass = value.javaClass.superclass val refTypeName = when { // hardcoded string is used cause class is not public className in typesOfObjectsToRecreate -> className - superclassName == PrintStream::class.qualifiedName -> superclassName + // superClass is null for Object class + superClass != null && superClass.name == PrintStream::class.qualifiedName -> superClass.name // we want to generate an unbounded symbolic variable for every unknown class as well else -> workaround(MAKE_SYMBOLIC) { className } } return asMethodResult { val addr = UtAddrExpression(mkBVConst("staticVariable${value.hashCode()}", UtInt32Sort)) - createObject(addr, Scene.v().getRefType(refTypeName), useConcreteType = true) + // Try to take a type from an object from the Runtime. + // If it is impossible, create an instance of a default `sootType` without a concrete type. + val (type, useConcreteType) = Scene.v() + .getRefTypeUnsafe(refTypeName) + ?.let { type -> type to true } + ?: (sootType as RefType to false) + + // We don't want to mock values we took from a concrete environment + createObject(addr, type, useConcreteType, mockInfoGenerator = null) } } } @@ -1160,7 +1317,7 @@ private fun Traverser.arrayToMethodResult( elementType: Type, takeElement: (Int) -> UtExpression ): MethodResult { - val updatedSize = min(size, HARD_MAX_ARRAY_SIZE) + val updatedSize = min(size, hardMaxArraySize) val newAddr = findNewAddr() val length = memory.findArrayLength(newAddr) @@ -1181,9 +1338,10 @@ private fun Traverser.arrayToMethodResult( } val memoryUpdate = MemoryUpdate( - stores = persistentListOf(simplifiedNamedStore(descriptor, newAddr, updatedArray)), - touchedChunkDescriptors = persistentSetOf(descriptor) + stores = persistentListOf(namedStore(descriptor, newAddr, updatedArray)), + touchedChunkDescriptors = persistentSetOf(descriptor), ) + return MethodResult( ArrayValue(typeStorage, newAddr), constraints.asHardConstraint(), diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/StreamWrappers.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/StreamWrappers.kt index f28937fcd0..0cef3d4473 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/StreamWrappers.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/StreamWrappers.kt @@ -13,21 +13,23 @@ 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.UtPrimitiveModel -import org.utbot.framework.plugin.api.UtStatementModel import org.utbot.framework.plugin.api.classId import org.utbot.framework.plugin.api.util.defaultValueModel import org.utbot.framework.plugin.api.util.doubleArrayClassId import org.utbot.framework.plugin.api.util.doubleClassId -import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.doubleStreamClassId import org.utbot.framework.plugin.api.util.intArrayClassId import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.intStreamClassId import org.utbot.framework.plugin.api.util.isArray import org.utbot.framework.plugin.api.util.isPrimitiveWrapper import org.utbot.framework.plugin.api.util.longArrayClassId import org.utbot.framework.plugin.api.util.longClassId +import org.utbot.framework.plugin.api.util.longStreamClassId 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.plugin.api.util.streamClassId import org.utbot.framework.util.nextModelName /** @@ -58,10 +60,10 @@ enum class UtStreamClass { val overriddenStreamClassId: ClassId get() = when (this) { - UT_STREAM -> java.util.stream.Stream::class.java.id - UT_INT_STREAM -> java.util.stream.IntStream::class.java.id - UT_LONG_STREAM -> java.util.stream.LongStream::class.java.id - UT_DOUBLE_STREAM -> java.util.stream.DoubleStream::class.java.id + UT_STREAM -> streamClassId + UT_INT_STREAM -> intStreamClassId + UT_LONG_STREAM -> longStreamClassId + UT_DOUBLE_STREAM -> doubleStreamClassId } } 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 0af61b14f8..49abe98a65 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Strings.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Strings.kt @@ -1,40 +1,28 @@ package org.utbot.engine import com.github.curiousoddman.rgxgen.RgxGen -import org.utbot.engine.overrides.strings.UtNativeString import org.utbot.engine.overrides.strings.UtString import org.utbot.engine.overrides.strings.UtStringBuffer import org.utbot.engine.overrides.strings.UtStringBuilder -import org.utbot.engine.pc.RewritingVisitor import org.utbot.engine.pc.UtAddrExpression import org.utbot.engine.pc.UtBoolExpression -import org.utbot.engine.pc.UtConvertToString import org.utbot.engine.pc.UtFalse -import org.utbot.engine.pc.UtIntSort -import org.utbot.engine.pc.UtLongSort -import org.utbot.engine.pc.UtStringCharAt -import org.utbot.engine.pc.UtStringLength -import org.utbot.engine.pc.UtStringToArray -import org.utbot.engine.pc.UtStringToInt import org.utbot.engine.pc.UtTrue -import org.utbot.engine.pc.cast import org.utbot.engine.pc.isConcrete import org.utbot.engine.pc.mkAnd import org.utbot.engine.pc.mkChar import org.utbot.engine.pc.mkEq import org.utbot.engine.pc.mkInt import org.utbot.engine.pc.mkNot -import org.utbot.engine.pc.mkString import org.utbot.engine.pc.select import org.utbot.engine.pc.toConcrete import org.utbot.engine.symbolic.asHardConstraint +import org.utbot.engine.types.STRING_TYPE 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.UtModel -import org.utbot.framework.plugin.api.UtNullModel import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.UtStatementModel import org.utbot.framework.plugin.api.classId import org.utbot.framework.plugin.api.id import org.utbot.framework.plugin.api.util.charArrayClassId @@ -43,15 +31,12 @@ import org.utbot.framework.plugin.api.util.constructorId import org.utbot.framework.plugin.api.util.defaultValueModel import org.utbot.framework.util.nextModelName import kotlin.math.max -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentSetOf import soot.CharType import soot.IntType import soot.Scene import soot.SootClass import soot.SootField import soot.SootMethod -import kotlin.reflect.KFunction4 val utStringClass: SootClass get() = Scene.v().getSootClass(UtString::class.qualifiedName) @@ -74,74 +59,18 @@ class StringWrapper : BaseOverriddenWrapper(utStringClass.name) { ): List? { return when (method.subSignature) { toStringMethodSignature -> { - listOf(MethodResult(wrapper.copy(typeStorage = TypeStorage(method.returnType)))) + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(method.returnType) + listOf(MethodResult(wrapper.copy(typeStorage = typeStorage))) } matchesMethodSignature -> { - val arg = parameters[0] as ObjectValue - val matchingLengthExpr = getIntFieldValue(arg, STRING_LENGTH).accept(RewritingVisitor()) - - if (!matchingLengthExpr.isConcrete) return null - - val matchingValueExpr = - selectArrayExpressionFromMemory(getValueArray(arg.addr)).accept(RewritingVisitor()) - val matchingLength = matchingLengthExpr.toConcrete() as Int - val matchingValue = CharArray(matchingLength) - - for (i in 0 until matchingLength) { - val charExpr = matchingValueExpr.select(mkInt(i)).accept(RewritingVisitor()) - - if (!charExpr.isConcrete) return null - - matchingValue[i] = (charExpr.toConcrete() as Number).toChar() - } - - val rgxGen = RgxGen(String(matchingValue)) - val matching = (rgxGen.generate()) - val notMatching = rgxGen.generateNotMatching() - - val thisLength = getIntFieldValue(wrapper, STRING_LENGTH) - val thisValue = selectArrayExpressionFromMemory(getValueArray(wrapper.addr)) - - val matchingConstraints = mutableSetOf() - matchingConstraints += mkEq(thisLength, mkInt(matching.length)) - for (i in matching.indices) { - matchingConstraints += mkEq(thisValue.select(mkInt(i)), mkChar(matching[i])) - } - - val notMatchingConstraints = mutableSetOf() - notMatchingConstraints += mkEq(thisLength, mkInt(notMatching.length)) - for (i in notMatching.indices) { - notMatchingConstraints += mkEq(thisValue.select(mkInt(i)), mkChar(notMatching[i])) - } - - return listOf( - MethodResult(UtTrue.toBoolValue(), matchingConstraints.asHardConstraint()), - MethodResult(UtFalse.toBoolValue(), notMatchingConstraints.asHardConstraint()) - ) + symbolicMatchesMethodImpl(wrapper, parameters) } charAtMethodSignature -> { - val index = parameters[0] as PrimitiveValue - val lengthExpr = getIntFieldValue(wrapper, STRING_LENGTH) - val inBoundsCondition = mkAnd(Le(0.toPrimitiveValue(), index), Lt(index, lengthExpr.toIntValue())) - val failMethodResult = - MethodResult( - explicitThrown( - StringIndexOutOfBoundsException(), - findNewAddr(), - environment.state.isInNestedMethod() - ), - hardConstraints = mkNot(inBoundsCondition).asHardConstraint() - ) - - val valueExpr = selectArrayExpressionFromMemory(getValueArray(wrapper.addr)) - - val returnResult = MethodResult( - valueExpr.select(index.expr).toCharValue(), - hardConstraints = inBoundsCondition.asHardConstraint() - ) - return listOf(returnResult, failMethodResult) + symbolicCharAtMethodImpl(wrapper, parameters) + } + else -> { + null } - else -> return null } } @@ -186,200 +115,79 @@ class StringWrapper : BaseOverriddenWrapper(utStringClass.name) { ) return UtAssembleModel(addr, classId, modelName, instantiationCall) } -} - -internal val utNativeStringClass = Scene.v().getSootClass(UtNativeString::class.qualifiedName) -private var stringNameIndex = 0 -private fun nextStringName() = "\$string${stringNameIndex++}" - -class UtNativeStringWrapper : WrapperInterface { - override fun isWrappedMethod(method: SootMethod): Boolean = method.subSignature in wrappedMethods - - override operator fun Traverser.invoke( + private fun Traverser.symbolicMatchesMethodImpl( wrapper: ObjectValue, - method: SootMethod, parameters: List - ): List { - val wrappedMethodResult = wrappedMethods[method.subSignature] - ?: error("unknown wrapped method ${method.subSignature} for ${this::class}") - - return wrappedMethodResult(this, wrapper, method, parameters) - } - - @Suppress("UNUSED_PARAMETER") - private fun defaultInitMethodWrapper( - traverser: Traverser, - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List = - with(traverser) { - val newString = mkString(nextStringName()) - - val memoryUpdate = MemoryUpdate( - stores = persistentListOf(simplifiedNamedStore(valueDescriptor, wrapper.addr, newString)), - touchedChunkDescriptors = persistentSetOf(valueDescriptor) - ) - - listOf( - MethodResult( - SymbolicSuccess(voidValue), - memoryUpdates = memoryUpdate - ) - ) - } - - @Suppress("UNUSED_PARAMETER") - private fun initFromIntMethodWrapper( - traverser: Traverser, - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List = - with(traverser) { - val newString = UtConvertToString((parameters[0] as PrimitiveValue).expr) - val memoryUpdate = MemoryUpdate( - stores = persistentListOf(simplifiedNamedStore(valueDescriptor, wrapper.addr, newString)), - touchedChunkDescriptors = persistentSetOf(valueDescriptor) - ) + ): List? { + val arg = parameters[0] as ObjectValue + val matchingLengthExpr = getIntFieldValue(arg, STRING_LENGTH).accept(this.simplificator) - listOf( - MethodResult( - SymbolicSuccess(voidValue), - memoryUpdates = memoryUpdate - ) - ) - } + if (!matchingLengthExpr.isConcrete) return null - @Suppress("UNUSED_PARAMETER") - private fun initFromLongMethodWrapper( - traverser: Traverser, - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List = - with(traverser) { - val newString = UtConvertToString((parameters[0] as PrimitiveValue).expr) - val memoryUpdate = MemoryUpdate( - stores = persistentListOf(simplifiedNamedStore(valueDescriptor, wrapper.addr, newString)), - touchedChunkDescriptors = persistentSetOf(valueDescriptor) - ) + val matchingValueExpr = + selectArrayExpressionFromMemory(getValueArray(arg.addr)).accept(this.simplificator) + val matchingLength = matchingLengthExpr.toConcrete() as Int + val matchingValue = CharArray(matchingLength) - listOf( - MethodResult( - SymbolicSuccess(voidValue), - memoryUpdates = memoryUpdate - ) - ) - } + for (i in 0 until matchingLength) { + val charExpr = matchingValueExpr.select(mkInt(i)).accept(this.simplificator) - @Suppress("UNUSED_PARAMETER") - private fun lengthMethodWrapper( - traverser: Traverser, - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List = - with(traverser) { - val result = UtStringLength(memory.nativeStringValue(wrapper.addr)) + if (!charExpr.isConcrete) return null - listOf(MethodResult(SymbolicSuccess(result.toByteValue().cast(IntType.v())))) + matchingValue[i] = (charExpr.toConcrete() as Number).toChar() } - @Suppress("UNUSED_PARAMETER") - private fun charAtMethodWrapper( - traverser: Traverser, - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List = - with(traverser) { - val index = (parameters[0] as PrimitiveValue).expr - val result = UtStringCharAt(memory.nativeStringValue(wrapper.addr), index) - - listOf(MethodResult(SymbolicSuccess(result.toCharValue()))) - } + val rgxGen = RgxGen(String(matchingValue)) + val matching = rgxGen.generate() + val notMatching = rgxGen.generateNotMatching() - @Suppress("UNUSED_PARAMETER") - private fun codePointAtMethodWrapper( - traverser: Traverser, - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List = - with(traverser) { - val index = (parameters[0] as PrimitiveValue).expr - val result = UtStringCharAt(memory.nativeStringValue(wrapper.addr), index) + val thisLength = getIntFieldValue(wrapper, STRING_LENGTH) + val thisValue = selectArrayExpressionFromMemory(getValueArray(wrapper.addr)) - listOf(MethodResult(SymbolicSuccess(result.toCharValue().cast(IntType.v())))) + val matchingConstraints = mutableSetOf() + matchingConstraints += mkEq(thisLength, mkInt(matching.length)) + for (i in matching.indices) { + matchingConstraints += mkEq(thisValue.select(mkInt(i)), mkChar(matching[i])) } - @Suppress("UNUSED_PARAMETER") - private fun toIntegerMethodWrapper( - traverser: Traverser, - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List = - with(traverser) { - val result = UtStringToInt(memory.nativeStringValue(wrapper.addr), UtIntSort) - - listOf(MethodResult(SymbolicSuccess(result.toIntValue()))) + val notMatchingConstraints = mutableSetOf() + notMatchingConstraints += mkEq(thisLength, mkInt(notMatching.length)) + for (i in notMatching.indices) { + notMatchingConstraints += mkEq(thisValue.select(mkInt(i)), mkChar(notMatching[i])) } - @Suppress("UNUSED_PARAMETER") - private fun toLongMethodWrapper( - traverser: Traverser, - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List = - with(traverser) { - val result = UtStringToInt(memory.nativeStringValue(wrapper.addr), UtLongSort) - - listOf(MethodResult(SymbolicSuccess(result.toLongValue()))) - } + return listOf( + MethodResult(UtTrue.toBoolValue(), matchingConstraints.asHardConstraint()), + MethodResult(UtFalse.toBoolValue(), notMatchingConstraints.asHardConstraint()) + ) + } - @Suppress("UNUSED_PARAMETER") - private fun toCharArrayMethodWrapper( - traverser: Traverser, + private fun Traverser.symbolicCharAtMethodImpl( wrapper: ObjectValue, - method: SootMethod, parameters: List - ): List = - with(traverser) { - val stringExpression = memory.nativeStringValue(wrapper.addr) - val result = UtStringToArray(stringExpression, parameters[0] as PrimitiveValue) - val length = UtStringLength(stringExpression) - val type = CharType.v() - val arrayType = type.arrayType - val arrayValue = createNewArray(length.toIntValue(), arrayType, type) - - listOf( - MethodResult( - SymbolicSuccess(arrayValue), - memoryUpdates = arrayUpdateWithValue(arrayValue.addr, arrayType, result) - ) + ): List { + val index = parameters[0] as PrimitiveValue + val lengthExpr = getIntFieldValue(wrapper, STRING_LENGTH) + val inBoundsCondition = mkAnd(Le(0.toPrimitiveValue(), index), Lt(index, lengthExpr.toIntValue())) + val failMethodResult = + MethodResult( + explicitThrown( + StringIndexOutOfBoundsException(), + findNewAddr(), + environment.state.isInNestedMethod() + ), + hardConstraints = mkNot(inBoundsCondition).asHardConstraint() ) - } - override val wrappedMethods: Map = - mapOf( - "void ()" to ::defaultInitMethodWrapper, - "void (int)" to ::initFromIntMethodWrapper, - "void (long)" to ::initFromLongMethodWrapper, - "int length()" to ::lengthMethodWrapper, - "char charAt(int)" to ::charAtMethodWrapper, - "int codePointAt(int)" to ::codePointAtMethodWrapper, - "int toInteger()" to ::toIntegerMethodWrapper, - "long toLong()" to ::toLongMethodWrapper, - "char[] toCharArray(int)" to ::toCharArrayMethodWrapper, - ) + val valueExpr = selectArrayExpressionFromMemory(getValueArray(wrapper.addr)) - private val valueDescriptor = NATIVE_STRING_VALUE_DESCRIPTOR - - override fun value(resolver: Resolver, wrapper: ObjectValue): UtModel = UtNullModel(STRING_TYPE.classId) + val returnResult = MethodResult( + valueExpr.select(index.expr).toCharValue(), + hardConstraints = inBoundsCondition.asHardConstraint() + ) + return listOf(returnResult, failMethodResult) + } } sealed class UtAbstractStringBuilderWrapper(className: String) : BaseOverriddenWrapper(className) { @@ -392,7 +200,11 @@ sealed class UtAbstractStringBuilderWrapper(className: String) : BaseOverriddenW parameters: List ): List? { if (method.subSignature == asStringBuilderMethodSignature) { - return listOf(MethodResult(wrapper.copy(typeStorage = TypeStorage(method.returnType)))) + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(method.returnType) + val resultingWrapper = wrapper.copy(typeStorage = typeStorage) + val methodResult = MethodResult(resultingWrapper) + + return listOf(methodResult) } return null @@ -408,7 +220,7 @@ sealed class UtAbstractStringBuilderWrapper(className: String) : BaseOverriddenW val arrayValuesChunkId = typeRegistry.arrayChunkId(charArrayType) val valuesFieldChunkId = hierarchy.chunkIdForField(overriddenClass.type, overriddenClass.valueField) - val valuesArrayAddrDescriptor = MemoryChunkDescriptor(valuesFieldChunkId, wrapper.type, charType) + val valuesArrayAddrDescriptor = MemoryChunkDescriptor(valuesFieldChunkId, wrapper.type, charArrayType) val valuesArrayAddr = findArray(valuesArrayAddrDescriptor, MemoryState.CURRENT).select(wrapper.addr) val valuesArrayDescriptor = MemoryChunkDescriptor(arrayValuesChunkId, charArrayType, charType) diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/SymbolicValue.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/SymbolicValue.kt index 808ea96eee..a7c8e960c0 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/SymbolicValue.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/SymbolicValue.kt @@ -51,7 +51,7 @@ data class PrimitiveValue( val expr: UtExpression, override val concrete: Concrete? = null ) : SymbolicValue() { - constructor(type: Type, expr: UtExpression) : this(TypeStorage(type), expr) + constructor(type: Type, expr: UtExpression) : this(TypeStorage.constructTypeStorageWithSingleType(type), expr) override val type get() = typeStorage.leastCommonType @@ -166,6 +166,12 @@ val SymbolicValue.addr is PrimitiveValue -> error("PrimitiveValue $this doesn't have an address") } +val SymbolicValue.addrOrNull + get() = when (this) { + is ReferenceValue -> addr + is PrimitiveValue -> null + } + val SymbolicValue.isConcrete: Boolean get() = when (this) { is PrimitiveValue -> this.expr.isConcrete @@ -179,7 +185,7 @@ fun SymbolicValue.toConcrete(): Any = when (this) { // TODO: one more constructor? fun objectValue(type: RefType, addr: UtAddrExpression, implementation: WrapperInterface) = - ObjectValue(TypeStorage(type), addr, Concrete(implementation)) + ObjectValue(TypeStorage.constructTypeStorageWithSingleType(type), addr, Concrete(implementation)) val voidValue get() = PrimitiveValue(VoidType.v(), nullObjectAddr) diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ThreadWrappers.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ThreadWrappers.kt new file mode 100644 index 0000000000..bb5bd68a23 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ThreadWrappers.kt @@ -0,0 +1,236 @@ +package org.utbot.engine + +import org.utbot.engine.overrides.threads.UtCompletableFuture +import org.utbot.engine.overrides.threads.UtCountDownLatch +import org.utbot.engine.overrides.threads.UtExecutorService +import org.utbot.engine.overrides.threads.UtThread +import org.utbot.engine.overrides.threads.UtThreadGroup +import org.utbot.engine.symbolic.asHardConstraint +import org.utbot.engine.types.COMPLETABLE_FUTURE_TYPE +import org.utbot.engine.types.COUNT_DOWN_LATCH_TYPE +import org.utbot.engine.types.EXECUTORS_TYPE +import org.utbot.engine.types.OBJECT_TYPE +import org.utbot.engine.types.STRING_TYPE +import org.utbot.engine.types.THREAD_GROUP_TYPE +import org.utbot.engine.types.THREAD_TYPE +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtLambdaModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.id +import org.utbot.framework.plugin.api.util.constructorId +import org.utbot.framework.plugin.api.util.defaultValueModel +import org.utbot.framework.plugin.api.util.fieldId +import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.framework.util.executableId +import org.utbot.framework.util.nextModelName +import soot.IntType +import soot.RefType +import soot.Scene +import soot.SootClass +import soot.SootMethod + +val utThreadClass: SootClass + get() = Scene.v().getSootClass(UtThread::class.qualifiedName) +val utThreadGroupClass: SootClass + get() = Scene.v().getSootClass(UtThreadGroup::class.qualifiedName) +val utCompletableFutureClass: SootClass + get() = Scene.v().getSootClass(UtCompletableFuture::class.qualifiedName) +val utExecutorServiceClass: SootClass + get() = Scene.v().getSootClass(UtExecutorService::class.qualifiedName) +val utCountDownLatchClass: SootClass + get() = Scene.v().getSootClass(UtCountDownLatch::class.qualifiedName) + +class ThreadWrapper : BaseOverriddenWrapper(utThreadClass.name) { + override fun Traverser.overrideInvoke( + wrapper: ObjectValue, + method: SootMethod, + parameters: List + ): List? = null + + override fun value(resolver: Resolver, wrapper: ObjectValue): UtModel = + resolver.run { + val classId = THREAD_TYPE.id + val addr = holder.concreteAddr(wrapper.addr) + val modelName = nextModelName("thread") + + val values = collectFieldModels(wrapper.addr, overriddenClass.type) + val targetModel = values[targetFieldId] as? UtLambdaModel + + val (constructor, params) = if (targetModel == null) { + constructorId(classId) to emptyList() + } else { + constructorId(classId, runnableType.id) to listOf(targetModel) + } + + val instantiationCall = UtExecutableCallModel( + instance = null, + constructor, + params + ) + + return UtAssembleModel(addr, classId, modelName, instantiationCall) + } + + companion object { + private val runnableType: RefType + get() = Scene.v().getSootClass(Runnable::class.qualifiedName).type!! + private val targetFieldId: FieldId + get() = utThreadClass.getField("target", runnableType).fieldId + } +} + +class ThreadGroupWrapper : BaseOverriddenWrapper(utThreadGroupClass.name) { + override fun Traverser.overrideInvoke( + wrapper: ObjectValue, + method: SootMethod, + parameters: List + ): List? = null + + override fun value(resolver: Resolver, wrapper: ObjectValue): UtModel = + resolver.run { + val classId = THREAD_GROUP_TYPE.id + val addr = holder.concreteAddr(wrapper.addr) + val modelName = nextModelName("threadGroup") + + val values = collectFieldModels(wrapper.addr, overriddenClass.type) + val nameModel = values[nameFieldId] ?: UtNullModel(stringClassId) + + val instantiationCall = UtExecutableCallModel( + instance = null, + constructorId(classId, stringClassId), + listOf(nameModel) + ) + + return UtAssembleModel(addr, classId, modelName, instantiationCall) + } + + companion object { + private val nameFieldId: FieldId + get() = utThreadGroupClass.getField("name", STRING_TYPE).fieldId + } +} + +private val TO_COMPLETABLE_FUTURE_SIGNATURE: String = + utCompletableFutureClass.getMethodByName(UtCompletableFuture<*>::toCompletableFuture.name).signature +private val UT_COMPLETABLE_FUTURE_EQ_GENERIC_TYPE_SIGNATURE: String = + utCompletableFutureClass.getMethodByName(UtCompletableFuture<*>::eqGenericType.name).signature + +class CompletableFutureWrapper : BaseOverriddenWrapper(utCompletableFutureClass.name) { + override fun Traverser.overrideInvoke( + wrapper: ObjectValue, + method: SootMethod, + parameters: List + ): List? = + when (method.signature) { + TO_COMPLETABLE_FUTURE_SIGNATURE -> { + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(method.returnType) + val resultingWrapper = wrapper.copy(typeStorage = typeStorage) + val methodResult = MethodResult(resultingWrapper) + + listOf(methodResult) + } + UT_COMPLETABLE_FUTURE_EQ_GENERIC_TYPE_SIGNATURE -> { + val firstParameter = parameters.single() + val genericTypeParameterTypeConstraint = typeRegistry.typeConstraintToGenericTypeParameter( + firstParameter.addr, + wrapper.addr, + i = 0 + ) + val methodResult = MethodResult( + firstParameter, + genericTypeParameterTypeConstraint.asHardConstraint() + ) + + listOf(methodResult) + } + else -> null + } + + override fun value(resolver: Resolver, wrapper: ObjectValue): UtModel = + resolver.run { + val classId = COMPLETABLE_FUTURE_TYPE.id + val addr = holder.concreteAddr(wrapper.addr) + val modelName = nextModelName("completableFuture") + + val values = collectFieldModels(wrapper.addr, overriddenClass.type) + val resultModel = values[resultFieldId] ?: UtNullModel(objectClassId) + + val instantiationCall = UtExecutableCallModel( + instance = null, + constructorId(classId, objectClassId), + listOf(resultModel) + ) + + return UtAssembleModel(addr, classId, modelName, instantiationCall) + } + + companion object { + private val resultFieldId: FieldId + get() = utCompletableFutureClass.getField("result", OBJECT_TYPE).fieldId + } +} + +class ExecutorServiceWrapper : BaseOverriddenWrapper(utExecutorServiceClass.name) { + override fun Traverser.overrideInvoke( + wrapper: ObjectValue, + method: SootMethod, + parameters: List + ): List? = null + + override fun value(resolver: Resolver, wrapper: ObjectValue): UtModel = + resolver.run { + val classId = EXECUTORS_TYPE.id + val addr = holder.concreteAddr(wrapper.addr) + val modelName = nextModelName("executorService") + + val instantiationCall = UtExecutableCallModel( + instance = null, + newSingleThreadExecutorMethod, + emptyList() + ) + + return UtAssembleModel(addr, classId, modelName, instantiationCall) + } + + companion object { + val newSingleThreadExecutorMethod: ExecutableId + get() = EXECUTORS_TYPE.sootClass.getMethod("newSingleThreadExecutor", emptyList()).executableId + } +} + +class CountDownLatchWrapper : BaseOverriddenWrapper(utCountDownLatchClass.name) { + override fun Traverser.overrideInvoke( + wrapper: ObjectValue, + method: SootMethod, + parameters: List + ): List? = null + + override fun value(resolver: Resolver, wrapper: ObjectValue): UtModel = + resolver.run { + val classId = COUNT_DOWN_LATCH_TYPE.id + val addr = holder.concreteAddr(wrapper.addr) + val modelName = nextModelName("countDownLatch") + + val values = collectFieldModels(wrapper.addr, overriddenClass.type) + val countModel = values[countFieldId] ?: intClassId.defaultValueModel() + + val instantiationCall = UtExecutableCallModel( + instance = null, + constructorId(classId, intClassId), + listOf(countModel) + ) + + return UtAssembleModel(addr, classId, modelName, instantiationCall) + } + + companion object { + private val countFieldId: FieldId + get() = utCountDownLatchClass.getField("count", IntType.v()).fieldId + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/TraversalContext.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/TraversalContext.kt index 22a9c19304..99fa3836e5 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/TraversalContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/TraversalContext.kt @@ -1,5 +1,7 @@ package org.utbot.engine +import org.utbot.engine.state.ExecutionState + /** * Represents a mutable _Context_ during the [ExecutionState] traversing. This _Context_ consists of all mutable and * immutable properties and fields which are created and updated during analysis of a **single** Jimple instruction. diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt index 28d83e6ce4..33d9ca61ce 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt @@ -5,7 +5,12 @@ import kotlinx.collections.immutable.persistentHashSetOf 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 mu.KotlinLogging +import org.utbot.framework.plugin.api.ArtificialError +import org.utbot.framework.plugin.api.OverflowDetectionError +import org.utbot.framework.plugin.api.TaintAnalysisError import org.utbot.common.WorkaroundReason.HACK import org.utbot.framework.UtSettings.ignoreStaticsFromTrustedLibraries import org.utbot.common.WorkaroundReason.IGNORE_STATICS_FROM_TRUSTED_LIBRARIES @@ -16,6 +21,7 @@ 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.Simplificator import org.utbot.engine.pc.UtAddNoOverflowExpression import org.utbot.engine.pc.UtAddrExpression import org.utbot.engine.pc.UtAndBoolExpression @@ -66,6 +72,16 @@ import org.utbot.engine.pc.mkNot import org.utbot.engine.pc.mkOr import org.utbot.engine.pc.select import org.utbot.engine.pc.store +import org.utbot.engine.state.Edge +import org.utbot.engine.state.ExecutionState +import org.utbot.engine.state.LocalVariableMemory +import org.utbot.engine.state.StateLabel +import org.utbot.engine.state.createExceptionState +import org.utbot.engine.state.pop +import org.utbot.engine.state.push +import org.utbot.engine.state.update +import org.utbot.engine.state.withLabel +import org.utbot.engine.symbolic.HardConstraint import org.utbot.engine.symbolic.emptyAssumption import org.utbot.engine.symbolic.emptyHardConstraint import org.utbot.engine.symbolic.emptySoftConstraint @@ -74,32 +90,52 @@ 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.trusted.isFromTrustedLibrary +import org.utbot.engine.simplificators.MemoryUpdateSimplificator +import org.utbot.engine.simplificators.simplifySymbolicStateUpdate +import org.utbot.engine.simplificators.simplifySymbolicValue +import org.utbot.engine.types.* +import org.utbot.engine.types.ARRAYS_SOOT_CLASS +import org.utbot.engine.types.CLASS_REF_SOOT_CLASS +import org.utbot.engine.types.CLASS_REF_TYPE +import org.utbot.engine.types.ENUM_ORDINAL +import org.utbot.engine.types.EQUALS_SIGNATURE +import org.utbot.engine.types.HASHCODE_SIGNATURE +import org.utbot.engine.types.METHOD_FILTER_MAP_FIELD_SIGNATURE +import org.utbot.engine.types.NEW_INSTANCE_SIGNATURE +import org.utbot.engine.types.NUMBER_OF_PREFERRED_TYPES +import org.utbot.engine.types.OBJECT_TYPE +import org.utbot.engine.types.SECURITY_FIELD_SIGNATURE 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.ExploreThrowableDepth import org.utbot.framework.UtSettings -import org.utbot.framework.UtSettings.maximizeCoverageUsingReflection import org.utbot.framework.UtSettings.preferredCexOption import org.utbot.framework.UtSettings.substituteStaticsWithSymbolicVariable +import org.utbot.framework.isFromTrustedLibrary +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.context.NonNullSpeculator +import org.utbot.framework.context.TypeReplacer import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.TypeReplacementMode.AnyImplementor +import org.utbot.framework.plugin.api.TypeReplacementMode.KnownImplementor +import org.utbot.framework.plugin.api.TypeReplacementMode.NoImplementors import org.utbot.framework.plugin.api.classId import org.utbot.framework.plugin.api.id -import org.utbot.framework.plugin.api.util.executable -import org.utbot.framework.plugin.api.util.jField -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.isConstructor -import org.utbot.framework.plugin.api.util.utContext +import org.utbot.framework.plugin.api.isAbstractType +import org.utbot.framework.plugin.api.util.* import org.utbot.framework.util.executableId import org.utbot.framework.util.graph -import org.utbot.framework.util.isInaccessibleViaReflection +import org.utbot.summary.ast.declaredClassName +import org.utbot.framework.util.sootMethodOrNull +import org.utbot.taint.TaintContext +import org.utbot.taint.model.* import java.lang.reflect.ParameterizedType import kotlin.collections.plus import kotlin.collections.plusAssign @@ -193,11 +229,9 @@ import soot.toolkits.graph.ExceptionalUnitGraph import java.lang.reflect.GenericArrayType import java.lang.reflect.TypeVariable import java.lang.reflect.WildcardType -import java.util.concurrent.atomic.AtomicInteger -import kotlin.reflect.full.instanceParameter -import kotlin.reflect.jvm.javaType private val CAUGHT_EXCEPTION = LocalVariable("@caughtexception") +private val logger = KotlinLogging.logger {} class Traverser( private val methodUnderTest: ExecutableId, @@ -207,6 +241,9 @@ class Traverser( internal val typeResolver: TypeResolver, private val globalGraph: InterProceduralUnitGraph, private val mocker: Mocker, + private val typeReplacer: TypeReplacer, + private val nonNullSpeculator: NonNullSpeculator, + private val taintContext: TaintContext, ) : UtContextInitializer() { private val visitedStmts: MutableSet = mutableSetOf() @@ -231,16 +268,20 @@ class Traverser( /** * Contains information about the generic types used in the parameters of the method under test. + * + * Mutable set here is required since this object might be passed into several methods + * and get several piece of information about their parameterized types */ - private val parameterAddrToGenericType = mutableMapOf() + private val instanceAddrToGenericType = mutableMapOf>() private val preferredCexInstanceCache = mutableMapOf>() private var queuedSymbolicStateUpdates = SymbolicStateUpdate() - private val objectCounter = AtomicInteger(TypeRegistry.objectCounterInitialValue) + internal val objectCounter = ObjectCounter(TypeRegistry.objectCounterInitialValue) + private fun findNewAddr(insideStaticInitializer: Boolean): UtAddrExpression { - val newAddr = objectCounter.getAndIncrement() + val newAddr = objectCounter.createNewAddr() // return negative address for objects created inside static initializer // to make their address space be intersected with address space of // parameters of method under symbolic execution @@ -250,6 +291,8 @@ class Traverser( } internal fun findNewAddr() = findNewAddr(environment.state.isInsideStaticInitializer).also { touchAddress(it) } + private val dynamicInvokeResolver: DynamicInvokeResolver = DelegatingDynamicInvokeResolver() + // Counter used for a creation symbolic results of "hashcode" and "equals" methods. private var equalsCounter = 0 private var hashcodeCounter = 0 @@ -291,6 +334,9 @@ class Traverser( return context.nextStates } + internal val simplificator = Simplificator() + private val memoryUpdateSimplificator = MemoryUpdateSimplificator(simplificator) + private fun TraversalContext.traverseStmt(current: Stmt) { if (doPreparatoryWorkIfRequired(current)) return @@ -304,17 +350,18 @@ class Traverser( is JReturnVoidStmt -> processResult(SymbolicSuccess(voidValue)) is JRetStmt -> error("This one should be already removed by Soot: $current") is JThrowStmt -> traverseThrowStmt(current) - is JBreakpointStmt -> offerState(environment.state.updateQueued(globalGraph.succ(current))) - is JGotoStmt -> offerState(environment.state.updateQueued(globalGraph.succ(current))) - is JNopStmt -> offerState(environment.state.updateQueued(globalGraph.succ(current))) - is MonitorStmt -> offerState(environment.state.updateQueued(globalGraph.succ(current))) + is JBreakpointStmt -> offerState(updateQueued(globalGraph.succ(current))) + is JGotoStmt -> offerState(updateQueued(globalGraph.succ(current))) + is JNopStmt -> offerState(updateQueued(globalGraph.succ(current))) + is MonitorStmt -> offerState(updateQueued(globalGraph.succ(current))) is DefinitionStmt -> TODO("$current") else -> error("Unsupported: ${current::class}") } } /** - * Handles preparatory work for static initializers and multi-dimensional arrays creation. + * Handles preparatory work for static initializers, multi-dimensional arrays creation + * and `newInstance` reflection call post-processing. * * For instance, it could push handmade graph with preparation statements to the path selector. * @@ -330,6 +377,7 @@ class Traverser( return when { processStaticInitializerIfRequired(current) -> true unfoldMultiArrayExprIfRequired(current) -> true + pushInitGraphAfterNewInstanceReflectionCall(current) -> true else -> false } } @@ -385,6 +433,53 @@ class Traverser( return true } + /** + * If the previous stms was `newInstance` method invocation, + * pushes a graph of the default constructor of the constructed type, if present, + * and pushes a state with a [InstantiationException] otherwise. + */ + private fun TraversalContext.pushInitGraphAfterNewInstanceReflectionCall(stmt: JAssignStmt): Boolean { + // Check whether the previous stmt was a `newInstance` invocation + val lastStmt = environment.state.path.lastOrNull() as? JAssignStmt ?: return false + if (!lastStmt.containsInvokeExpr()) { + return false + } + + val lastMethodInvocation = lastStmt.invokeExpr.method + if (lastMethodInvocation.subSignature != NEW_INSTANCE_SIGNATURE) { + return false + } + + // Process the current stmt as cast expression + val right = stmt.rightOp as? JCastExpr ?: return false + val castType = right.castType as? RefType ?: return false + val castedJimpleVariable = right.op as? JimpleLocal ?: return false + + val castedLocalVariable = (localVariableMemory.local(castedJimpleVariable.variable) as? ReferenceValue) ?: return false + + val castSootClass = castType.sootClass + + // We need to consider a situation when this class does not have a default constructor + // Since it can be a cast of a class with constructor to the interface (or ot the ancestor without default constructor), + // we cannot always throw a `java.lang.InstantiationException`. + // So, instead we will just continue the analysis without analysis of . + val initMethod = castSootClass.getMethodUnsafe("void ()") ?: return false + + if (!initMethod.canRetrieveBody()) { + return false + } + + val initGraph = ExceptionalUnitGraph(initMethod.activeBody) + + pushToPathSelector( + initGraph, + castedLocalVariable, + callParameters = emptyList(), + ) + + return true + } + /** * Processes static initialization for class. * @@ -400,10 +495,23 @@ class Traverser( fieldRef: StaticFieldRef, stmt: Stmt ): Boolean { + // This order of processing options is important. + // First, we should process classes that + // cannot be analyzed without clinit sections, e.g., enums if (shouldProcessStaticFieldConcretely(fieldRef)) { return processStaticFieldConcretely(fieldRef, stmt) } + // Then we should check if we should analyze clinit sections at all + if (!UtSettings.enableClinitSectionsAnalysis) { + return false + } + + // Finally, we decide whether we should analyze clinit sections concretely or not + if (UtSettings.processAllClinitSectionsConcretely) { + return processStaticFieldConcretely(fieldRef, stmt) + } + val field = fieldRef.field val declaringClass = field.declaringClass val declaringClassId = declaringClass.id @@ -427,7 +535,7 @@ class Traverser( // This branch could be useful if we have a static field, i.e. x = 5 / 0 is SymbolicFailure -> traverseException(stmt, result.symbolicResult) is SymbolicSuccess -> offerState( - environment.state.updateQueued( + updateQueued( environment.state.lastEdge!!, result.symbolicStateUpdate ) @@ -514,7 +622,7 @@ class Traverser( // $r0 = ; val edge = environment.state.lastEdge ?: globalGraph.succ(stmt) - val newState = environment.state.updateQueued(edge, updates) + val newState = updateQueued(edge, updates) offerState(newState) return true @@ -526,8 +634,8 @@ class Traverser( ): Pair { val jClass = type.id.jClass - // symbolic value for enum class itself - val enumClassValue = findOrCreateStaticObject(type) + // symbolic value for enum class itself, we don't want to have mocks for this value + val enumClassValue = findOrCreateStaticObject(type, mockInfoGenerator = null) // values for enum constants val enumConstantConcreteValues = jClass.enumConstants.filterIsInstance>() @@ -600,8 +708,10 @@ class Traverser( val initializedFieldUpdate = MemoryUpdate(initializedStaticFields = persistentHashSetOf(fieldId)) + val mockInfoGenerator = UtMockInfoGenerator { addr -> UtStaticObjectMockInfo(declaringClass.id, addr) } + val objectUpdate = objectUpdate( - instance = findOrCreateStaticObject(declaringClass.type), + instance = findOrCreateStaticObject(declaringClass.type, mockInfoGenerator), field = field, value = valueToExpression(symbolicValue, field.type) ) @@ -662,34 +772,41 @@ class Traverser( } private fun isStaticInstanceInMethodResult(id: ClassId, methodResult: MethodResult?) = - methodResult != null && id in methodResult.memoryUpdates.staticInstanceStorage + methodResult != null && id in methodResult.symbolicStateUpdate.memoryUpdates.staticInstanceStorage private fun TraversalContext.skipVerticesForThrowableCreation(current: JAssignStmt) { val rightType = current.rightOp.type as RefType - val exceptionType = Scene.v().getSootClass(rightType.className).type - val createdException = createObject(findNewAddr(), exceptionType, true) + val exceptionType = Scene.v().getRefType(rightType.className) + val mockInfoGenerator = UtMockInfoGenerator { mockAddr -> + UtNewInstanceMockInfo(exceptionType.id, mockAddr, environment.method.declaringClass.id) + } + val createdException = createObject( + findNewAddr(), + exceptionType, + useConcreteType = true, + mockInfoGenerator = mockInfoGenerator + ) val currentExceptionJimpleLocal = current.leftOp as JimpleLocal queuedSymbolicStateUpdates += localMemoryUpdate(currentExceptionJimpleLocal.variable to createdException) // mark the rest of the path leading to the '' statement as covered do { - environment.state = environment.state.updateQueued(globalGraph.succ(environment.state.stmt)) + environment.state = updateQueued(globalGraph.succ(environment.state.stmt)) globalGraph.visitEdge(environment.state.lastEdge!!) globalGraph.visitNode(environment.state) } while (!environment.state.stmt.isConstructorCall(currentExceptionJimpleLocal)) - offerState(environment.state.updateQueued(globalGraph.succ(environment.state.stmt))) + offerState(updateQueued(globalGraph.succ(environment.state.stmt))) } private fun TraversalContext.traverseAssignStmt(current: JAssignStmt) { val rightValue = current.rightOp - workaround(HACK) { + if (UtSettings.exploreThrowableDepth == ExploreThrowableDepth.SKIP_ALL_STATEMENTS) { val rightType = rightValue.type if (rightValue is JNewExpr && rightType is RefType) { - val throwableType = Scene.v().getSootClass("java.lang.Throwable").type - val throwableInheritors = typeResolver.findOrConstructInheritorsIncludingTypes(throwableType) + val throwableInheritors = typeResolver.findOrConstructInheritorsIncludingTypes(THROWABLE_TYPE) // skip all the vertices in the CFG between `new` and `` statements if (rightType in throwableInheritors) { @@ -707,14 +824,20 @@ class Traverser( } rightPartWrappedAsMethodResults.forEach { methodResult -> + val taintAnalysisUpdate = if (UtSettings.useTaintAnalysis && rightValue is InvokeExpr) { + processTaintAnalysis(rightValue, methodResult) + } else { + SymbolicStateUpdate() + } + when (methodResult.symbolicResult) { is SymbolicFailure -> { //exception thrown if (environment.state.executionStack.last().doesntThrow) return@forEach - val nextState = environment.state.createExceptionState( + val nextState = createExceptionStateQueued( methodResult.symbolicResult, - queuedSymbolicStateUpdates + methodResult.symbolicStateUpdate + methodResult.symbolicStateUpdate + taintAnalysisUpdate ) globalGraph.registerImplicitEdge(nextState.lastEdge!!) offerState(nextState) @@ -726,9 +849,9 @@ class Traverser( methodResult.symbolicResult.value ) offerState( - environment.state.updateQueued( + updateQueued( globalGraph.succ(current), - update + methodResult.symbolicStateUpdate + update + methodResult.symbolicStateUpdate + taintAnalysisUpdate ) ) } @@ -847,8 +970,10 @@ class Traverser( } val typeStorage = typeResolver.constructTypeStorage(OBJECT_TYPE, arrayPossibleTypes) - queuedSymbolicStateUpdates += typeRegistry.typeConstraint(arrayInstance.addr, typeStorage) - .isConstraint().asHardConstraint() + queuedSymbolicStateUpdates += typeRegistry + .typeConstraint(arrayInstance.addr, typeStorage) + .isConstraint() + .asHardConstraint() } val elementType = arrayInstance.type.elementType @@ -881,7 +1006,10 @@ class Traverser( } } - SymbolicStateUpdate(memoryUpdates = objectUpdate) + // Associate created value with field of used instance. For more information check Memory#fieldValue docs. + val fieldValuesUpdate = fieldUpdate(left.field, instanceForField.addr, value) + + SymbolicStateUpdate(memoryUpdates = objectUpdate + fieldValuesUpdate) } is JimpleLocal -> SymbolicStateUpdate(localMemoryUpdates = localMemoryUpdate(left.variable to value)) is InvokeExpr -> TODO("Not implemented: $left") @@ -897,15 +1025,13 @@ class Traverser( // Ignores the result of resolve(). resolve(fieldRef) val baseObject = resolve(fieldRef.base) as ObjectValue - val typeStorage = TypeStorage(fieldRef.field.declaringClass.type) + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(fieldRef.field.declaringClass.type) baseObject.copy(typeStorage = typeStorage) } is StaticFieldRef -> { val declaringClassType = fieldRef.field.declaringClass.type - val fieldTypeId = fieldRef.field.type.classId val generator = UtMockInfoGenerator { mockAddr -> - val fieldId = FieldId(declaringClassType.id, fieldRef.field.name) - UtFieldMockInfo(fieldTypeId, mockAddr, fieldId, ownerAddr = null) + UtStaticObjectMockInfo(declaringClassType.id, mockAddr) } findOrCreateStaticObject(declaringClassType, generator) } @@ -952,7 +1078,8 @@ class Traverser( if (createdValue is ReferenceValue) { // Update generic type info for method under test' parameters - updateGenericTypeInfo(identityRef, createdValue) + val index = (identityRef as? ParameterRef)?.index?.plus(1) ?: 0 + updateGenericTypeInfoFromMethod(methodUnderTest, createdValue, index) if (isNonNullable) { queuedSymbolicStateUpdates += mkNot( @@ -971,7 +1098,7 @@ class Traverser( environment.state.parameters += Parameter(localVariable, identityRef.type, value) - val nextState = environment.state.updateQueued( + val nextState = updateQueued( globalGraph.succ(current), SymbolicStateUpdate(localMemoryUpdates = localMemoryUpdate(localVariable to value)) ) @@ -980,7 +1107,7 @@ class Traverser( is JCaughtExceptionRef -> { val value = localVariableMemory.local(CAUGHT_EXCEPTION) ?: error("Exception wasn't caught, stmt: $current, line: ${current.lines}") - val nextState = environment.state.updateQueued( + val nextState = updateQueued( globalGraph.succ(current), SymbolicStateUpdate(localMemoryUpdates = localMemoryUpdate(localVariable to value, CAUGHT_EXCEPTION to null)) ) @@ -1003,80 +1130,120 @@ class Traverser( return UtMockInfoGenerator { mockAddr -> UtObjectMockInfo(type.id, mockAddr) } } + private fun updateGenericTypeInfoFromMethod(method: ExecutableId, value: ReferenceValue, parameterIndex: Int) { + val type = extractParameterizedType(method, parameterIndex) as? ParameterizedType ?: return + + updateGenericTypeInfo(type, value) + } + /** * Stores information about the generic types used in the parameters of the method under test. */ - private fun updateGenericTypeInfo(identityRef: IdentityRef, value: ReferenceValue) { - val callable = methodUnderTest.executable - val kCallable = ::updateGenericTypeInfo - val test = kCallable.instanceParameter?.type?.javaType - val type = if (identityRef is ThisRef) { - // TODO: for ThisRef both methods don't return parameterized type - if (methodUnderTest.isConstructor) { - callable.annotatedReturnType?.type - } else { - callable.declaringClass // same as it was, but it isn't parametrized type - ?: error("No instanceParameter for ${callable} found") - } - } else { - // Sometimes out of bound exception occurred here, e.g., com.alibaba.fescar.core.model.GlobalStatus. - workaround(HACK) { - val index = (identityRef as ParameterRef).index - val valueParameters = callable.genericParameterTypes - - if (index > valueParameters.lastIndex) return - valueParameters[index] - } - } + private fun updateGenericTypeInfo(type: ParameterizedType, value: ReferenceValue) { + val typeStorages = type.actualTypeArguments.map { actualTypeArgument -> + when (actualTypeArgument) { + is WildcardType -> { + val upperBounds = actualTypeArgument.upperBounds + val lowerBounds = actualTypeArgument.lowerBounds + val allTypes = upperBounds + lowerBounds + + if (allTypes.any { it is GenericArrayType }) { + val errorTypes = allTypes.filterIsInstance() + logger.warn { "we do not support GenericArrayTypeImpl yet, and $errorTypes found. SAT-1446" } + return + } - if (type is ParameterizedType) { - val typeStorages = type.actualTypeArguments.map { actualTypeArgument -> - when (actualTypeArgument) { - is WildcardType -> { - val upperBounds = actualTypeArgument.upperBounds - val lowerBounds = actualTypeArgument.lowerBounds - val allTypes = upperBounds + lowerBounds + val upperBoundsTypes = typeResolver.intersectInheritors(upperBounds) + val lowerBoundsTypes = typeResolver.intersectAncestors(lowerBounds) - if (allTypes.any { it is GenericArrayType }) { - val errorTypes = allTypes.filterIsInstance() - TODO("we do not support GenericArrayTypeImpl yet, and $errorTypes found. SAT-1446") - } + // For now, we take into account only one type bound. + // If we have the only upper bound, we should create a type storage + // with a corresponding type if it exists or with + // OBJECT_TYPE if there is no such type (e.g., E or T) + val leastCommonType = upperBounds + .singleOrNull() + ?.let { Scene.v().getRefTypeUnsafe(it.typeName) } + ?: OBJECT_TYPE - val upperBoundsTypes = typeResolver.intersectInheritors(upperBounds) - val lowerBoundsTypes = typeResolver.intersectAncestors(lowerBounds) + typeResolver.constructTypeStorage(leastCommonType, upperBoundsTypes.intersect(lowerBoundsTypes)) + } + is TypeVariable<*> -> { // it is a type variable for the whole class, not the function type variable + val upperBounds = actualTypeArgument.bounds - typeResolver.constructTypeStorage(OBJECT_TYPE, upperBoundsTypes.intersect(lowerBoundsTypes)) + if (upperBounds.any { it is GenericArrayType }) { + val errorTypes = upperBounds.filterIsInstance() + logger.warn { "we do not support GenericArrayType yet, and $errorTypes found. SAT-1446" } + return } - is TypeVariable<*> -> { // it is a type variable for the whole class, not the function type variable - val upperBounds = actualTypeArgument.bounds - if (upperBounds.any { it is GenericArrayType }) { - val errorTypes = upperBounds.filterIsInstance() - TODO("we do not support GenericArrayType yet, and $errorTypes found. SAT-1446") - } + val upperBoundsTypes = typeResolver.intersectInheritors(upperBounds) - val upperBoundsTypes = typeResolver.intersectInheritors(upperBounds) + // For now, we take into account only one type bound. + // If we have the only upper bound, we should create a type storage + // with a corresponding type if it exists or with + // OBJECT_TYPE if there is no such type (e.g., E or T) + val leastCommonType = upperBounds + .singleOrNull() + ?.let { Scene.v().getRefTypeUnsafe(it.typeName) } + ?: OBJECT_TYPE - typeResolver.constructTypeStorage(OBJECT_TYPE, upperBoundsTypes) - } - is GenericArrayType -> { - // TODO bug with T[][], because there is no such time T JIRA:1446 - typeResolver.constructTypeStorage(OBJECT_TYPE, useConcreteType = false) - } - is ParameterizedType, is Class<*> -> { - val sootType = Scene.v().getType(actualTypeArgument.rawType.typeName) + typeResolver.constructTypeStorage(leastCommonType, upperBoundsTypes) + } + is GenericArrayType -> { + // TODO bug with T[][], because there is no such time T JIRA:1446 + typeResolver.constructTypeStorage(OBJECT_TYPE, useConcreteType = false) + } + is ParameterizedType, is Class<*> -> { + val sootType = Scene.v().getType(actualTypeArgument.rawType.typeName) - typeResolver.constructTypeStorage(sootType, useConcreteType = false) - } - else -> error("Unsupported argument type ${actualTypeArgument::class}") + typeResolver.constructTypeStorage(sootType, useConcreteType = false) } + else -> error("Unsupported argument type ${actualTypeArgument::class}") } + } + + queuedSymbolicStateUpdates += typeRegistry + .genericTypeParameterConstraint(value.addr, typeStorages) + .asHardConstraint() + + instanceAddrToGenericType.getOrPut(value.addr) { mutableSetOf() }.add(type) + + val memoryUpdate = TypeResolver.createGenericTypeInfoUpdate( + value.addr, + typeStorages, + memory.getAllGenericTypeInfo() + ) + + queuedSymbolicStateUpdates += memoryUpdate + } + + private fun extractParameterizedType( + method: ExecutableId, + index: Int + ): java.lang.reflect.Type? { + // If we don't have access to methodUnderTest's jClass, the engine should not fail + // We just won't update generic information for it + val callable = runCatching { method.executable }.getOrNull() ?: return null - queuedSymbolicStateUpdates += typeRegistry.genericTypeParameterConstraint(value.addr, typeStorages).asHardConstraint() - parameterAddrToGenericType += value.addr to type + val type = if (index == 0) { + // TODO: for ThisRef both methods don't return parameterized type + if (method.isConstructor) { + callable.annotatedReturnType?.type + } else { + callable.declaringClass // same as it was, but it isn't parametrized type + ?: error("No instanceParameter for $callable found") + } + } else { + // Sometimes out of bound exception occurred here, e.g., com.alibaba.fescar.core.model.GlobalStatus. + workaround(HACK) { + val valueParameters = callable.genericParameterTypes - typeRegistry.saveObjectParameterTypeStorages(value.addr, typeStorages) + if (index - 1 > valueParameters.lastIndex) return null + valueParameters[index - 1] + } } + + return type } private fun TraversalContext.traverseIfStmt(current: JIfStmt) { @@ -1114,7 +1281,7 @@ class Traverser( if (!isAssumeExpr) { positiveCaseEdge?.let { edge -> environment.state.expectUndefined() - val positiveCaseState = environment.state.updateQueued( + val positiveCaseState = updateQueued( edge, SymbolicStateUpdate( hardConstraints = positiveCasePathConstraint.asHardConstraint(), @@ -1129,7 +1296,7 @@ class Traverser( val hardConstraints = if (!isAssumeExpr) negativeCasePathConstraint.asHardConstraint() else emptyHardConstraint() val assumption = if (isAssumeExpr) negativeCasePathConstraint.asAssumption() else emptyAssumption() - val negativeCaseState = environment.state.updateQueued( + val negativeCaseState = updateQueued( negativeCaseEdge, SymbolicStateUpdate( hardConstraints = hardConstraints, @@ -1159,11 +1326,11 @@ class Traverser( offerState( when (result.symbolicResult) { - is SymbolicFailure -> environment.state.createExceptionState( + is SymbolicFailure -> createExceptionStateQueued( result.symbolicResult, - queuedSymbolicStateUpdates + result.symbolicStateUpdate + result.symbolicStateUpdate ) - is SymbolicSuccess -> environment.state.updateQueued( + is SymbolicSuccess -> updateQueued( globalGraph.succ(current), result.symbolicStateUpdate ) @@ -1201,7 +1368,7 @@ class Traverser( successors.forEach { (target, expr) -> offerState( - environment.state.updateQueued( + updateQueued( target, SymbolicStateUpdate(hardConstraints = expr.asHardConstraint()), ) @@ -1214,28 +1381,62 @@ class Traverser( traverseException(current, symException) } - // TODO HACK violation of encapsulation + // TODO: HACK violation of encapsulation fun createObject( addr: UtAddrExpression, type: RefType, useConcreteType: Boolean, - mockInfoGenerator: UtMockInfoGenerator? = null + mockInfoGenerator: UtMockInfoGenerator? ): ObjectValue { touchAddress(addr) + val nullEqualityConstraint = mkEq(addr, nullObjectAddr) + + // Some types (e.g., interfaces) need to be mocked or replaced with the concrete implementor. + // Typically, this implementor is selected by SMT solver later. + // However, if we have the restriction on implementor type (it may be obtained + // from Spring bean definitions, for example), we can just create a symbolic object + // with hard constraint on the mentioned type. + val replacedClassId = when (typeReplacer.typeReplacementMode) { + KnownImplementor -> typeReplacer.replaceTypeIfNeeded(type.id) + AnyImplementor, + NoImplementors -> null + } + + replacedClassId?.let { + val sootType = Scene.v().getRefType(it.canonicalName) + val typeStorage = typeResolver.constructTypeStorage(sootType, useConcreteType = false) + + val typeHardConstraint = typeRegistry.typeConstraint(addr, typeStorage).all().asHardConstraint() + queuedSymbolicStateUpdates += typeHardConstraint + + return ObjectValue(typeStorage, addr) + } if (mockInfoGenerator != null) { val mockInfo = mockInfoGenerator.generate(addr) queuedSymbolicStateUpdates += MemoryUpdate(addrToMockInfo = persistentHashMapOf(addr to mockInfo)) - val mockedObject = mocker.mock(type, mockInfo) + val mockedObjectInfo = mocker.mock(type, mockInfo) + + if (mockedObjectInfo is UnexpectedMock) { + // if mock occurs, but it is unexpected due to some reasons + // (e.g. we do not have mock framework installed), + // we can only generate a test that uses null value for mocked object + queuedSymbolicStateUpdates += nullEqualityConstraint.asHardConstraint() + return mockedObjectInfo.value + } + val mockedObject = mockedObjectInfo.value if (mockedObject != null) { queuedSymbolicStateUpdates += MemoryUpdate(mockInfos = persistentListOf(MockInfoEnriched(mockInfo))) // add typeConstraint for mocked object. It's a declared type of the object. - queuedSymbolicStateUpdates += typeRegistry.typeConstraint(addr, mockedObject.typeStorage).all().asHardConstraint() - queuedSymbolicStateUpdates += mkEq(typeRegistry.isMock(mockedObject.addr), UtTrue).asHardConstraint() + val typeConstraint = typeRegistry.typeConstraint(addr, mockedObject.typeStorage).all() + val isMockConstraint = typeRegistry.isMockConstraint(mockedObject.addr) + + queuedSymbolicStateUpdates += typeConstraint.asHardConstraint() + queuedSymbolicStateUpdates += isMockConstraint.asHardConstraint() return mockedObject } @@ -1250,13 +1451,58 @@ class Traverser( // It is required because we do not want to have situations when some object might have // only artificial classes as their possible, that would cause problems in the type constraints. val typeStorage = if (leastCommonType in wrapperToClass.keys) { - typeStoragePossiblyWithOverriddenTypes.copy(possibleConcreteTypes = wrapperToClass.getValue(leastCommonType)) + val possibleConcreteTypes = wrapperToClass.getValue(leastCommonType) + + TypeStorage.constructTypeStorageUnsafe( + typeStoragePossiblyWithOverriddenTypes.leastCommonType, + possibleConcreteTypes + ) } else { - typeStoragePossiblyWithOverriddenTypes + if (addr.isThisAddr) { + val possibleTypes = typeStoragePossiblyWithOverriddenTypes.possibleConcreteTypes + val isTypeInappropriate = type.sootClass?.isInappropriate == true + + // If we're trying to construct this instance and it has an inappropriate type, + // we won't be able to instantiate its instance in resulting tests. + // Therefore, we have to test one of its inheritors that does not override + // the method under test + if (isTypeInappropriate) { + require(possibleTypes.isNotEmpty()) { + "We do not support testing for abstract classes (or interfaces) without any non-abstract " + + "inheritors (implementors). Probably, it'll be supported in the future." + } + + val possibleTypesWithNonOverriddenMethod = possibleTypes + .filterTo(mutableSetOf()) { + val methods = (it as RefType).sootClass.methods + methods.none { method -> + val methodUnderTest = environment.method + val parameterTypes = method.parameterTypes + + method.name == methodUnderTest.name && parameterTypes == methodUnderTest.parameterTypes + } + } + + require(possibleTypesWithNonOverriddenMethod.isNotEmpty()) { + "There is no instantiatable inheritor of the class under test that does not override " + + "a method given for testing" + } + + TypeStorage.constructTypeStorageUnsafe(type, possibleTypesWithNonOverriddenMethod) + } else { + // If we create a `this` instance and its type is instantiatable, + // we should construct a type storage with single type + TypeStorage.constructTypeStorageWithSingleType(type) + } + } else { + typeStoragePossiblyWithOverriddenTypes + } } + val typeHardConstraint = typeRegistry.typeConstraint(addr, typeStorage).all().asHardConstraint() + wrapper(type, addr)?.let { - queuedSymbolicStateUpdates += typeRegistry.typeConstraint(addr, typeStorage).all().asHardConstraint() + queuedSymbolicStateUpdates += typeHardConstraint return it } @@ -1266,27 +1512,93 @@ class Traverser( "but there is no mock info generator provided to construct a mock value." } - val mockInfo = mockInfoGenerator.generate(addr) - val mockedObject = mocker.forceMock(type, mockInfoGenerator.generate(addr)) - - queuedSymbolicStateUpdates += MemoryUpdate(mockInfos = persistentListOf(MockInfoEnriched(mockInfo))) + return createMockedObject(addr, type, mockInfoGenerator, nullEqualityConstraint) + } - // add typeConstraint for mocked object. It's a declared type of the object. - queuedSymbolicStateUpdates += typeRegistry.typeConstraint(addr, mockedObject.typeStorage).all().asHardConstraint() - queuedSymbolicStateUpdates += mkEq(typeRegistry.isMock(mockedObject.addr), UtTrue).asHardConstraint() + val concreteImplementation: Concrete? = when (typeReplacer.typeReplacementMode) { + AnyImplementor -> findConcreteImplementation(addr, type, typeHardConstraint) + + // If our type is not abstract, both in `KnownImplementors` and `NoImplementors` mode, + // we should just still use concrete implementation that represents itself + // + // Otherwise: + // In case of `KnownImplementor` mode we should have already tried to replace type using `replaceTypeIfNeeded`. + // However, this replacement attempt might be unsuccessful even if some possible concrete types are present. + // For example, we may have two concrete implementors present in Spring bean definitions, so we do not know + // which one to use. In such case we try to mock this type, if it is possible, or prune branch as unsatisfiable. + // + // In case of `NoImplementors` mode we should try to mock this type or prune branch as unsatisfiable. + // Mocking can be impossible here as there are no guaranties that `mockInfoGenerator` is instantiated. + KnownImplementor, + NoImplementors -> { + if (!type.isAbstractType) { + findConcreteImplementation(addr, type, typeHardConstraint) + } else { + mockInfoGenerator?.let { + return createMockedObject(addr, type, it, nullEqualityConstraint) + } - return mockedObject + queuedSymbolicStateUpdates += mkFalse().asHardConstraint() + null + } + } } + return ObjectValue(typeStorage, addr, concreteImplementation) + } + + private fun findConcreteImplementation( + addr: UtAddrExpression, + type: RefType, + typeHardConstraint: HardConstraint, + ): Concrete? { + val isMockConstraint = mkEq(typeRegistry.isMock(addr), UtFalse) + + queuedSymbolicStateUpdates += typeHardConstraint + queuedSymbolicStateUpdates += isMockConstraint.asHardConstraint() + // If we have this$0 with UtArrayList type, we have to create such instance. // We should create an object with typeStorage of all possible real types and concrete implementation // Otherwise we'd have either a wrong type in the resolver, or missing method like 'preconditionCheck'. - val concreteImplementation = wrapperToClass[type]?.first()?.let { wrapper(it, addr) }?.concrete + return wrapperToClass[type]?.first()?.let { wrapper(it, addr) }?.concrete + } - queuedSymbolicStateUpdates += typeRegistry.typeConstraint(addr, typeStorage).all().asHardConstraint() - queuedSymbolicStateUpdates += mkEq(typeRegistry.isMock(addr), UtFalse).asHardConstraint() + private fun createMockedObject( + addr: UtAddrExpression, + type: RefType, + mockInfoGenerator: UtMockInfoGenerator, + nullEqualityConstraint: UtBoolExpression, + ): ObjectValue { + val mockInfo = mockInfoGenerator.generate(addr) + val mockedObjectInfo = mocker.forceMock(type, mockInfoGenerator.generate(addr)) + + val mockedObject: ObjectValue = when (mockedObjectInfo) { + is NoMock -> error("Value must be mocked after the force mock") + is ExpectedMock -> mockedObjectInfo.value + is UnexpectedMock -> { + // if mock occurs, but it is unexpected due to some reasons + // (e.g. we do not have mock framework installed), + // we can only generate a test that uses null value for mocked object + queuedSymbolicStateUpdates += nullEqualityConstraint.asHardConstraint() + + mockedObjectInfo.value + } + } - return ObjectValue(typeStorage, addr, concreteImplementation) + if (mockedObjectInfo is UnexpectedMock) { + return mockedObject + } + + queuedSymbolicStateUpdates += MemoryUpdate(mockInfos = persistentListOf(MockInfoEnriched(mockInfo))) + + // add typeConstraint for mocked object. It's a declared type of the object. + val typeConstraint = typeRegistry.typeConstraint(addr, mockedObject.typeStorage).all() + val isMockConstraint = typeRegistry.isMockConstraint(mockedObject.addr) + + queuedSymbolicStateUpdates += typeConstraint.asHardConstraint() + queuedSymbolicStateUpdates += isMockConstraint.asHardConstraint() + + return mockedObject } private fun TraversalContext.resolveConstant(constant: Constant): SymbolicValue = @@ -1304,10 +1616,14 @@ class Traverser( // instead of it we create an unbounded symbolic variable workaround(HACK) { offerState(environment.state.withLabel(StateLabel.CONCRETE)) - createObject(addr, refType, useConcreteType = true) + // We don't need to mock a string constant creation + createObject(addr, refType, useConcreteType = true, mockInfoGenerator = null) } } else { - queuedSymbolicStateUpdates += typeRegistry.typeConstraint(addr, TypeStorage(refType)).all().asHardConstraint() + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(refType) + val typeConstraint = typeRegistry.typeConstraint(addr, typeStorage).all().asHardConstraint() + + queuedSymbolicStateUpdates += typeConstraint objectValue(refType, addr, StringWrapper()).also { initStringLiteral(it, constant.value) @@ -1417,8 +1733,10 @@ class Traverser( } private fun initStringLiteral(stringWrapper: ObjectValue, value: String) { + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(utStringClass.type) + queuedSymbolicStateUpdates += objectUpdate( - stringWrapper.copy(typeStorage = TypeStorage(utStringClass.type)), + stringWrapper.copy(typeStorage = typeStorage), STRING_LENGTH, mkInt(value.length) ) @@ -1431,7 +1749,7 @@ class Traverser( queuedSymbolicStateUpdates += arrayUpdateWithValue(it.addr, arrayType, defaultValue as UtArrayExpressionBase) } queuedSymbolicStateUpdates += objectUpdate( - stringWrapper.copy(typeStorage = TypeStorage(utStringClass.type)), + stringWrapper.copy(typeStorage = typeStorage), STRING_VALUE, arrayValue.addr ) @@ -1440,7 +1758,7 @@ class Traverser( } queuedSymbolicStateUpdates += arrayUpdateWithValue(arrayValue.addr, CharType.v().arrayType, newArray) - environment.state = environment.state.update(queuedSymbolicStateUpdates) + environment.state = updateQueued(SymbolicStateUpdate()) queuedSymbolicStateUpdates = queuedSymbolicStateUpdates.copy(memoryUpdates = MemoryUpdate()) } @@ -1708,7 +2026,19 @@ class Traverser( queuedSymbolicStateUpdates += Ge(length, 0).asHardConstraint() workaround(HACK) { if (size.expr is UtBvLiteral) { - softMaxArraySize = min(HARD_MAX_ARRAY_SIZE, max(size.expr.value.toInt(), softMaxArraySize)) + val sizeValue = size.expr.value.toInt() + softMaxArraySize = min(hardMaxArraySize, max(sizeValue, softMaxArraySize)) + + if (sizeValue > hardMaxArraySize) { + logger.warn( + "The engine encountered an array initialization with $sizeValue size." + + " It leads to elimination of paths containing current instruction." + ) + logger.warn("Current instruction: ${environment.state.stmt}") + logger.warn("Please, consider increasing `UtSettings.maxArraySize` value, " + + "currently it is ${UtSettings.maxArraySize}." + ) + } } } queuedSymbolicStateUpdates += Le(length, softMaxArraySize).asHardConstraint() // TODO: fix big array length @@ -1719,19 +2049,12 @@ class Traverser( val chunkId = typeRegistry.arrayChunkId(type) touchMemoryChunk(MemoryChunkDescriptor(chunkId, type, elementType)) - return ArrayValue(TypeStorage(type), addr).also { + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(type) + return ArrayValue(typeStorage, addr).also { queuedSymbolicStateUpdates += typeRegistry.typeConstraint(addr, it.typeStorage).all().asHardConstraint() } } - private fun SymbolicValue.simplify(): SymbolicValue = - when (this) { - is PrimitiveValue -> copy(expr = expr.accept(solver.rewriter)) - is ObjectValue -> copy(addr = addr.accept(solver.rewriter) as UtAddrExpression) - is ArrayValue -> copy(addr = addr.accept(solver.rewriter) as UtAddrExpression) - } - - // Type is needed for null values: we should know, which null do we require. // If valueType is NullType, return typelessNullObject. It can happen in a situation, // where we cannot find the type, for example in condition (null == null) @@ -1741,7 +2064,7 @@ class Traverser( ): SymbolicValue = when (value) { is JimpleLocal -> localVariableMemory.local(value.variable) ?: error("${value.name} not found in the locals") is Constant -> if (value is NullConstant) typeResolver.nullObject(valueType) else resolveConstant(value) - is Expr -> resolve(value, valueType).simplify() + is Expr -> resolve(value, valueType) is JInstanceFieldRef -> { val instance = (resolve(value.base) as ObjectValue) recordInstanceFieldRead(instance.addr, value.field) @@ -1807,12 +2130,14 @@ class Traverser( } is StaticFieldRef -> readStaticField(value) else -> error("${value::class} is not supported") + }.also { + with(simplificator) { simplifySymbolicValue(it) } } - private fun readStaticField(fieldRef: StaticFieldRef): SymbolicValue { + private fun TraversalContext.readStaticField(fieldRef: StaticFieldRef): SymbolicValue { val field = fieldRef.field val declaringClassType = field.declaringClass.type - val staticObject = findOrCreateStaticObject(declaringClassType) + val staticObject = resolveInstanceForField(fieldRef) val generator = (field.type as? RefType)?.let { refType -> UtMockInfoGenerator { mockAddr -> @@ -1834,7 +2159,6 @@ class Traverser( val touchedStaticFields = persistentListOf(staticFieldMemoryUpdate) queuedSymbolicStateUpdates += MemoryUpdate(staticFieldsUpdates = touchedStaticFields) - // TODO filter enum constant static fields JIRA:1681 if (!environment.method.isStaticInitializer && isStaticFieldMeaningful(field)) { queuedSymbolicStateUpdates += MemoryUpdate(meaningfulStaticFields = persistentSetOf(fieldId)) } @@ -1844,7 +2168,7 @@ class Traverser( /** * For now the field is `meaningful` if it is safe to set, that is, it is not an internal system field nor a - * synthetic field. This filter is needed to prohibit changing internal fields, which can break up our own + * synthetic field or a wrapper's field. This filter is needed to prohibit changing internal fields, which can break up our own * code and which are useless for the user. * * @return `true` if the field is meaningful, `false` otherwise. @@ -1853,6 +2177,10 @@ class Traverser( !Modifier.isSynthetic(field.modifiers) && // we don't want to set fields that cannot be set via reflection anyway !field.fieldId.isInaccessibleViaReflection && + // we should not set static fields from wrappers + !field.declaringClass.isOverridden && + // we should not manually set enum constants + !(field.declaringClass.isEnum && field.isEnumConstant) && // we don't want to set fields from library classes !workaround(IGNORE_STATICS_FROM_TRUSTED_LIBRARIES) { ignoreStaticsFromTrustedLibraries && field.declaringClass.isFromTrustedLibrary() @@ -1872,7 +2200,7 @@ class Traverser( */ private fun findOrCreateStaticObject( classType: RefType, - mockInfoGenerator: UtMockInfoGenerator? = null + mockInfoGenerator: UtMockInfoGenerator? ): ObjectValue { val fromMemory = locateStaticObject(classType) @@ -1888,7 +2216,7 @@ class Traverser( return created } - private fun TraversalContext.resolveParameters(parameters: List, types: List) = + fun TraversalContext.resolveParameters(parameters: List, types: List) = parameters.zip(types).map { (value, type) -> resolve(value, type) } private fun applyPreferredConstraints(value: SymbolicValue) { @@ -1937,7 +2265,7 @@ class Traverser( addr: UtAddrExpression, fieldType: Type, chunkId: ChunkId, - mockInfoGenerator: UtMockInfoGenerator? = null + mockInfoGenerator: UtMockInfoGenerator? ): SymbolicValue { val descriptor = MemoryChunkDescriptor(chunkId, objectType, fieldType) val array = memory.findArray(descriptor) @@ -1964,6 +2292,10 @@ class Traverser( field: SootField, mockInfoGenerator: UtMockInfoGenerator? ): SymbolicValue { + memory.fieldValue(field, addr)?.let { + return it + } + val chunkId = hierarchy.chunkIdForField(objectType, field) val createdField = createField(objectType, addr, field.type, chunkId, mockInfoGenerator) @@ -1976,33 +2308,40 @@ class Traverser( checkAndMarkLibraryFieldSpeculativelyNotNull(field, createdField) } + updateGenericInfoForField(createdField, field) + return createdField } /** - * Marks the [createdField] as speculatively not null if the [field] is considering as - * not producing [NullPointerException]. - * - * @see [SootField.speculativelyCannotProduceNullPointerException], [markAsSpeculativelyNotNull], [isFromTrustedLibrary]. + * Updates generic info for provided [field] and [createdField] using + * type information. If [createdField] is not a reference value or + * if field's type is not a parameterized one, nothing will happen. */ - private fun checkAndMarkLibraryFieldSpeculativelyNotNull(field: SootField, createdField: SymbolicValue) { - if (maximizeCoverageUsingReflection || !field.declaringClass.isFromTrustedLibrary()) { - return - } + private fun updateGenericInfoForField(createdField: SymbolicValue, field: SootField) { + runCatching { + if (createdField !is ReferenceValue) return - if (field.speculativelyCannotProduceNullPointerException()) { - markAsSpeculativelyNotNull(createdField.addr) + // We must have `runCatching` here since might be a situation when we do not have + // such declaring class in a classpath, that might (but should not) lead to an exception + val classId = field.declaringClass.id + val requiredField = classId.findFieldByIdOrNull(field.fieldId) + val genericInfo = requiredField?.genericType as? ParameterizedType ?: return + + updateGenericTypeInfo(genericInfo, createdField) } } /** - * Checks whether accessing [this] field (with a method invocation or field access) speculatively can produce - * [NullPointerException] (according to its finality or accessibility). + * Marks the [createdField] as speculatively not null if the [field] is considering + * as not producing [NullPointerException]. * - * @see docs/SpeculativeFieldNonNullability.md for more information. + * See more detailed documentation in [ApplicationContext] mentioned methods. */ - @Suppress("KDocUnresolvedReference") - private fun SootField.speculativelyCannotProduceNullPointerException(): Boolean = isFinal || !isPublic + private fun checkAndMarkLibraryFieldSpeculativelyNotNull(field: SootField, createdField: SymbolicValue) { + if (nonNullSpeculator.speculativelyCannotProduceNullPointerException(field, methodUnderTest.classId)) + markAsSpeculativelyNotNull(createdField.addr) + } private fun createArray(pName: String, type: ArrayType): ArrayValue { val addr = UtAddrExpression(mkBVConst(pName, UtIntSort)) @@ -2063,10 +2402,10 @@ class Traverser( * Since createConst called only for objects from outside at the beginning of the analysis, * we can set Le(addr, NULL_ADDR) for all RefValue objects. */ - private fun Value.createConst(pName: String, mockInfoGenerator: UtMockInfoGenerator? = null): SymbolicValue = + private fun Value.createConst(pName: String, mockInfoGenerator: UtMockInfoGenerator?): SymbolicValue = createConst(type, pName, mockInfoGenerator) - fun createConst(type: Type, pName: String, mockInfoGenerator: UtMockInfoGenerator? = null): SymbolicValue = + fun createConst(type: Type, pName: String, mockInfoGenerator: UtMockInfoGenerator?): SymbolicValue = when (type) { is ByteType -> mkBVConst(pName, UtByteSort).toByteValue() is ShortType -> mkBVConst(pName, UtShortSort).toShortValue() @@ -2089,17 +2428,19 @@ class Traverser( queuedSymbolicStateUpdates += addrEq(addr, nullObjectAddr).asSoftConstraint() if (type.sootClass.isEnum) { - createEnum(type, addr) + // We don't know which enum constant should we create, so we + // have to create an instance of unknown type to support virtual invokes. + createEnum(type, addr, useConcreteType = false) } else { - createObject(addr, type, useConcreteType = addr.isThisAddr, mockInfoGenerator) + createObject(addr, type, useConcreteType = false, mockInfoGenerator) } } is VoidType -> voidValue else -> error("Can't create const from ${type::class}") } - private fun createEnum(type: RefType, addr: UtAddrExpression): ObjectValue { - val typeStorage = typeResolver.constructTypeStorage(type, useConcreteType = true) + private fun createEnum(type: RefType, addr: UtAddrExpression, useConcreteType: Boolean): ObjectValue { + val typeStorage = typeResolver.constructTypeStorage(type, useConcreteType) queuedSymbolicStateUpdates += typeRegistry.typeConstraint(addr, typeStorage).all().asHardConstraint() @@ -2115,6 +2456,7 @@ class Traverser( return ObjectValue(typeStorage, addr) } + @Suppress("SameParameterValue") private fun arrayUpdate(array: ArrayValue, index: PrimitiveValue, value: UtExpression): MemoryUpdate { val type = array.type val chunkId = typeRegistry.arrayChunkId(type) @@ -2124,7 +2466,7 @@ class Traverser( .select(array.addr) .store(index.expr, value) - return MemoryUpdate(persistentListOf(simplifiedNamedStore(descriptor, array.addr, updatedNestedArray))) + return MemoryUpdate(persistentListOf(namedStore(descriptor, array.addr, updatedNestedArray))) } fun objectUpdate( @@ -2134,7 +2476,21 @@ class Traverser( ): MemoryUpdate { val chunkId = hierarchy.chunkIdForField(instance.type, field) val descriptor = MemoryChunkDescriptor(chunkId, instance.type, field.type) - return MemoryUpdate(persistentListOf(simplifiedNamedStore(descriptor, instance.addr, value))) + return MemoryUpdate(persistentListOf(namedStore(descriptor, instance.addr, value))) + } + + /** + * Creates a [MemoryUpdate] with [MemoryUpdate.fieldValues] containing [fieldValue] associated with the non-staitc [field] + * of the object instance with the specified [instanceAddr]. + */ + private fun fieldUpdate( + field: SootField, + instanceAddr: UtAddrExpression, + fieldValue: SymbolicValue + ): MemoryUpdate { + val fieldValuesUpdate = persistentHashMapOf(field to persistentHashMapOf(instanceAddr to fieldValue)) + + return MemoryUpdate(fieldValues = fieldValuesUpdate) } fun arrayUpdateWithValue( @@ -2147,7 +2503,7 @@ class Traverser( val chunkId = typeRegistry.arrayChunkId(type) val descriptor = MemoryChunkDescriptor(chunkId, type, type.elementType) - return MemoryUpdate(persistentListOf(simplifiedNamedStore(descriptor, addr, newValue))) + return MemoryUpdate(persistentListOf(namedStore(descriptor, addr, newValue))) } fun selectArrayExpressionFromMemory( @@ -2201,6 +2557,10 @@ class Traverser( exception: SymbolicFailure, conditions: Set ): Boolean { + if (exception.concrete is ArtificialError) { + return false + } + val classId = exception.fold( { it.javaClass.id }, { (exception.symbolic as ObjectValue).type.id } @@ -2208,7 +2568,7 @@ class Traverser( val edge = findCatchBlock(current, classId) ?: return false offerState( - environment.state.updateQueued( + updateQueued( edge, SymbolicStateUpdate( hardConstraints = conditions.asHardConstraint(), @@ -2226,9 +2586,17 @@ class Traverser( }.firstOrNull { it.second in hierarchy.ancestors(classId) }?.first } - private fun TraversalContext.invokeResult(invokeExpr: Expr): List = - environment.state.methodResult?.let { - listOf(it) + private fun TraversalContext.invokeResult(invokeExpr: InvokeExpr): List = + environment.state.methodResult?.let { methodResult -> + val taintUpdate = if (UtSettings.useTaintAnalysis) { + processTaintAnalysis(invokeExpr, methodResult) + } else { + SymbolicStateUpdate() + } + + listOf( + methodResult.copy(symbolicStateUpdate = methodResult.symbolicStateUpdate + taintUpdate) + ) } ?: when (invokeExpr) { is JStaticInvokeExpr -> staticInvoke(invokeExpr) is JInterfaceInvokeExpr -> virtualAndInterfaceInvoke(invokeExpr.base, invokeExpr.methodRef, invokeExpr.args) @@ -2327,13 +2695,18 @@ class Traverser( MethodResult( mockValue, hardConstraints = additionalConstraint.asHardConstraint(), - memoryUpdates = if (isInternalMock) MemoryUpdate() else mockMethodResult.memoryUpdates + memoryUpdates = if (isInternalMock) MemoryUpdate() else mockMethodResult.symbolicStateUpdate.memoryUpdates ) ) } private fun TraversalContext.staticInvoke(invokeExpr: JStaticInvokeExpr): List { val parameters = resolveParameters(invokeExpr.args, invokeExpr.method.parameterTypes) + + if (UtSettings.useTaintAnalysis) { + processTaintSink(SymbolicMethodData(invokeExpr.method.executableId, base = null, args = parameters, result = null)) + } + val result = mockMakeSymbolic(invokeExpr) ?: mockStaticMethod(invokeExpr.method, parameters) if (result != null) return result @@ -2363,6 +2736,10 @@ class Traverser( val method = methodRef.resolve() val resolvedParameters = resolveParameters(parameters, method.parameterTypes) + if (UtSettings.useTaintAnalysis) { + processTaintSink(SymbolicMethodData(method.executableId, instance, resolvedParameters, result = null)) + } + val invocation = Invocation(instance, method, resolvedParameters) { when (instance) { is ObjectValue -> findInvocationTargets(instance, methodRef.subSignature.string) @@ -2381,19 +2758,30 @@ class Traverser( instance: ObjectValue, methodSubSignature: String ): List { - val visitor = solver.rewriter.axiomInstantiationVisitor + val visitor = solver.simplificator.axiomInstantiationVisitor val simplifiedAddr = instance.addr.accept(visitor) + // UtIsExpression for object with address the same as instance.addr - val instanceOfConstraint = solver.assertions.singleOrNull { - it is UtIsExpression && it.addr == simplifiedAddr - } as? UtIsExpression + // If there are several such constraints, take the one with the least number of possible types + val instanceOfConstraint = solver.assertions + .filter { it is UtIsExpression && it.addr == simplifiedAddr } + .takeIf { it.isNotEmpty() } + ?.minBy { (it as UtIsExpression).typeStorage.possibleConcreteTypes.size } as? UtIsExpression + // if we have UtIsExpression constraint for [instance], then find invocation targets // for possibleTypes from this constraints, instead of the type maintained by solver. // While using simplifications with RewritingVisitor, assertions can maintain types // for objects (especially objects with type equals to type parameter of generic) // better than engine. - val types = instanceOfConstraint?.typeStorage?.possibleConcreteTypes ?: instance.possibleConcreteTypes + val types = instanceOfConstraint + ?.typeStorage + ?.possibleConcreteTypes + // we should take this constraint into consideration only if it has less + // possible types than our current object, otherwise, it doesn't add + // any helpful information + ?.takeIf { it.size < instance.possibleConcreteTypes.size } + ?: instance.possibleConcreteTypes val allPossibleConcreteTypes = typeResolver .constructTypeStorage(instance.type, useConcreteType = false) @@ -2408,7 +2796,7 @@ class Traverser( .map { (method, implementationClass, possibleTypes) -> val typeStorage = typeResolver.constructTypeStorage(implementationClass, possibleTypes) val mockInfo = memory.mockInfoByAddr(instance.addr) - val mockedObject = mockInfo?.let { + val mockedObjectInfo = mockInfo?.let { // TODO rewrite to fix JIRA:1611 val type = Scene.v().getSootClass(mockInfo.classId.name).type val ancestorTypes = typeResolver.findOrConstructAncestorsIncludingTypes(type) @@ -2417,28 +2805,42 @@ class Traverser( } else { it.copyWithClassId(classId = implementationClass.id) } - mocker.mock(implementationClass, updatedMockInfo) - } - if (mockedObject == null) { - // Above we might get implementationClass that has to be substituted. - // For example, for a call "Collection.size()" such classes will be produced. - val wrapperOrInstance = wrapper(implementationClass, instance.addr) - ?: instance.copy(typeStorage = typeStorage) - - val typeConstraint = typeRegistry.typeConstraint(instance.addr, wrapperOrInstance.typeStorage) - val constraints = setOf(typeConstraint.isOrNullConstraint()) + mocker.mock(implementationClass, updatedMockInfo) + } ?: NoMock - // TODO add memory updated for types JIRA:1523 + when (mockedObjectInfo) { + is NoMock -> { + // Above we might get implementationClass that has to be substituted. + // For example, for a call "Collection.size()" such classes will be produced. + val wrapperOrInstance = wrapper(implementationClass, instance.addr) + ?: instance.copy(typeStorage = typeStorage) - InvocationTarget(wrapperOrInstance, method, constraints) - } else { - val typeConstraint = typeRegistry.typeConstraint(mockedObject.addr, mockedObject.typeStorage) - val constraints = setOf(typeConstraint.isOrNullConstraint()) + val typeConstraint = typeRegistry.typeConstraint(instance.addr, wrapperOrInstance.typeStorage) + val constraints = setOf(typeConstraint.isOrNullConstraint()) - // TODO add memory updated for types JIRA:1523 - // TODO isMock???? - InvocationTarget(mockedObject, method, constraints) + // TODO add memory updated for types JIRA:1523 + InvocationTarget(wrapperOrInstance, method, constraints) + } + is ExpectedMock -> { + val mockedObject = mockedObjectInfo.value + val typeConstraint = typeRegistry.typeConstraint(mockedObject.addr, mockedObject.typeStorage) + val constraints = setOf(typeConstraint.isOrNullConstraint()) + + // TODO add memory updated for types JIRA:1523 + // TODO isMock???? + InvocationTarget(mockedObject, method, constraints) + } + /* + Currently, it is unclear how this could happen. + Perhaps, the answer is somewhere in the following situation: + you have an interface with an abstract method `foo`, and it has an abstract inheritor with the implementation of the method, + but this inheritor doesn't have any concrete inheritors. It looks like in this case we would mock this instance + (because it doesn't have any possible concrete type), but it is impossible since either this class cannot present + in possible types of the object on which we call `foo` (since they contain only concrete types), + or this class would be already mocked (since it doesn't contain any concrete implementors). + */ + is UnexpectedMock -> unreachableBranch("If it ever happens, it should be investigated") } } } @@ -2486,6 +2888,17 @@ class Traverser( } private fun TraversalContext.specialInvoke(invokeExpr: JSpecialInvokeExpr): List { + if (UtSettings.exploreThrowableDepth == ExploreThrowableDepth.SKIP_INIT_STATEMENT) { + val throwableInheritors = typeResolver.findOrConstructInheritorsIncludingTypes(THROWABLE_TYPE) + if (invokeExpr.method.isConstructor && invokeExpr.method.declaringClass.type in throwableInheritors) { + // skip an exception's init + globalGraph.visitEdge(environment.state.lastEdge!!) + globalGraph.visitNode(environment.state) + offerState(updateQueued(globalGraph.succ(environment.state.stmt))) + return emptyList() + } + } + val instance = resolve(invokeExpr.base) if (instance !is ReferenceValue) error("We cannot run ${invokeExpr.methodRef} on $instance") @@ -2495,6 +2908,11 @@ class Traverser( val method = invokeExpr.retrieveMethod() val parameters = resolveParameters(invokeExpr.args, method.parameterTypes) + + if (UtSettings.useTaintAnalysis) { + processTaintSink(SymbolicMethodData(method.executableId, instance, parameters, result = null)) + } + val invocation = Invocation(instance, method, parameters, InvocationTarget(instance, method)) // Calls with super syntax are represented by invokeSpecial instruction, but we don't support them in wrappers @@ -2503,12 +2921,19 @@ class Traverser( } private fun TraversalContext.dynamicInvoke(invokeExpr: JDynamicInvokeExpr): List { - workaround(HACK) { - // The engine does not yet support JDynamicInvokeExpr, so switch to concrete execution if we encounter it - offerState(environment.state.withLabel(StateLabel.CONCRETE)) - queuedSymbolicStateUpdates += UtFalse.asHardConstraint() - return emptyList() + val invocation = with(dynamicInvokeResolver) { resolveDynamicInvoke(invokeExpr) } + + if (invocation == null) { + workaround(HACK) { + logger.warn { "Marking state as a concrete, because of an unknown dynamic invoke instruction: $invokeExpr" } + // The engine does not yet support JDynamicInvokeExpr, so switch to concrete execution if we encounter it + offerState(environment.state.withLabel(StateLabel.CONCRETE)) + queuedSymbolicStateUpdates += UtFalse.asHardConstraint() + return emptyList() + } } + + return commonInvokePart(invocation) } /** @@ -2517,6 +2942,22 @@ class Traverser( * Returns results of native calls cause other calls push changes directly to path selector. */ private fun TraversalContext.commonInvokePart(invocation: Invocation): List { + val method = invocation.method.executableId + + // This code is supposed to support generic information from signatures for nested methods. + // If we have some method 'foo` and a method `bar(List), and inside `foo` + // there is an invocation `bar(object)`, this object must have information about + // its `Integer` generic type. + invocation.parameters.forEachIndexed { index, param -> + if (param !is ReferenceValue) return@forEachIndexed + + updateGenericTypeInfoFromMethod(method, param, parameterIndex = index + 1) + } + + if (invocation.instance != null) { + updateGenericTypeInfoFromMethod(method, invocation.instance, parameterIndex = 0) + } + /** * First, check if there is override for the invocation itself, not for the targets. * @@ -2648,7 +3089,7 @@ class Traverser( assumptions = assumptions.asAssumption() ) - val stateToContinue = environment.state.updateQueued( + val stateToContinue = updateQueued( globalGraph.succ(environment.state.stmt), symbolicStateUpdate ) @@ -2664,9 +3105,25 @@ class Traverser( declaringClass == utArrayMockClass -> utArrayMockInvoke(target, parameters) isUtMockForbidClassCastException -> isUtMockDisableClassCastExceptionCheckInvoke(parameters) else -> { - val graph = substitutedMethod?.jimpleBody()?.graph() ?: jimpleBody().graph() - pushToPathSelector(graph, target.instance, parameters, target.constraints, isLibraryMethod) - emptyList() + // Try to extract a body from substitution of our method or from the method itself. + // For the substitution, if it exists, we have a corresponding body and graph, + // but for the method itself its body might not be present in the memory. + // This may happen because of classloading issues (e.g. absence of required library JAR file) + val graph = (substitutedMethod ?: this).takeIf { it.canRetrieveBody() }?.jimpleBody()?.graph() + + if (graph != null) { + // If we have a graph to analyze, do it + pushToPathSelector(graph, target.instance, parameters, target.constraints, isLibraryMethod) + emptyList() + } else { + // Otherwise, depending on [treatAbsentMethodsAsUnboundedValue] either throw an exception + // or continue analysis with an unbounded variable as a result of the [this] method + if (UtSettings.treatAbsentMethodsAsUnboundedValue) { + listOf(unboundedVariable("methodWithoutBodyResult", method = this)) + } else { + error("Cannot retrieve body for a $declaredClassName.$name method") + } + } } } } @@ -2695,7 +3152,7 @@ class Traverser( ) } utOverrideMockDoesntThrowMethodName -> { - val stateToContinue = environment.state.updateQueued( + val stateToContinue = updateQueued( globalGraph.succ(environment.state.stmt), doesntThrow = true ) @@ -2707,7 +3164,7 @@ class Traverser( when (val param = parameters.single() as ReferenceValue) { is ObjectValue -> { val addr = param.addr.toIntValue() - val stateToContinue = environment.state.updateQueued( + val stateToContinue = updateQueued( globalGraph.succ(environment.state.stmt), SymbolicStateUpdate( hardConstraints = Le(addr, nullObjectAddr.toIntValue()).asHardConstraint() @@ -2726,7 +3183,7 @@ class Traverser( val update = MemoryUpdate( persistentListOf( - simplifiedNamedStore( + namedStore( descriptor, addr, UtArrayApplyForAll(memory.findArray(descriptor).select(addr)) { array, i -> @@ -2735,7 +3192,7 @@ class Traverser( ) ) ) - val stateToContinue = environment.state.updateQueued( + val stateToContinue = updateQueued( edge = globalGraph.succ(environment.state.stmt), SymbolicStateUpdate( hardConstraints = Le(addr.toIntValue(), nullObjectAddr.toIntValue()).asHardConstraint(), @@ -2780,22 +3237,28 @@ class Traverser( ) ) } - utArrayMockCopyOfMethodName -> { - val src = parameters[0] as ArrayValue - val length = parameters[1] as PrimitiveValue - val arrayType = target.method.returnType as ArrayType - val newArray = createNewArray(length, arrayType, arrayType.elementType) - return listOf( - MethodResult( - newArray, - memoryUpdates = arrayUpdateWithValue(newArray.addr, arrayType, selectArrayExpressionFromMemory(src)) - ) - ) - } + utArrayMockCopyOfMethodName -> return listOf(createArrayCopyWithSpecifiedLength(parameters)) else -> unreachableBranch("unknown method ${target.method.signature} for ${UtArrayMock::class.qualifiedName}") } } + private fun createArrayCopyWithSpecifiedLength(parameters: List): MethodResult { + val src = parameters[0] as ArrayValue + val length = parameters[1] as PrimitiveValue + val arrayType = src.type + + // Even if the new length differs from the original one, it does not affect elements - we will retrieve + // correct elements in the new array anyway + val newArray = createNewArray(length, arrayType, arrayType.elementType) + + // Since z3 arrays are persistent, we can just copy the whole original array value instead of manual + // setting elements equality by indices + return MethodResult( + newArray, + memoryUpdates = arrayUpdateWithValue(newArray.addr, arrayType, selectArrayExpressionFromMemory(src)) + ) + } + private fun utLogicMockInvoke(target: InvocationTarget, parameters: List): List { when (target.method.name) { utLogicMockLessMethodName -> { @@ -2863,6 +3326,22 @@ class Traverser( } } + // Return an unbounded symbolic variable for any overloading of method `forName` of class `java.lang.Class` + // NOTE: we cannot match by a subsignature here since `forName` method has a few overloadings + if (instance == null && invocation.method.declaringClass == CLASS_REF_SOOT_CLASS && invocation.method.name == "forName") { + val forNameResult = unboundedVariable(name = "classForName", invocation.method) + + return OverrideResult(success = true, forNameResult) + } + + // Return an unbounded symbolic variable for the `newInstance` method invocation, + // and at the next traversing step push graph of the resulted type + if (instance?.type == CLASS_REF_TYPE && subSignature == NEW_INSTANCE_SIGNATURE) { + val getInstanceResult = unboundedVariable(name = "newInstance", invocation.method) + + return OverrideResult(success = true, getInstanceResult) + } + val instanceAsWrapperOrNull = instance?.asWrapperOrNull if (instanceAsWrapperOrNull is UtMockWrapper && subSignature == HASHCODE_SIGNATURE) { @@ -2894,6 +3373,14 @@ class Traverser( return OverrideResult(success = true, cloneArray(instance)) } + if (instance == null && invocation.method.declaringClass == ARRAYS_SOOT_CLASS && invocation.method.name == "copyOf") { + return OverrideResult(success = true, copyOf(invocation.parameters)) + } + + if (instance == null && invocation.method.declaringClass == ARRAYS_SOOT_CLASS && invocation.method.name == "copyOfRange") { + return OverrideResult(success = true, copyOfRange(invocation.parameters)) + } + instanceAsWrapperOrNull?.run { // For methods with concrete implementation (for example, RangeModifiableUnlimitedArray.toCastedArray) // we should not return successful override result. @@ -2941,19 +3428,81 @@ class Traverser( val memoryUpdate = MemoryUpdate(touchedChunkDescriptors = persistentSetOf(descriptor)) - val clone = ArrayValue(TypeStorage(array.type), addr) + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(array.type) + val clone = ArrayValue(typeStorage, addr) + return MethodResult(clone, constraints.asHardConstraint(), memoryUpdates = memoryUpdate) } + private fun TraversalContext.copyOf(parameters: List): MethodResult { + val src = parameters[0] as ArrayValue + nullPointerExceptionCheck(src.addr) + + val length = parameters[1] as PrimitiveValue + val isNegativeLength = Lt(length, 0) + implicitlyThrowException(NegativeArraySizeException("Length is less than zero"), setOf(isNegativeLength)) + queuedSymbolicStateUpdates += mkNot(isNegativeLength).asHardConstraint() + + return createArrayCopyWithSpecifiedLength(parameters) + } + + private fun TraversalContext.copyOfRange(parameters: List): MethodResult { + val original = parameters[0] as ArrayValue + nullPointerExceptionCheck(original.addr) + + val from = parameters[1] as PrimitiveValue + val to = parameters[2] as PrimitiveValue + + val originalLength = memory.findArrayLength(original.addr) + + val isNegativeFrom = Lt(from, 0) + implicitlyThrowException(ArrayIndexOutOfBoundsException("From is less than zero"), setOf(isNegativeFrom)) + queuedSymbolicStateUpdates += mkNot(isNegativeFrom).asHardConstraint() + + val isFromBiggerThanLength = Gt(from, originalLength) + implicitlyThrowException(ArrayIndexOutOfBoundsException("From is bigger than original length"), setOf(isFromBiggerThanLength)) + queuedSymbolicStateUpdates += mkNot(isFromBiggerThanLength).asHardConstraint() + + val isFromBiggerThanTo = Gt(from, to) + implicitlyThrowException(IllegalArgumentException("From is bigger than to"), setOf(isFromBiggerThanTo)) + queuedSymbolicStateUpdates += mkNot(isFromBiggerThanTo).asHardConstraint() + + val newLength = Sub(to, from) + val newLengthValue = newLength.toIntValue() + + val originalLengthDifference = Sub(originalLength, from) + val originalLengthDifferenceValue = originalLengthDifference.toIntValue() + + val resultedLength = + UtIteExpression(Lt(originalLengthDifferenceValue, newLengthValue), originalLengthDifference, newLength) + val resultedLengthValue = resultedLength.toIntValue() + + val arrayType = original.type + val newArray = createNewArray(newLengthValue, arrayType, arrayType.elementType) + val destPos = 0.toPrimitiveValue() + val copyValue = UtArraySetRange( + selectArrayExpressionFromMemory(newArray), + destPos, + selectArrayExpressionFromMemory(original), + from, + resultedLengthValue + ) + + return MethodResult( + newArray, + memoryUpdates = arrayUpdateWithValue(newArray.addr, newArray.type, copyValue) + ) + } + // For now, we just create unbounded symbolic variable as a result. private fun processNativeMethod(target: InvocationTarget): List = listOf(unboundedVariable(name = "nativeConst", target.method)) private fun unboundedVariable(name: String, method: SootMethod): MethodResult { val value = when (val returnType = method.returnType) { - is RefType -> createObject(findNewAddr(), returnType, useConcreteType = true) + is RefType -> createObject(findNewAddr(), returnType, useConcreteType = true, mockInfoGenerator = null) is ArrayType -> createArray(findNewAddr(), returnType, useConcreteType = true) - else -> createConst(returnType, "$name${unboundedConstCounter++}") + else -> createConst(returnType, "$name${unboundedConstCounter++}", mockInfoGenerator = null) } return MethodResult(value) @@ -2985,25 +3534,10 @@ class Traverser( globalGraph.join(environment.state.stmt, graph, !isLibraryMethod) val parametersWithThis = listOfNotNull(caller) + callParameters offerState( - environment.state.push( - graph.head, - inputArguments = ArrayDeque(parametersWithThis), - queuedSymbolicStateUpdates + constraints.asHardConstraint(), - graph.body.method - ) + pushQueued(graph, parametersWithThis, constraints.asHardConstraint()) ) } - private fun ExecutionState.updateQueued( - edge: Edge, - update: SymbolicStateUpdate = SymbolicStateUpdate(), - doesntThrow: Boolean = false - ) = this.update( - edge, - queuedSymbolicStateUpdates + update, - doesntThrow - ) - private fun TraversalContext.resolveIfCondition(cond: BinopExpr): ResolvedCondition { // We add cond.op.type for null values only. If we have condition like "null == r1" // we'll have ObjectInstance(r1::type) and ObjectInstance(r1::type) for now @@ -3276,28 +3810,47 @@ class Traverser( private fun TraversalContext.intOverflowCheck(op: BinopExpr, leftRaw: PrimitiveValue, rightRaw: PrimitiveValue) { // cast to the bigger type val sort = simpleMaxSort(leftRaw, rightRaw) as UtPrimitiveSort - val left = leftRaw.expr.toPrimitiveValue(sort.type) - val right = rightRaw.expr.toPrimitiveValue(sort.type) + val left = UtCastExpression(leftRaw, sort.type) + val right = UtCastExpression(rightRaw, sort.type) + + val leftPrimValue = left.toPrimitiveValue(left.type) + val rightPrimValue = right.toPrimitiveValue(right.type) val overflow = when (op) { - is JAddExpr -> { - mkNot(UtAddNoOverflowExpression(left.expr, right.expr)) - } - is JSubExpr -> { - mkNot(UtSubNoOverflowExpression(left.expr, right.expr)) - } + is JAddExpr -> mkNot(UtAddNoOverflowExpression(left, right)) + is JSubExpr -> mkNot(UtSubNoOverflowExpression(left, right)) 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() } + is ByteType -> lowerIntMulOverflowCheck( + leftPrimValue, + rightPrimValue, + Byte.MIN_VALUE.toInt(), + Byte.MAX_VALUE.toInt() + ) + is ShortType -> lowerIntMulOverflowCheck( + leftPrimValue, + rightPrimValue, + Short.MIN_VALUE.toInt(), + Short.MAX_VALUE.toInt() + ) + is IntType -> higherIntMulOverflowCheck( + leftPrimValue, + rightPrimValue, + Int.SIZE_BITS, + Int.MIN_VALUE.toLong() + ) { it: UtExpression -> it.toIntValue() } + is LongType -> higherIntMulOverflowCheck( + leftPrimValue, + rightPrimValue, + Long.SIZE_BITS, + Long.MIN_VALUE + ) { it: UtExpression -> it.toLongValue() } else -> null } else -> null } if (overflow != null) { - implicitlyThrowException(ArithmeticException("${left.type} ${op.symbol} overflow"), setOf(overflow)) + implicitlyThrowException(OverflowDetectionError("${left.type} ${op.symbol} overflow"), setOf(overflow)) queuedSymbolicStateUpdates += mkNot(overflow).asHardConstraint() } } @@ -3332,17 +3885,14 @@ class Traverser( // Expected in the parameters baseType is an RefType because it is either an RefType itself or an array of RefType values if (baseTypeAfterCast is RefType) { // Find parameterized type for the object if it is a parameter of the method under test and it has generic type - val newAddr = addr.accept(solver.rewriter) as UtAddrExpression - val parameterizedType = when (newAddr.internal) { - is UtArraySelectExpression -> parameterAddrToGenericType[findTheMostNestedAddr(newAddr.internal)] - is UtBvConst -> parameterAddrToGenericType[newAddr] + val newAddr = addr.accept(solver.simplificator) as UtAddrExpression + val parameterizedTypes = when (newAddr.internal) { + is UtArraySelectExpression -> instanceAddrToGenericType[findTheMostNestedAddr(newAddr.internal)] + is UtBvConst -> instanceAddrToGenericType[newAddr] else -> null } - if (parameterizedType != null) { - // Find all generics used in the type of the parameter and it's superclasses - // If we're trying to cast something related to the parameter and typeAfterCast is equal to one of the generic - // types used in it, don't throw ClassCastException + parameterizedTypes?.forEach { parameterizedType -> val genericTypes = generateSequence(parameterizedType) { it.ownerType as? ParameterizedType } .flatMapTo(mutableSetOf()) { it.actualTypeArguments.map { arg -> arg.typeName } } @@ -3373,20 +3923,18 @@ class Traverser( } private fun TraversalContext.implicitlyThrowException( - exception: Exception, + throwable: Throwable, conditions: Set, softConditions: Set = emptySet() ) { if (environment.state.executionStack.last().doesntThrow) return - val symException = implicitThrown(exception, findNewAddr(), environment.state.isInNestedMethod()) + val symException = implicitThrown(throwable, findNewAddr(), environment.state.isInNestedMethod()) if (!traverseCatchBlock(environment.state.stmt, symException, conditions)) { environment.state.expectUndefined() - val nextState = environment.state.createExceptionState( + val nextState = createExceptionStateQueued( symException, - queuedSymbolicStateUpdates - + conditions.asHardConstraint() - + softConditions.asSoftConstraint() + SymbolicStateUpdate(conditions.asHardConstraint(), softConditions.asSoftConstraint()) ) globalGraph.registerImplicitEdge(nextState.lastEdge!!) offerState(nextState) @@ -3407,6 +3955,7 @@ class Traverser( val preferredTypes = typeResolver.findTopRatedTypes(possibleTypes, take = NUMBER_OF_PREFERRED_TYPES) val mostCommonType = preferredTypes.singleOrNull() ?: OBJECT_TYPE val typeStorage = typeResolver.constructTypeStorage(mostCommonType, preferredTypes) + return typeRegistry.typeConstraint(value.addr, typeStorage).isOrNullConstraint() } @@ -3442,6 +3991,7 @@ class Traverser( val declaringClassId = declaringClass.id val staticFieldsUpdates = updates.staticFieldsUpdates.toMutableList() + val fieldValuesUpdates = updates.fieldValues.toMutableMap() val updatedFields = staticFieldsUpdates.mapTo(mutableSetOf()) { it.fieldId } val objectUpdates = mutableListOf() @@ -3454,8 +4004,14 @@ class Traverser( // remove updates from clinit, because we'll replace those values // with new unbounded symbolic variable staticFieldsUpdates.removeAll { update -> update.fieldId == it.fieldId } + fieldValuesUpdates.keys.removeAll { key -> key.fieldId == it.fieldId } + + val generator = UtMockInfoGenerator { mockAddr -> + val fieldId = FieldId(it.declaringClass.id, it.name) + UtFieldMockInfo(it.type.classId, mockAddr, fieldId, ownerAddr = null) + } - val value = createConst(it.type, it.name) + val value = createConst(it.type, it.name, generator) val valueToStore = if (value is ReferenceValue) { value.addr } else { @@ -3473,6 +4029,7 @@ class Traverser( return updates.copy( stores = updates.stores.addAll(objectUpdates), staticFieldsUpdates = staticFieldsUpdates.toPersistentList(), + fieldValues = fieldValuesUpdates.toPersistentMap(), classIdToClearStatics = declaringClassId ) } @@ -3503,12 +4060,14 @@ class Traverser( val memory = symbolicState.memory val solver = symbolicState.solver - //no need to respect soft constraints in NestedMethod - val holder = solver.check(respectSoft = !environment.state.isInNestedMethod()) + if (!UtSettings.disableUnsatChecking) { + //no need to respect soft constraints in NestedMethod + val holder = solver.check(respectSoft = !environment.state.isInNestedMethod()) - if (holder !is UtSolverStatusSAT) { - logger.trace { "processResult<${environment.method.signature}> UNSAT" } - return + if (holder !is UtSolverStatusSAT) { + logger.trace { "processResult<${environment.method.signature}> UNSAT" } + return + } } val methodResult = MethodResult(symbolicResult) @@ -3525,7 +4084,7 @@ class Traverser( MemoryUpdate() // all memory updates are already added in [environment.state] } val methodResultWithUpdates = methodResult.copy(symbolicStateUpdate = queuedSymbolicStateUpdates + updates) - val stateToOffer = environment.state.pop(methodResultWithUpdates) + val stateToOffer = pop(methodResultWithUpdates) offerState(stateToOffer) logger.trace { "processResult<${environment.method.signature}> return from nested method" } @@ -3565,4 +4124,201 @@ class Traverser( queuedSymbolicStateUpdates = prevSymbolicStateUpdate } } + + private fun createExceptionStateQueued(exception: SymbolicFailure, update: SymbolicStateUpdate): ExecutionState { + val simplifiedUpdates = with (memoryUpdateSimplificator) { + simplifySymbolicStateUpdate(queuedSymbolicStateUpdates + update) + } + val simplifiedResult = with(simplificator) { + simplifySymbolicValue(exception.symbolic) + } + return environment.state.createExceptionState( + exception.copy(symbolic = simplifiedResult), + update = simplifiedUpdates + ) + } + + private fun updateQueued(update: SymbolicStateUpdate): ExecutionState { + val symbolicStateUpdate = with(memoryUpdateSimplificator) { + simplifySymbolicStateUpdate(queuedSymbolicStateUpdates + update) + } + + return environment.state.update( + symbolicStateUpdate, + ) + } + + private fun updateQueued( + edge: Edge, + update: SymbolicStateUpdate = SymbolicStateUpdate(), + doesntThrow: Boolean = false + ): ExecutionState { + val simplifiedUpdates = + with(memoryUpdateSimplificator) { + simplifySymbolicStateUpdate(queuedSymbolicStateUpdates + update) + } + + return environment.state.update( + edge, + simplifiedUpdates, + doesntThrow + ) + } + + private fun pushQueued( + graph: ExceptionalUnitGraph, + parametersWithThis: List, + hardConstraint: HardConstraint + ): ExecutionState { + val simplifiedUpdates = with(memoryUpdateSimplificator) { + simplifySymbolicStateUpdate(queuedSymbolicStateUpdates + hardConstraint) + } + return environment.state.push( + graph.head, + inputArguments = ArrayDeque(parametersWithThis), + simplifiedUpdates, + graph.body.method + ) + } + + private fun pop(methodResultWithUpdates: MethodResult): ExecutionState { + return environment.state.pop(methodResultWithUpdates) + } + + // taint analysis + + private fun TraversalContext.resolveMethodId( + invokeExpr: InvokeExpr, + methodResult: MethodResult + ): SymbolicMethodData { + val methodId = invokeExpr.method.executableId + // The method not in soot for some reasons. + // For example, it is synthetic and so it should not be processed in taint analysis. + val sootMethod = methodId.sootMethodOrNull ?: return SymbolicMethodData.constructInvalid(methodId) + + val symbolicBase = invokeExpr.baseOrNull()?.let { resolve(it, sootMethod.declaringClass.type) } + val symbolicArgs = resolveParameters(invokeExpr.args, sootMethod.parameterTypes) + val symbolicResult = (methodResult.symbolicResult as? SymbolicSuccess)?.value + + return SymbolicMethodData(methodId, symbolicBase, symbolicArgs, symbolicResult) + } + + private fun TraversalContext.processTaintAnalysis( + invokeExpr: InvokeExpr, + methodResult: MethodResult + ): SymbolicStateUpdate { + var result = SymbolicStateUpdate() + + val methodData = resolveMethodId(invokeExpr, methodResult) + result += processTaintSource(methodData) + result += processTaintCleaner(methodData) + result += processTaintPass(methodData) + + return result + } + + private fun processTaintSource(methodData: SymbolicMethodData): SymbolicStateUpdate { + val sourceConfigurations = taintContext.configuration.getSourcesBy(methodData.methodId) + val symbolicStateUpdates = sourceConfigurations.flatMap { source -> + val allAddresses = source.addTo.entities.mapNotNull { entity -> + methodData.choose(entity)?.addrOrNull + } + val condition = source.condition.toBoolExpr(this@Traverser, methodData) + + allAddresses.map { addr -> + taintContext.markManager.setMarks(memory, addr, source.marks, condition) + } + } + + return symbolicStateUpdates.fold(SymbolicStateUpdate()) { acc, update -> acc + update } + } + + private fun processTaintCleaner(methodData: SymbolicMethodData): SymbolicStateUpdate { + val cleanerConfigurations = taintContext.configuration.getCleanersBy(methodData.methodId) + val symbolicStateUpdates = cleanerConfigurations.flatMap { cleaner -> + val allAddresses = cleaner.removeFrom.entities.mapNotNull { entity -> + methodData.choose(entity)?.addrOrNull + } + val condition = cleaner.condition.toBoolExpr(this@Traverser, methodData) + + allAddresses.map { addr -> + taintContext.markManager.clearMarks(memory, addr, cleaner.marks, condition) + } + } + + return symbolicStateUpdates.fold(SymbolicStateUpdate()) { acc, update -> acc + update } + } + + private fun processTaintPass(methodData: SymbolicMethodData): SymbolicStateUpdate { + val passConfigurations = taintContext.configuration.getPassesBy(methodData.methodId) + val symbolicStateUpdates = passConfigurations.flatMap { pass -> + val getFromAddresses = pass.getFrom.entities.mapNotNull { entity -> + methodData.choose(entity)?.addrOrNull + } + val addToAddresses = pass.addTo.entities.mapNotNull { entity -> + methodData.choose(entity)?.addrOrNull + } + val condition = pass.condition.toBoolExpr(this@Traverser, methodData) + + getFromAddresses.flatMap { fromAddr -> + addToAddresses.map { toAddr -> + taintContext.markManager.passMarks(memory, fromAddr, toAddr, pass.marks, condition) + } + } + } + + return symbolicStateUpdates.fold(SymbolicStateUpdate()) { acc, update -> acc + update } + } + + private fun TraversalContext.processTaintSink(methodData: SymbolicMethodData) { + val sinkConfigurations = taintContext.configuration.getSinksBy(methodData.methodId) + sinkConfigurations.forEach { sink -> + val condition = sink.condition.toBoolExpr(this@Traverser, methodData) + sink.check.entities.forEach { entity -> + implicitlyThrowTaintError(methodData, entity, sink.marks, condition) + } + } + } + + private fun TraversalContext.implicitlyThrowTaintError( + methodData: SymbolicMethodData, + entity: TaintEntity, + marks: TaintMarks, + condition: UtBoolExpression, + ) { + val symbolicEntity = methodData.choose(entity) ?: return + val entityAddr = symbolicEntity.addrOrNull ?: return + + val methodName = methodData.methodId.simpleNameWithClass + val taintedVarType = symbolicEntity.type.toQuotedString() + + when (marks) { + is TaintMarksAll -> { + val containsAnyMark = taintContext.markManager.containsAnyMark(memory, entityAddr) + implicitlyThrowException( + TaintAnalysisError(methodName, taintedVarType, "tainted"), + setOf(mkAnd(containsAnyMark, condition)) + ) + } + is TaintMarksSet -> { + if (UtSettings.throwTaintErrorForEachMarkSeparately) { + marks.marks.forEach { mark -> + val containsMark = taintContext.markManager.containsMark(memory, entityAddr, mark) + implicitlyThrowException( + TaintAnalysisError(methodName, taintedVarType, mark.name), + setOf(mkAnd(containsMark, condition)) + ) + } + } else { + val containsAnySpecifiedMark = marks.marks.map { mark -> + taintContext.markManager.containsMark(memory, entityAddr, mark) + } + implicitlyThrowException( + TaintAnalysisError(methodName, taintedVarType, "tainted"), + setOf(mkAnd(mkOr(containsAnySpecifiedMark), condition)) + ) + } + } + } + } } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/TypeResolver.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/TypeResolver.kt deleted file mode 100644 index 6ad34adc84..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/TypeResolver.kt +++ /dev/null @@ -1,350 +0,0 @@ -package org.utbot.engine - -import org.utbot.common.WorkaroundReason -import org.utbot.common.workaround -import org.utbot.engine.pc.UtAddrExpression -import org.utbot.engine.pc.UtBoolExpression -import org.utbot.engine.pc.mkAnd -import org.utbot.engine.pc.mkEq -import org.utbot.framework.plugin.api.id -import org.utbot.framework.plugin.api.util.id -import soot.ArrayType -import soot.IntType -import soot.NullType -import soot.PrimType -import soot.RefType -import soot.Scene -import soot.SootField -import soot.Type -import soot.VoidType - -class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy: Hierarchy) { - - fun findOrConstructInheritorsIncludingTypes(type: RefType) = typeRegistry.findInheritorsIncludingTypes(type) { - hierarchy.inheritors(type.sootClass.id).mapTo(mutableSetOf()) { it.type } - } - - fun findOrConstructAncestorsIncludingTypes(type: RefType) = typeRegistry.findAncestorsIncludingTypes(type) { - hierarchy.ancestors(type.sootClass.id).mapTo(mutableSetOf()) { it.type } - } - - /** - * Finds all the inheritors for each type from the [types] and returns their intersection. - * - * Note: every type from the result satisfies [isAppropriate] condition. - */ - fun intersectInheritors(types: Array): Set = intersectTypes(types) { - findOrConstructInheritorsIncludingTypes(it) - } - - /** - * Finds all the ancestors for each type from the [types] and return their intersection. - * - * Note: every type from the result satisfies [isAppropriate] condition. - */ - fun intersectAncestors(types: Array): Set = intersectTypes(types) { - findOrConstructAncestorsIncludingTypes(it) - } - - private fun intersectTypes( - types: Array, - retrieveFunction: (RefType) -> Set - ): Set { - val allObjects = findOrConstructInheritorsIncludingTypes(OBJECT_TYPE) - - // TODO we do not support constructions like List here, be aware of it - // TODO JIRA:1446 - - return types - .map { classOrDefault(it.rawType.typeName) } - .fold(allObjects) { acc, value -> acc.intersect(retrieveFunction(value)) } - .filter { it.sootClass.isAppropriate } - .toSet() - } - - private fun classOrDefault(typeName: String): RefType = - runCatching { Scene.v().getRefType(typeName) }.getOrDefault(OBJECT_TYPE) - - fun findFields(type: RefType) = typeRegistry.findFields(type) { - hierarchy - .ancestors(type.sootClass.id) - .flatMap { it.fields } - } - - /** - * Returns given number of appropriate types that have the highest rating. - * - * @param types Collection of types to sort - * @param take Number of types to take - * - * @see TypeRegistry.findRating - * @see appropriateClasses - */ - fun findTopRatedTypes(types: Collection, take: Int = Int.MAX_VALUE) = - types.appropriateClasses() - .sortedByDescending { type -> - val baseType = if (type is ArrayType) type.baseType else type - // primitive baseType has the highest possible rating - if (baseType is RefType) typeRegistry.findRating(baseType) else Int.MAX_VALUE - } - .take(take) - - /** - * Constructs a [TypeStorage] instance containing [type] as its most common type and - * appropriate types from [possibleTypes] in its [TypeStorage.possibleConcreteTypes]. - * - * @param type the most common type of the constructed type storage. - * @param possibleTypes a list of types to be contained in the constructed type storage. - * - * @return constructed type storage. - * - * Note: [TypeStorage.possibleConcreteTypes] of the type storage returned by this method contains only - * classes we can instantiate: there will be no interfaces, abstract or local classes. - * If there are no such classes, [TypeStorage.possibleConcreteTypes] is an empty set. - * - * @see isAppropriate - */ - fun constructTypeStorage(type: Type, possibleTypes: Collection): TypeStorage { - val concretePossibleTypes = possibleTypes - .map { (if (it is ArrayType) it.baseType else it) to it.numDimensions } - .filterNot { (baseType, numDimensions) -> isInappropriateOrArrayOfMocksOrLocals(numDimensions, baseType) } - .mapTo(mutableSetOf()) { (baseType, numDimensions) -> - if (numDimensions == 0) baseType else baseType.makeArrayType(numDimensions) - } - - return TypeStorage(type, concretePossibleTypes).removeInappropriateTypes() - } - - 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. - * - * @param type a type for which we want to construct type storage. - * @param useConcreteType a boolean parameter to determine whether we want to include inheritors of the type or not. - * - * @return constructed type storage. - * - * Note: [TypeStorage.possibleConcreteTypes] of the type storage returned by this method contains only - * classes we can instantiate: there will be no interfaces, abstract or local classes. - * If there are no such classes, [TypeStorage.possibleConcreteTypes] is an empty set. - * - * @see isAppropriate - */ - fun constructTypeStorage(type: Type, useConcreteType: Boolean): TypeStorage { - // create a typeStorage with concreteType even if the type belongs to an interface or an abstract class - if (useConcreteType) return TypeStorage(type) - - val baseType = type.baseType - - val inheritors = if (baseType !is RefType) { - setOf(baseType) - } else { - // use only 'appropriate' classes in the TypeStorage construction - val allInheritors = findOrConstructInheritorsIncludingTypes(baseType) - - // if the type is ArrayType, we don't have to filter abstract classes and interfaces from the inheritors - // because we still can instantiate, i.e., Number[]. - if (type is ArrayType) { - allInheritors - } else { - allInheritors.filterTo(mutableSetOf()) { it.sootClass.isAppropriate } - } - } - - val extendedInheritors = if (baseType.isJavaLangObject()) inheritors + TypeRegistry.primTypes else inheritors - - val possibleTypes = when (type) { - is RefType, is PrimType -> extendedInheritors - is ArrayType -> when (baseType) { - is RefType -> extendedInheritors.map { it.makeArrayType(type.numDimensions) }.toSet() - else -> setOf(baseType.makeArrayType(type.numDimensions)) - } - else -> error("Unexpected type $type") - } - - return TypeStorage(type, possibleTypes).removeInappropriateTypes() - } - - /** - * Remove wrapper types and, if any other type is available, artificial entities. - */ - private fun TypeStorage.removeInappropriateTypes(): TypeStorage { - val leastCommonSootClass = (leastCommonType as? RefType)?.sootClass - val keepArtificialEntities = leastCommonSootClass?.isArtificialEntity == true - - val appropriateTypes = possibleConcreteTypes.filter { - // All not RefType should be included in the concreteTypes, e.g., arrays - val sootClass = (it.baseType as? RefType)?.sootClass ?: return@filter true - - // All artificial entities except anonymous functions should be filtered out if we have another types - if (sootClass.isArtificialEntity) { - if (sootClass.isLambda) { - return@filter true - } - - return@filter keepArtificialEntities - } - - // All wrappers should filtered out because they could not be instantiated - workaround(WorkaroundReason.HACK) { - if (leastCommonSootClass == OBJECT_TYPE && sootClass.isOverridden) { - return@filter false - } - } - - return@filter true - }.toSet() - - return copy(possibleConcreteTypes = appropriateTypes) - } - - /** - * Constructs a nullObject with TypeStorage containing all the inheritors for the given type - */ - fun nullObject(type: Type) = when (type) { - is RefType, is NullType, is VoidType -> ObjectValue(TypeStorage(type), nullObjectAddr) - is ArrayType -> ArrayValue(TypeStorage(type), nullObjectAddr) - else -> error("Unsupported nullType $type") - } - - fun downCast(arrayValue: ArrayValue, typeToCast: ArrayType): ArrayValue { - val typesBeforeCast = findOrConstructInheritorsIncludingTypes(arrayValue.type.baseType as RefType) - val typesAfterCast = findOrConstructInheritorsIncludingTypes(typeToCast.baseType as RefType) - val possibleTypes = typesBeforeCast.filter { it in typesAfterCast }.map { - it.makeArrayType(arrayValue.type.numDimensions) - } - - return arrayValue.copy(typeStorage = constructTypeStorage(typeToCast, possibleTypes)) - } - - fun downCast(objectValue: ObjectValue, typeToCast: RefType): ObjectValue { - val inheritorsTypes = findOrConstructInheritorsIncludingTypes(typeToCast) - val possibleTypes = objectValue.possibleConcreteTypes.filter { it in inheritorsTypes } - - return wrapper(typeToCast, objectValue.addr) ?: objectValue.copy( - typeStorage = constructTypeStorage( - typeToCast, - possibleTypes - ) - ) - } - - /** - * Connects types and number of dimensions for the two given addresses. Uses for reading from arrays: - * cell should have the same type and number of dimensions as the objects taken/put from/in it. - * It is a simplification, because the object can be subtype of the type of the cell, but it is ignored for now. - */ - fun connectArrayCeilType(ceilAddr: UtAddrExpression, valueAddr: UtAddrExpression): UtBoolExpression { - val ceilSymType = typeRegistry.symTypeId(ceilAddr) - val valueSymType = typeRegistry.symTypeId(valueAddr) - val ceilSymDimension = typeRegistry.symNumDimensions(ceilAddr) - val valueSymDimension = typeRegistry.symNumDimensions(valueAddr) - - return mkAnd(mkEq(ceilSymType, valueSymType), mkEq(ceilSymDimension, valueSymDimension)) - } - - fun findAnyConcreteInheritorIncludingOrDefaultUnsafe(evaluatedType: RefType, defaultType: RefType): RefType = - findAnyConcreteInheritorIncluding(evaluatedType) ?: findAnyConcreteInheritorIncluding(defaultType) - ?: error("No concrete types found neither for $evaluatedType, nor for $defaultType") - - fun findAnyConcreteInheritorIncludingOrDefault(evaluatedType: RefType, defaultType: RefType): RefType? = - findAnyConcreteInheritorIncluding(evaluatedType) ?: findAnyConcreteInheritorIncluding(defaultType) - - private fun findAnyConcreteInheritorIncluding(type: RefType): RefType? = - if (type.sootClass.isAppropriate) { - type - } else { - findOrConstructInheritorsIncludingTypes(type) - .filterNot { !it.hasSootClass() && (it.sootClass.isOverridden || it.sootClass.isUtMock) } - .sortedByDescending { typeRegistry.findRating(it) } - .firstOrNull { it.sootClass.isAppropriate } - } -} - -internal const val NUMBER_OF_PREFERRED_TYPES = 3 - -internal val SootField.isEnumOrdinal - get() = this.name == "ordinal" && this.declaringClass.name == ENUM_CLASSNAME - -internal val ENUM_CLASSNAME: String = java.lang.Enum::class.java.canonicalName -internal val ENUM_ORDINAL = ChunkId(ENUM_CLASSNAME, "ordinal") -internal val CLASS_REF_CLASSNAME: String = Class::class.java.canonicalName -internal val CLASS_REF_CLASS_ID = Class::class.java.id - -internal val CLASS_REF_TYPE_DESCRIPTOR: MemoryChunkDescriptor - get() = MemoryChunkDescriptor( - ChunkId(CLASS_REF_CLASSNAME, "modeledType"), - CLASS_REF_TYPE, - IntType.v() - ) - -internal val CLASS_REF_NUM_DIMENSIONS_DESCRIPTOR: MemoryChunkDescriptor - get() = MemoryChunkDescriptor( - ChunkId(CLASS_REF_CLASSNAME, "modeledNumDimensions"), - CLASS_REF_TYPE, - IntType.v() - ) - -internal val OBJECT_TYPE: RefType - get() = Scene.v().getSootClass(Object::class.java.canonicalName).type -internal val STRING_TYPE: RefType - get() = Scene.v().getSootClass(String::class.java.canonicalName).type -internal val CLASS_REF_TYPE: RefType - get() = Scene.v().getSootClass(CLASS_REF_CLASSNAME).type - -internal val HASHCODE_SIGNATURE: String = - Scene.v() - .getSootClass(Object::class.java.canonicalName) - .getMethodByName(Object::hashCode.name) - .subSignature - -internal val EQUALS_SIGNATURE: String = - Scene.v() - .getSootClass(Object::class.java.canonicalName) - .getMethodByName(Object::equals.name) - .subSignature - -/** - * Represents [java.lang.System.security] field signature. - * Hardcoded string literal because it is differently processed in Android. - */ -internal const val SECURITY_FIELD_SIGNATURE: String = "" - -/** - * Represents [sun.reflect.Reflection.fieldFilterMap] field signature. - * Hardcoded string literal because [sun.reflect.Reflection] is removed in Java 11. - */ -internal const val FIELD_FILTER_MAP_FIELD_SIGNATURE: String = "" - -/** - * Represents [sun.reflect.Reflection.methodFilterMap] field signature. - * Hardcoded string literal because [sun.reflect.Reflection] is removed in Java 11. - */ -internal const val METHOD_FILTER_MAP_FIELD_SIGNATURE: String = "" - -/** - * Special type represents string literal, which is not String Java object - */ -object SeqType : Type() { - override fun toString() = "SeqType" -} 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 86083c30b3..49ec333e76 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -1,35 +1,27 @@ package org.utbot.engine +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import mu.KotlinLogging import org.utbot.analytics.EngineAnalyticsContext import org.utbot.analytics.FeatureProcessor import org.utbot.analytics.Predictors -import org.utbot.common.bracket +import org.utbot.api.exception.UtMockAssumptionViolatedException import org.utbot.common.debug +import org.utbot.common.measureTime import org.utbot.engine.MockStrategy.NO_MOCKS -import org.utbot.engine.pc.UtArraySelectExpression -import org.utbot.engine.pc.UtBoolExpression -import org.utbot.engine.pc.UtContextInitializer -import org.utbot.engine.pc.UtSolver -import org.utbot.engine.pc.UtSolverStatusSAT -import org.utbot.engine.pc.findTheMostNestedAddr -import org.utbot.engine.pc.mkEq -import org.utbot.engine.pc.mkInt -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.mlSelector +import org.utbot.engine.pc.* +import org.utbot.engine.selectors.* 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.state.ExecutionStackElement +import org.utbot.engine.state.ExecutionState +import org.utbot.engine.state.StateLabel import org.utbot.engine.symbolic.SymbolicState import org.utbot.engine.symbolic.asHardConstraint +import org.utbot.engine.types.TypeRegistry +import org.utbot.engine.types.TypeResolver import org.utbot.engine.util.mockListeners.MockListener import org.utbot.engine.util.mockListeners.MockListenerController import org.utbot.framework.PathSelectorType @@ -40,73 +32,34 @@ import org.utbot.framework.UtSettings.pathSelectorStepsLimit import org.utbot.framework.UtSettings.pathSelectorType import org.utbot.framework.UtSettings.processUnknownStatesDuringConcreteExecution import org.utbot.framework.UtSettings.useDebugVisualization -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.ConcreteExecutionFailureException -import org.utbot.framework.plugin.api.EnvironmentModels -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.Instruction +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.context.ConcreteExecutionContext.FuzzingContextParams +import org.utbot.framework.fuzzer.ReferencePreservingIntIdGenerator +import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.Step -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtConcreteExecutionFailure -import org.utbot.framework.plugin.api.UtError -import org.utbot.framework.plugin.api.UtFailedExecution -import org.utbot.framework.plugin.api.UtInstrumentation -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.UtSymbolicExecution +import org.utbot.framework.plugin.api.util.* +import org.utbot.framework.util.calculateSize +import org.utbot.framework.util.convertToAssemble import org.utbot.framework.util.graph -import org.utbot.framework.plugin.api.util.isStatic -import org.utbot.framework.plugin.api.util.description -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.isConstructor -import org.utbot.framework.plugin.api.util.isEnum -import org.utbot.framework.plugin.api.util.utContext -import org.utbot.framework.plugin.api.util.voidClassId import org.utbot.framework.util.sootMethod -import org.utbot.fuzzer.FallbackModelProvider -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ReferencePreservingIntIdGenerator -import org.utbot.fuzzer.Trie -import org.utbot.fuzzer.TrieBasedFuzzerStatistics -import org.utbot.fuzzer.UtFuzzedExecution -import org.utbot.fuzzer.collectConstantsForFuzzer -import org.utbot.fuzzer.defaultModelMutators -import org.utbot.fuzzer.defaultModelProviders -import org.utbot.fuzzer.flipCoin -import org.utbot.fuzzer.fuzz -import org.utbot.fuzzer.providers.ObjectModelProvider -import org.utbot.fuzzer.withMutations +import org.utbot.fuzzer.* +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.Trie import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionData +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import org.utbot.taint.* +import org.utbot.taint.model.TaintConfiguration import soot.jimple.Stmt import soot.tagkit.ParamNamesTag import java.lang.reflect.Method -import kotlin.random.Random +import java.util.function.Consumer +import kotlin.math.min import kotlin.system.measureTimeMillis -import kotlinx.collections.immutable.persistentListOf -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 org.utbot.framework.plugin.api.UtExecutionSuccess -import org.utbot.framework.plugin.api.UtLambdaModel -import org.utbot.framework.plugin.api.util.executable -import org.utbot.fuzzer.toFuzzerType - -val logger = KotlinLogging.logger {} + +private val logger = KotlinLogging.logger {} val pathLogger = KotlinLogging.logger(logger.name + ".path") //in future we should put all timeouts here @@ -130,6 +83,9 @@ private fun pathSelector(graph: InterProceduralUnitGraph, typeRegistry: TypeRegi PathSelectorType.INHERITORS_SELECTOR -> inheritorsSelector(graph, typeRegistry) { withStepsLimit(pathSelectorStepsLimit) } + PathSelectorType.BFS_SELECTOR -> bfsSelector(graph, StrategyOption.VISIT_COUNTING) { + withStepsLimit(pathSelectorStepsLimit) + } PathSelectorType.SUBPATH_GUIDED_SELECTOR -> subpathGuidedSelector(graph, StrategyOption.DISTANCE) { withStepsLimit(pathSelectorStepsLimit) } @@ -158,10 +114,14 @@ class UtBotSymbolicEngine( private val methodUnderTest: ExecutableId, classpath: String, dependencyPaths: String, - mockStrategy: MockStrategy = NO_MOCKS, + val mockStrategy: MockStrategy = NO_MOCKS, chosenClassesToMockAlways: Set, - private val solverTimeoutInMillis: Int = checkSolverTimeoutMillis + val applicationContext: ApplicationContext, + val concreteExecutionContext: ConcreteExecutionContext, + userTaintConfigurationProvider: TaintConfigurationProvider? = null, + private val solverTimeoutInMillis: Int = checkSolverTimeoutMillis, ) : UtContextInitializer() { + private val graph = methodUnderTest.sootMethod.jimpleBody().apply { logger.trace { "JIMPLE for $methodUnderTest:\n$this" } }.graph() @@ -183,15 +143,48 @@ class UtBotSymbolicEngine( classUnderTest, hierarchy, chosenClassesToMockAlways, - MockListenerController(controller) + MockListenerController(controller), + mockerContext = applicationContext.mockerContext, ) + private val stateListeners: MutableList = mutableListOf(); + + fun addListener(listener: ExecutionStateListener): UtBotSymbolicEngine { + stateListeners += listener + return this + } + + fun removeListener(listener: ExecutionStateListener): UtBotSymbolicEngine { + stateListeners -= listener + return this + } + fun attachMockListener(mockListener: MockListener) = mocker.mockListenerController?.attach(mockListener) fun detachMockListener(mockListener: MockListener) = mocker.mockListenerController?.detach(mockListener) private val statesForConcreteExecution: MutableList = mutableListOf() + private val taintConfigurationProvider = if (UtSettings.useTaintAnalysis) { + TaintConfigurationProviderCombiner( + listOf( + userTaintConfigurationProvider ?: TaintConfigurationProviderEmpty, + TaintConfigurationProviderCached("resources", TaintConfigurationProviderResources()) + ) + ) + } else { + TaintConfigurationProviderEmpty + } + private val taintConfiguration: TaintConfiguration = run { + val config = taintConfigurationProvider.getConfiguration() + logger.debug { "Taint analysis configuration: $config" } + config + } + + private val taintMarkRegistry: TaintMarkRegistry = TaintMarkRegistry() + private val taintMarkManager: TaintMarkManager = TaintMarkManager(taintMarkRegistry) + private val taintContext: TaintContext = TaintContext(taintMarkManager, taintConfiguration) + private val traverser = Traverser( methodUnderTest, typeRegistry, @@ -199,6 +192,9 @@ class UtBotSymbolicEngine( typeResolver, globalGraph, mocker, + applicationContext.typeReplacer, + applicationContext.nonNullSpeculator, + taintContext, ) //HACK (long strings) @@ -206,9 +202,8 @@ class UtBotSymbolicEngine( private val concreteExecutor = ConcreteExecutor( - UtExecutionInstrumentation, + concreteExecutionContext.instrumentationFactory, classpath, - dependencyPaths ).apply { this.classLoader = utContext.classLoader } private val featureProcessor: FeatureProcessor? = @@ -239,6 +234,22 @@ class UtBotSymbolicEngine( .onStart { preTraverse() } .onCompletion { postTraverse() } + /** + * Traverse through all states and get results. + * + * This method is supposed to used when calling [traverse] is not suitable, + * e.g. from Java programs. It runs traversing with blocking style using callback + * to provide [UtResult]. + */ + @JvmOverloads + fun traverseAll(consumer: Consumer = Consumer { }) { + runBlocking { + traverse().collect { + consumer.accept(it) + } + } + } + private fun traverseImpl(): Flow = flow { require(trackableResources.isEmpty()) @@ -275,7 +286,7 @@ class UtBotSymbolicEngine( "queue size=${(pathSelector as? NonUniformRandomSearch)?.size ?: -1}" } - if (controller.executeConcretely || statesForConcreteExecution.isNotEmpty()) { + if (UtSettings.useConcreteExecution && (controller.executeConcretely || statesForConcreteExecution.isNotEmpty())) { val state = pathSelector.pollUntilFastSAT() ?: statesForConcreteExecution.pollUntilSat(processUnknownStatesDuringConcreteExecution) ?: break @@ -286,7 +297,7 @@ class UtBotSymbolicEngine( logger.trace { "executing $state concretely..." } - logger.debug().bracket("concolicStrategy<$methodUnderTest>: execute concretely") { + logger.debug().measureTime({ "concolicStrategy<$methodUnderTest>: execute concretely"} ) { val resolver = Resolver( hierarchy, state.memory, @@ -294,7 +305,8 @@ class UtBotSymbolicEngine( typeResolver, state.solver.lastStatus as UtSolverStatusSAT, methodUnderTest, - softMaxArraySize + softMaxArraySize, + traverser.objectCounter ) val resolvedParameters = state.methodUnderTestParameters @@ -303,13 +315,22 @@ class UtBotSymbolicEngine( try { val concreteExecutionResult = - concreteExecutor.executeConcretely(methodUnderTest, stateBefore, instrumentation) + concreteExecutor.executeConcretely(methodUnderTest, stateBefore, instrumentation, UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis) + + if (failureCanBeProcessedGracefully(concreteExecutionResult, executionToRollbackOn = null)) { + return@measureTime + } + + if (concreteExecutionResult.violatesUtMockAssumption()) { + logger.debug { "Generated test case violates the UtMock assumption: $concreteExecutionResult" } + return@measureTime + } val concreteUtExecution = UtSymbolicExecution( stateBefore, concreteExecutionResult.stateAfter, concreteExecutionResult.result, - instrumentation, + concreteExecutionResult.newInstrumentation ?: instrumentation, mutableListOf(), listOf(), concreteExecutionResult.coverage @@ -319,13 +340,21 @@ class UtBotSymbolicEngine( logger.debug { "concolicStrategy<${methodUnderTest}>: returned $concreteUtExecution" } } catch (e: CancellationException) { logger.debug(e) { "Cancellation happened" } - } catch (e: ConcreteExecutionFailureException) { + } catch (e: InstrumentedProcessDeathException) { emitFailedConcreteExecutionResult(stateBefore, e) } catch (e: Throwable) { emit(UtError("Concrete execution failed", e)) } } + // I am not sure this part works correctly when concrete execution is enabled. + // todo test this part more accurate + try { + fireExecutionStateEvent(state) + } catch (ce: CancellationException) { + break + } + } else { val state = pathSelector.poll() @@ -370,142 +399,173 @@ class UtBotSymbolicEngine( // TODO: think about concise modifying globalGraph in Traverser and UtBotSymbolicEngine globalGraph.visitNode(state) + + try { + fireExecutionStateEvent(state) + } catch (ce: CancellationException) { + break + } } } } } + private fun fireExecutionStateEvent(state: ExecutionState) { + stateListeners.forEach { l -> + try { + l.visit(globalGraph, state) + } catch (t: Throwable) { + logger.error(t) { "$l failed with error" } + } + } + } + /** * 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 { + fun fuzzing(until: Long = Long.MAX_VALUE) = flow { val isFuzzable = methodUnderTest.parameters.all { classId -> - classId != Method::class.java.id && // causes the child process crash at invocation + classId != Method::class.java.id && // causes the instrumented process crash at invocation classId != Class::class.java.id // causes java.lang.IllegalAccessException: java.lang.Class at sun.misc.Unsafe.allocateInstance(Native Method) } if (!isFuzzable) { return@flow } - - val fallbackModelProvider = FallbackModelProvider(defaultIdGenerator) - val constantValues = collectConstantsForFuzzer(graph) - - val syntheticMethodForFuzzingThisInstanceDescription = - FuzzedMethodDescription("thisInstance", voidClassId, listOf(classUnderTest), constantValues).apply { - className = classUnderTest.simpleName - packageName = classUnderTest.packageName - } - - val random = Random(0) - val thisInstance = when { - methodUnderTest.isStatic -> null - methodUnderTest.isConstructor -> if ( - classUnderTest.isAbstract || // can't instantiate abstract class - classUnderTest.isEnum // can't reflectively create enum objects - ) { - return@flow - } else { - null - } - else -> { - ObjectModelProvider(defaultIdGenerator).withFallback(fallbackModelProvider).generate( - syntheticMethodForFuzzingThisInstanceDescription - ).take(10).shuffled(random).map { it.value.model }.first().apply { - if (this is UtNullModel) { // it will definitely fail because of NPE, - return@flow - } - } - } - } - - val methodUnderTestDescription = FuzzedMethodDescription(methodUnderTest, collectConstantsForFuzzer(graph)).apply { - compilableName = if (!methodUnderTest.isConstructor) methodUnderTest.name else null - className = classUnderTest.simpleName - packageName = classUnderTest.packageName - val names = graph.body.method.tags.filterIsInstance().firstOrNull()?.names - parameterNameMap = { index -> names?.getOrNull(index) } - fuzzerType = { try { toFuzzerType(methodUnderTest.executable.genericParameterTypes[it]) } catch (_: Throwable) { null } } - } - val coveredInstructionTracker = Trie(Instruction::id) - val coveredInstructionValues = linkedMapOf, List>() + val errorStackTraceTracker = Trie(StackTraceElement::toString) var attempts = 0 val attemptsLimit = UtSettings.fuzzingMaxAttempts - val hasMethodUnderTestParametersToFuzz = methodUnderTest.parameters.isNotEmpty() - val fuzzedValues = if (hasMethodUnderTestParametersToFuzz) { - fuzz(methodUnderTestDescription, modelProvider(defaultModelProviders(defaultIdGenerator))) - } else { - // in case a method with no parameters is passed fuzzing tries to fuzz this instance with different constructors, setters and field mutators - fuzz(syntheticMethodForFuzzingThisInstanceDescription, ObjectModelProvider(defaultIdGenerator).apply { - totalLimit = 500 - }) - }.withMutations( - TrieBasedFuzzerStatistics(coveredInstructionValues), methodUnderTestDescription, *defaultModelMutators().toTypedArray() - ) - fuzzedValues.forEach { values -> - if (controller.job?.isActive == false || System.currentTimeMillis() >= until) { + val names = graph.body.method.tags.filterIsInstance().firstOrNull()?.names ?: emptyList() + var testEmittedByFuzzer = 0 + + val fuzzingContext = try { + concreteExecutionContext.tryCreateFuzzingContext( + FuzzingContextParams( + concreteExecutor = concreteExecutor, + classUnderTest = classUnderTest, + idGenerator = defaultIdGenerator, + fuzzingStartTimeMillis = System.currentTimeMillis(), + fuzzingEndTimeMillis = until, + mockStrategy = mockStrategy, + ) + ) + } catch (e: Exception) { + emit(UtError(e.message ?: "Failed to create ValueProvider", e)) + return@flow + } + + val coverageToMinStateBeforeSize = mutableMapOf, Int>() + + runJavaFuzzing( + defaultIdGenerator, + methodUnderTest, + constants = collectConstantsForFuzzer(graph), + names = names, + providers = listOf(fuzzingContext.valueProvider), + ) { thisInstance, descr, values -> + val diff = until - System.currentTimeMillis() + val thresholdMillisForFuzzingOperation = 0 // may be better use 10-20 millis as it might not be possible + // to concretely execute that values because request to instrumentation process involves + // 1. serializing/deserializing it with kryo + // 2. sending over rd + // 3. concrete execution itself + // 4. analyzing concrete result + if (controller.job?.isActive == false || diff <= thresholdMillisForFuzzingOperation) { logger.info { "Fuzzing overtime: $methodUnderTest" } - return@flow + logger.info { "Test created by fuzzer: $testEmittedByFuzzer" } + return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) } - val initialEnvironmentModels = if (hasMethodUnderTestParametersToFuzz) { - EnvironmentModels(thisInstance, values.map { it.model }, mapOf()) - } else { - check(values.size == 1 && values.first().model is UtAssembleModel) - EnvironmentModels(values.first().model, emptyList(), mapOf()) + if (thisInstance?.model is UtNullModel) { + // We should not try to run concretely any models with null-this. + // But fuzzer does generate such values, because it can fail to generate any "good" values. + return@runJavaFuzzing BaseFeedback(Trie.emptyNode(), Control.PASS) } + val stateBefore = fuzzingContext.createStateBefore( + thisInstance = thisInstance?.model, + parameters = values.map { it.model }, + statics = emptyMap(), + executableToCall = methodUnderTest, + ) + val concreteExecutionResult: UtConcreteExecutionResult? = try { - concreteExecutor.executeConcretely(methodUnderTest, initialEnvironmentModels, listOf()) + val timeoutMillis = min(UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis, diff) + concreteExecutor.executeConcretely(methodUnderTest, stateBefore, listOf(), timeoutMillis) } catch (e: CancellationException) { logger.debug { "Cancelled by timeout" }; null - } catch (e: ConcreteExecutionFailureException) { - emitFailedConcreteExecutionResult(initialEnvironmentModels, e); null + } catch (e: InstrumentedProcessDeathException) { + emitFailedConcreteExecutionResult(stateBefore, e); null } catch (e: Throwable) { emit(UtError("Default concrete execution failed", e)); null } // in case an exception occurred from the concrete execution - concreteExecutionResult ?: return@forEach + concreteExecutionResult ?: return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + + fuzzingContext.handleFuzzedConcreteExecutionResult(methodUnderTest, concreteExecutionResult) + + // in case of processed failure in the concrete execution + concreteExecutionResult.processedFailure()?.let { failure -> + logger.debug { "Instrumented process failed with exception ${failure.exception} before concrete execution started" } + return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + } + if (concreteExecutionResult.violatesUtMockAssumption()) { + logger.debug { "Generated test case by fuzzer violates the UtMock assumption: $concreteExecutionResult" } + return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + } + + val result = concreteExecutionResult.result val coveredInstructions = concreteExecutionResult.coverage.coveredInstructions + var trieNode: Trie.Node? = null + if (coveredInstructions.isNotEmpty()) { - val coverageKey = coveredInstructionTracker.add(coveredInstructions) - if (coverageKey.count > 1) { + trieNode = descr.tracer.add(coveredInstructions) + + val earlierStateBeforeSize = coverageToMinStateBeforeSize[trieNode] + val curStateBeforeSize = concreteExecutionResult.stateBefore.calculateSize() + + if (earlierStateBeforeSize == null || curStateBeforeSize < earlierStateBeforeSize) + coverageToMinStateBeforeSize[trieNode] = curStateBeforeSize + else { if (++attempts >= attemptsLimit) { - return@flow + return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) } - // Update the seeded values sometimes - // This is necessary because some values cannot do a good values in mutation in any case - if (random.flipCoin(probability = 50)) { - coveredInstructionValues[coverageKey] = values - } - return@forEach + return@runJavaFuzzing BaseFeedback(result = trieNode, control = Control.CONTINUE) } - coveredInstructionValues[coverageKey] = values } else { - logger.error { "Coverage is empty for $methodUnderTest with ${values.map { it.model }}" } + logger.error { "Coverage is empty for $methodUnderTest with $values" } + if (result is UtSandboxFailure) { + val stackTraceElements = result.exception.stackTrace.reversed() + if (errorStackTraceTracker.add(stackTraceElements).count > 1) { + return@runJavaFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + } + } } emit( UtFuzzedExecution( - stateBefore = initialEnvironmentModels, + stateBefore = concreteExecutionResult.stateBefore, stateAfter = concreteExecutionResult.stateAfter, result = concreteExecutionResult.result, coverage = concreteExecutionResult.coverage, fuzzingValues = values, - fuzzedMethodDescription = methodUnderTestDescription + fuzzedMethodDescription = descr.description, + instrumentation = concreteExecutionResult.newInstrumentation ?: emptyList() ) ) + + testEmittedByFuzzer++ + BaseFeedback(result = trieNode ?: Trie.emptyNode(), control = Control.CONTINUE) } } private suspend fun FlowCollector.emitFailedConcreteExecutionResult( stateBefore: EnvironmentModels, - e: ConcreteExecutionFailureException + e: Throwable ) { val failedConcreteExecution = UtFailedExecution( stateBefore = stateBefore, @@ -526,14 +586,26 @@ class UtBotSymbolicEngine( val solver = state.solver val parameters = state.parameters.map { it.value } val symbolicResult = requireNotNull(state.methodResult?.symbolicResult) { "The state must have symbolicResult" } - val holder = requireNotNull(solver.lastStatus as? UtSolverStatusSAT) { "The state must be SAT!" } + val holder = if (UtSettings.disableUnsatChecking) { + (solver.lastStatus as? UtSolverStatusSAT) ?: return + } else { + requireNotNull(solver.lastStatus as? UtSolverStatusSAT) { "The state must be SAT!" } + } val predictedTestName = Predictors.testName.predict(state.path) Predictors.testName.provide(state.path, predictedTestName, "") // resolving - val resolver = - Resolver(hierarchy, memory, typeRegistry, typeResolver, holder, methodUnderTest, softMaxArraySize) + val resolver = Resolver( + hierarchy, + memory, + typeRegistry, + typeResolver, + holder, + methodUnderTest, + softMaxArraySize, + traverser.objectCounter + ) val (modelsBefore, modelsAfter, instrumentation) = resolver.resolveModels(parameters) @@ -549,7 +621,8 @@ class UtBotSymbolicEngine( result = symbolicExecutionResult, instrumentation = instrumentation, path = entryMethodPath(state), - fullPath = state.fullPath() + fullPath = state.fullPath(), + symbolicSteps = getSymbolicPath(state, symbolicResult) ) globalGraph.traversed(state) @@ -557,7 +630,9 @@ class UtBotSymbolicEngine( if (!UtSettings.useConcreteExecution || // Can't execute concretely because overflows do not cause actual exceptions. // Still, we need overflows to act as implicit exceptions. - (UtSettings.treatOverflowAsError && symbolicExecutionResult is UtOverflowFailure) + (UtSettings.treatOverflowAsError && symbolicExecutionResult is UtOverflowFailure) || + // the same for taint analysis errors + (UtSettings.useTaintAnalysis && symbolicExecutionResult is UtTaintAnalysisFailure) ) { logger.debug { "processResult<${methodUnderTest}>: no concrete execution allowed, " + @@ -578,30 +653,82 @@ class UtBotSymbolicEngine( return } + if (checkStaticMethodsMock(symbolicUtExecution)) { + logger.debug { + buildString { + append("processResult<${methodUnderTest}>: library static methods mock found ") + append("(we do not support it in concrete execution yet), ") + append("emit purely symbolic result $symbolicUtExecution") + } + } + + emit(symbolicUtExecution) + return + } + + //It's possible that symbolic and concrete stateAfter/results are diverged. //So we trust concrete results more. try { - logger.debug().bracket("processResult<$methodUnderTest>: concrete execution") { + logger.debug().measureTime({ "processResult<$methodUnderTest>: concrete execution" } ) { //this can throw CancellationException val concreteExecutionResult = concreteExecutor.executeConcretely( methodUnderTest, stateBefore, - instrumentation + instrumentation, + UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis ) + if (failureCanBeProcessedGracefully(concreteExecutionResult, symbolicUtExecution)) { + return + } + + if (concreteExecutionResult.violatesUtMockAssumption()) { + logger.debug { "Generated test case violates the UtMock assumption: $concreteExecutionResult" } + return + } + val concolicUtExecution = symbolicUtExecution.copy( stateAfter = concreteExecutionResult.stateAfter, result = concreteExecutionResult.result, - coverage = concreteExecutionResult.coverage + coverage = concreteExecutionResult.coverage, + instrumentation = concreteExecutionResult.newInstrumentation ?: instrumentation ) emit(concolicUtExecution) logger.debug { "processResult<${methodUnderTest}>: returned $concolicUtExecution" } } - } catch (e: ConcreteExecutionFailureException) { + } catch (e: InstrumentedProcessDeathException) { emitFailedConcreteExecutionResult(stateBefore, e) + } catch (e: CancellationException) { + logger.debug(e) { "Cancellation happened" } + } catch (e: Throwable) { + emit(UtError("Default concrete execution failed", e)) + } + } + + private suspend fun FlowCollector.failureCanBeProcessedGracefully( + concreteExecutionResult: UtConcreteExecutionResult, + executionToRollbackOn: UtExecution?, + ): Boolean { + concreteExecutionResult.processedFailure()?.let { failure -> + // If concrete execution failed to some reasons that are not process death or cancellation + // when we call something that is processed successfully by symbolic engine, + // we should: + // - roll back to symbolic execution data ignoring failing concrete (is symbolic execution exists); + // - do not emit an execution if there is nothing to roll back on. + + // Note that this situation is suspicious anyway, so we log a WARN message about the failure. + executionToRollbackOn?.let { + emit(it) + } + + logger.warn { "Instrumented process failed with exception ${failure.exception} before concrete execution started" } + return true } + + return false } /** @@ -619,6 +746,41 @@ class UtBotSymbolicEngine( } return entryPath } + + private fun getSymbolicPath(state: ExecutionState, symbolicResult: SymbolicResult): List { + val pathWithLines = state.fullPath().filter { step -> + step.stmt.javaSourceStartLineNumber != -1 + } + + val symbolicSteps = pathWithLines.map { step -> + val method = globalGraph.method(step.stmt) + SymbolicStep(method, step.stmt.javaSourceStartLineNumber, step.depth) + }.filter { step -> + step.method.declaringClass.packageName == methodUnderTest.classId.packageName + } + + return if (symbolicResult is SymbolicFailure && symbolicSteps.last().callDepth != 0) { + // If we have the following case: + // - method m1 calls method m2 + // - m2 calls m3 + // - m3 throws exception + // then `symbolicSteps` suffix looks like: + // - ... + // - method = m3, lineNumber = .., callDepth = 2 + // - method = m3, lineNumber = 30, callDepth = 2 <- line with thrown exception + // - method = m2, lineNumber = 20, callDepth = 1 + // - method = m1, lineNumber = 10, callDepth = 0 + // So, we want to remove 2 last entries (m1 and m2) because the execution finished at the line 30, + // but `state.fullPath()` contains also reverse exits from methods after exception. + // So, we need to remove the elements from the end of the list until the depth of the neighbors is the same. + symbolicSteps + .zipWithNext() + .dropLastWhile { (cur, next) -> cur.callDepth != next.callDepth } + .map { (cur, _) -> cur } + } else { + symbolicSteps + } + } } private fun ResolvedModels.constructStateForMethod(methodUnderTest: ExecutableId): EnvironmentModels { @@ -627,18 +789,25 @@ private fun ResolvedModels.constructStateForMethod(methodUnderTest: ExecutableId methodUnderTest.isConstructor -> null to parameters.drop(1) else -> parameters.first() to parameters.drop(1) } - return EnvironmentModels(thisInstanceBefore, paramsBefore, statics) + return EnvironmentModels(thisInstanceBefore, paramsBefore, statics, methodUnderTest) } -private suspend fun ConcreteExecutor.executeConcretely( +suspend fun ConcreteExecutor>.executeConcretely( methodUnderTest: ExecutableId, stateBefore: EnvironmentModels, - instrumentation: List + instrumentation: List, + timeoutInMillis: Long, + isRerun: Boolean = false, ): UtConcreteExecutionResult = executeAsync( methodUnderTest.classId.name, methodUnderTest.signature, arrayOf(), - parameters = UtConcreteExecutionData(stateBefore, instrumentation) + parameters = UtConcreteExecutionData( + stateBefore, + instrumentation, + timeoutInMillis, + isRerun, + ) ).convertToAssemble(methodUnderTest.classId.packageName) /** @@ -686,3 +855,17 @@ private fun makeWrapperConsistencyCheck( val visitedSelectExpression = memory.isVisited(symbolicValue.addr) visitedConstraints += mkEq(visitedSelectExpression, mkInt(1)) } + +private fun UtConcreteExecutionResult.violatesUtMockAssumption(): Boolean { + // We should compare FQNs instead of `if (... is UtMockAssumptionViolatedException)` + // because the exception from the `concreteExecutionResult` is loaded by user's ClassLoader, + // but the `UtMockAssumptionViolatedException` is loaded by the current ClassLoader, + // so we can't cast them to each other. + return result.exceptionOrNull()?.javaClass?.name == UtMockAssumptionViolatedException::class.java.name +} + +private fun UtConcreteExecutionResult.processedFailure(): UtConcreteExecutionProcessedFailure? + = result as? UtConcreteExecutionProcessedFailure + +private fun checkStaticMethodsMock(execution: UtSymbolicExecution) = + execution.instrumentation.any { it is UtStaticMethodInstrumentation} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/ExtendedArrayExpressions.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/ExtendedArrayExpressions.kt index e905322647..637dc6fccd 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/ExtendedArrayExpressions.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/ExtendedArrayExpressions.kt @@ -12,7 +12,6 @@ data class UtArrayInsert( ) : UtExtendedArrayExpression(arrayExpression.sort as UtArraySort) { override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - override val hashCode = Objects.hash(arrayExpression, index, element) override fun hashCode(): Int = hashCode @@ -162,29 +161,6 @@ data class UtArrayRemoveRange( } } -data class UtStringToArray( - val stringExpression: UtExpression, - val offset: PrimitiveValue -) : UtExtendedArrayExpression(UtArraySort(UtIntSort, UtCharSort)) { - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override val hashCode = Objects.hash(stringExpression, offset) - - override fun hashCode(): Int = hashCode - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtStringToArray - - if (stringExpression != other.stringExpression) return false - if (offset != other.offset) return false - - return true - } -} - data class UtArrayApplyForAll( val arrayExpression: UtExpression, val constraint: (UtExpression, PrimitiveValue) -> UtBoolExpression 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 6911237cea..0b7b579360 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 @@ -82,7 +82,7 @@ class UnsatQuery(hard: PersistentSet) : BaseQuery( /** * Query of UtExpressions with applying simplifications if [useExpressionSimplification] is true. * - * @see RewritingVisitor + * @see Simplificator * * @param eqs - map that matches symbolic expressions with concrete values. * @param lts - map of upper bounds of integral symbolic expressions. @@ -99,10 +99,10 @@ data class Query( private val gts: PersistentMap = persistentHashMapOf() ) : BaseQuery(hard, soft, assumptions, status, lastAdded) { - val rewriter: RewritingVisitor - get() = RewritingVisitor(eqs, lts, gts) + val simplificator: Simplificator + get() = Simplificator(eqs, lts, gts) - private fun UtBoolExpression.simplify(visitor: RewritingVisitor): UtBoolExpression = + private fun UtBoolExpression.simplify(visitor: Simplificator): UtBoolExpression = this.accept(visitor) as UtBoolExpression private fun simplifyGeneric(expr: UtBoolExpression): UtBoolExpression = @@ -132,7 +132,7 @@ data class Query( eqs: Map = this@Query.eqs, lts: Map = this@Query.lts, gts: Map = this@Query.gts - ) = AxiomInstantiationRewritingVisitor(eqs, lts, gts).let { visitor -> + ) = AxiomInstantiationSimplificator(eqs, lts, gts).let { visitor -> this.map { it.simplify(visitor) } .map { simplifyGeneric(it) } .flatMap { splitAnd(it) } + visitor.instantiatedArrayAxioms @@ -165,19 +165,6 @@ data class Query( } } - /** - * Mark that part of UtStringEq is equal to concrete part to substitute it in constraints added later. - * - * Eq expressions with both concrete parts are simplified in RewritingVisitor.visit(UtStringEq) - */ - private fun MutableMap.putEq(eqExpr: UtStringEq) { - when { - eqExpr.left.isConcrete -> this[eqExpr.right] = eqExpr.left - eqExpr.right.isConcrete -> this[eqExpr.left] = eqExpr.right - else -> this[eqExpr] = UtTrue - } - } - /** * @return * [this] if constraints are satisfied under this model. @@ -208,7 +195,6 @@ data class Query( for (expr in addedHard) { when (expr) { is UtEqExpression -> addedEqs.putEq(expr) - is UtStringEq -> addedEqs.putEq(expr) is UtBoolOpExpression -> when (expr.operator) { Eq -> addedEqs.putEq(expr) Lt -> if (expr.right.expr.isConcrete && expr.right.expr.isInteger()) { diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/RewritingVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/RewritingVisitor.kt deleted file mode 100644 index 9453aa393e..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/RewritingVisitor.kt +++ /dev/null @@ -1,1414 +0,0 @@ -package org.utbot.engine.pc - -import org.utbot.common.unreachableBranch -import org.utbot.engine.Add -import org.utbot.engine.And -import org.utbot.engine.Cmp -import org.utbot.engine.Cmpg -import org.utbot.engine.Cmpl -import org.utbot.engine.Div -import org.utbot.engine.Eq -import org.utbot.engine.Ge -import org.utbot.engine.Gt -import org.utbot.engine.Le -import org.utbot.engine.Lt -import org.utbot.engine.Mul -import org.utbot.engine.Ne -import org.utbot.engine.Or -import org.utbot.engine.PrimitiveValue -import org.utbot.engine.Rem -import org.utbot.engine.Shl -import org.utbot.engine.Shr -import org.utbot.engine.Sub -import org.utbot.engine.Ushr -import org.utbot.engine.Xor -import org.utbot.engine.maxSort -import org.utbot.engine.primitiveToLiteral -import org.utbot.engine.primitiveToSymbolic -import org.utbot.engine.symbolic.asHardConstraint -import org.utbot.engine.toIntValue -import org.utbot.engine.toPrimitiveValue -import org.utbot.framework.UtSettings.useExpressionSimplification -import java.util.IdentityHashMap -import java.util.concurrent.atomic.AtomicInteger -import kotlinx.collections.immutable.toPersistentList -import soot.DoubleType -import soot.FloatType -import soot.IntType -import soot.IntegerType -import soot.LongType -import soot.Type - - -/** - * UtExpressionVisitor that performs simple rewritings on expressions with concrete values. - * - */ -open class RewritingVisitor( - private val eqs: Map = emptyMap(), - private val lts: Map = emptyMap(), - private val gts: Map = emptyMap() -) : UtExpressionVisitor { - val axiomInstantiationVisitor: RewritingVisitor - get() = AxiomInstantiationRewritingVisitor(eqs, lts, gts) - protected val selectIndexStack = mutableListOf() - private val simplificationCache = IdentityHashMap() - - private fun allConcrete(vararg exprs: UtExpression) = exprs.all { it.isConcrete } - - protected inline fun withNewSelect(index: UtExpression, block: () -> R): R { - selectIndexStack += index.toIntValue() - return block().also { - selectIndexStack.removeLast() - } - } - - @Suppress("MemberVisibilityCanBePrivate") - protected inline fun withEmptySelects(block: () -> R): R { - val currentSelectStack = selectIndexStack.toList() - selectIndexStack.clear() - return block().also { - selectIndexStack.addAll(currentSelectStack) - } - } - - @Suppress("MemberVisibilityCanBePrivate") - protected inline fun withRemoveSelect(block: () -> R): R { - val lastIndex = selectIndexStack.removeLastOrNull() - return block().also { - if (lastIndex != null) { - selectIndexStack += lastIndex - } - } - } - - /** - * if [useExpressionSimplification] is true then rewrite [expr] and - * substitute concrete values from [eqs] map for equal symbolic expressions. - * - * @param expr - simplified expression - * @param block - simplification function. Identity function by default - * @param substituteResult - if true then try to substitute result of block(expr) - * with equal concrete value from [eqs] map. - */ - private inline fun applySimplification( - expr: UtExpression, - substituteResult: Boolean = true, - crossinline block: (() -> UtExpression) = { expr }, - ): UtExpression { - val value = { - if (!useExpressionSimplification) { - expr - } else { - eqs[expr] ?: block().let { if (substituteResult) eqs[it] ?: it else it } - } - } - return if (expr.sort is UtArraySort && selectIndexStack.isNotEmpty()) { - checkSortsEquality(value, expr) - } else { - simplificationCache.getOrPut(expr) { - checkSortsEquality(value, expr) - } - } - } - - private fun checkSortsEquality(value: () -> UtExpression, sourceExpr: UtExpression): UtExpression { - val result = value() - - val sourceSort = sourceExpr.sort - val resultSort = result.sort - - // We should return simplified expression with the same sort as the original one. - // The only exception is a case with two primitive sorts and cast expression below. - // So, expression with, e.g., UtArraySort must be returned here. - if (resultSort == sourceSort) return result - - // There we must have the following situation: original expression is `x = Add(a, b)`, where - // x :: Int32, a = 0 :: Int32, b :: Short16. Simplifier removes the left part of the addition since it is - // equal to zero, now we have just b with a different from the original expression sort. - // We must cast it to the required sort, so the result will be `Cast(b, Int32)`. - require(sourceSort is UtPrimitiveSort && resultSort is UtPrimitiveSort) { - val messageParts = listOf( - "Expr to simplify had sort ${sourceSort}:", sourceExpr, "", - "After simplification expression got sort ${resultSort}:", result - ) - messageParts.joinToString(System.lineSeparator(), prefix = System.lineSeparator()) - } - - val expectedType = sourceSort.type - val resultType = resultSort.type - - return UtCastExpression(result.toPrimitiveValue(resultType), expectedType) - } - - override fun visit(expr: UtArraySelectExpression): UtExpression = - applySimplification(expr) { - val index = withEmptySelects { expr.index.accept(this) } - when (val array = withNewSelect(index) { expr.arrayExpression.accept(this) }) { - // select(constArray(a), i) --> a - is UtConstArrayExpression -> array.constValue - is UtArrayMultiStoreExpression -> { - // select(store(..(store(array, Concrete a, b), Concrete x, _)..), Concrete a) -> b - val newStores = array.stores.filter { UtEqExpression(index, it.index).accept(this) != UtFalse } - - when { - newStores.isEmpty() -> array.initial.select(index).accept(this) - UtEqExpression(index, newStores.last().index).accept(this) == UtTrue -> newStores.last().value - else -> UtArrayMultiStoreExpression(array.initial, newStores.toPersistentList()).select(index) - } - } - else -> array.select(index) - } - } - - override fun visit(expr: UtMkArrayExpression): UtExpression = expr - - // store(store(store(array, 1, b), 2, a), 1, a) ---> store(store(array, 2, a), 1, a) - override fun visit(expr: UtArrayMultiStoreExpression): UtExpression = - applySimplification(expr) { - val initial = expr.initial.accept(this) - val stores = withRemoveSelect { - expr.stores.map { - val index = it.index.accept(this) - val value = it.value.accept(this) - - require(value.sort == expr.sort.itemSort) { - "Unequal sorts occurred during rewriting UtArrayMultiStoreExpression:\n" + - "value with index $index has sort ${value.sort} while " + - "${expr.sort.itemSort} was expected." - } - - UtStore(index, value) - } - }.filterNot { it.value == initial.select(it.index) } - when { - stores.isEmpty() -> initial - initial is UtArrayMultiStoreExpression -> UtArrayMultiStoreExpression( - initial.initial, - (initial.stores + stores).asReversed().distinctBy { it.index }.asReversed().toPersistentList() - ) - else -> UtArrayMultiStoreExpression( - initial, - stores.asReversed().distinctBy { it.index }.asReversed().toPersistentList() - ) - } - } - - override fun visit(expr: UtBvLiteral): UtExpression = expr - - override fun visit(expr: UtBvConst): UtExpression = applySimplification(expr, true) - - override fun visit(expr: UtAddrExpression): UtExpression = - applySimplification(expr) { - UtAddrExpression( - expr.internal.accept(this) - ) - } - - override fun visit(expr: UtFpLiteral): UtExpression = expr - - override fun visit(expr: UtFpConst): UtExpression = applySimplification(expr) - - private fun evalIntegralLiterals(literal: UtExpression, sort: UtSort, block: (Long) -> Number): UtExpression = - block(literal.toLong()).toValueWithSort(sort).primitiveToLiteral() - - private fun evalIntegralLiterals( - left: UtExpression, - right: UtExpression, - sort: UtSort, - block: (Long, Long) -> Number - ): UtExpression = - block(left.toLong(), right.toLong()).toValueWithSort(sort).primitiveToLiteral() - - private fun evalIntegralLiterals( - left: UtExpression, - right: UtExpression, - block: (Long, Long) -> Boolean - ): UtExpression = - block(left.toLong(), right.toLong()).primitiveToLiteral() - - private fun Number.toValueWithSort(sort: UtSort): Any = - when (sort) { - UtBoolSort -> this != UtLongFalse - UtByteSort -> this.toByte() - UtShortSort -> this.toShort() - UtCharSort -> this.toChar() - UtIntSort -> this.toInt() - UtLongSort -> this.toLong() - UtFp32Sort -> this.toFloat() - UtFp64Sort -> this.toDouble() - else -> error("Can convert Number values only to primitive sorts. Sort $sort isn't primitive") - } - - private fun UtBoolLiteral.toLong(): Long = if (value) UtLongTrue else UtLongFalse - - private fun UtBvLiteral.toLong(): Long = value.toLong() - - private fun UtExpression.toLong(): Long = when (this) { - is UtBvLiteral -> toLong() - is UtBoolLiteral -> toLong() - is UtAddrExpression -> internal.toLong() - else -> error("$this isn't IntegralLiteral") - } - - private val UtExpression.isIntegralLiteral: Boolean - get() = when (this) { - is UtBvLiteral -> true - is UtBoolLiteral -> true - is UtAddrExpression -> internal.isIntegralLiteral - else -> false - } - - override fun visit(expr: UtOpExpression): UtExpression = - applySimplification(expr) { - val left = expr.left.expr.accept(this) - val right = expr.right.expr.accept(this) - val leftPrimitive = left.toPrimitiveValue(expr.left.type) - val rightPrimitive = right.toPrimitiveValue(expr.right.type) - - when (expr.operator) { - // Sort of pattern matching... - Add -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-addConcrete - // Add(Integral a, Integral b) ---> Integral (a + b) - left.isIntegralLiteral && right.isIntegralLiteral -> - evalIntegralLiterals(left, right, expr.resultSort, Long::plus) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-addA0 - // Add(a, Integral 0) ---> a - right.isIntegralLiteral && right.toLong() == 0L -> left - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-addLiteralToRight - // Add(Concrete a, b) ---> Add(b, Concrete a).accept(this) - left.isIntegralLiteral -> Add( - rightPrimitive, - leftPrimitive - ).accept(this) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-addAddOpenLiteral - // Add(Add(x, Integral a), Integral b)) -> Add(x, Integral (a + b)) - right.isIntegralLiteral && left is UtOpExpression && left.operator is Add && left.right.expr.isIntegralLiteral -> - Add( - left.left, - evalIntegralLiterals(left.right.expr, right, expr.right.expr.sort, Long::plus) - .toPrimitiveValue(expr.right.type) - ) - else -> Add(leftPrimitive, rightPrimitive) - } - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-subToAdd - // Sub(a, b) ---> Add(a, Neg b) - Sub -> Add( - leftPrimitive, - UtNegExpression(rightPrimitive).toPrimitiveValue(expr.right.type) - ).accept(this) - Mul -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulConcrete - // Mul(Integral a, Integral b) ---> Integral (a * b) - left.isIntegralLiteral && right.isIntegralLiteral -> - evalIntegralLiterals(left, right, expr.resultSort, Long::times) - right.isIntegralLiteral -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulA0 - // Mul(a, b@(Integral 0)) ---> b - right.toLong() == 0L -> right - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulA1 - // Mul(a, Integral 1) ---> a - right.toLong() == 1L -> left - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulAMinus1 - // Mul(a, Integral -1) ---> Neg a - right.toLong() == -1L -> UtNegExpression(leftPrimitive) - - // Mul(Op(_, Integral _), Integral _) - left is UtOpExpression && left.right.expr.isIntegralLiteral -> when (left.operator) { - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulDistr - // Mul(Add(x, Integral a), Integral b) ---> Add(Mul(x, b), Integral (a * b)) - is Add -> Add( - Mul( - left.left, - rightPrimitive - ).toPrimitiveValue(expr.left.type), - evalIntegralLiterals(left.right.expr, right, expr.resultSort, Long::times) - .toPrimitiveValue(expr.right.type) - ) - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulMulOpenLiteral - // Mul(Mul(x, Integral a), Integral b) ---> Mul(x, Integral (a * b)) - is Mul -> Mul( - left.left, - evalIntegralLiterals(left.right.expr, right, expr.right.expr.sort, Long::times) - .toPrimitiveValue(expr.right.type) - ) - else -> Mul(leftPrimitive, rightPrimitive) - } - else -> Mul(leftPrimitive, rightPrimitive) - } - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulLiteralToRight - // Mul(a@(Integral _), b) ---> Mul(b, a) - left.isIntegralLiteral -> Mul( - rightPrimitive, - leftPrimitive - ).accept(this) - else -> Mul(leftPrimitive, rightPrimitive) - } - Div -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-divConcrete - // Div(Integral a, Integral b) ---> Integral (a / b) - left.isIntegralLiteral && right.isIntegralLiteral && right.toLong() != 0L -> - evalIntegralLiterals(left, right, expr.resultSort, Long::div) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-divA1 - // Div(a, Concrete 1) ---> a - right.isIntegralLiteral && right.toLong() == 1L -> left - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-divAMinus1 - // Div(a, Concrete -1) ---> Neg a - right.isIntegralLiteral && right.toLong() == -1L -> UtNegExpression(leftPrimitive) - else -> Div(leftPrimitive, rightPrimitive) - } - And -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andConcrete - // And(Integral a, Integral b) ---> Integral (a `and` b) - left.isIntegralLiteral && right.isIntegralLiteral -> - evalIntegralLiterals(left, right, expr.resultSort, Long::and) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andA0 - // And(a@(Integral 0), b) ---> a - left.isIntegralLiteral && left.toLong() == 0L -> - evalIntegralLiterals(left, expr.resultSort) { it } - right.isIntegralLiteral && right.toLong() == 0L -> - evalIntegralLiterals(right, expr.resultSort) { it } - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andAtoRight - left.isIntegralLiteral -> And( - rightPrimitive, - leftPrimitive - ) - else -> And(leftPrimitive, rightPrimitive) - } - Cmp, Cmpg, Cmpl -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-cmpConcrete - // Cmp(Integral a, Integral b) ---> Integral (cmp a b) - left.isIntegralLiteral && right.isIntegralLiteral -> - evalIntegralLiterals(left, right, expr.resultSort) { a, b -> - when { - a < b -> -1 - a == b -> 0 - else -> 1 - } - } - else -> expr.operator(leftPrimitive, rightPrimitive) - } - Or -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-orConcrete - // Or(Integral a, Integral b) ---> Integral (a `or` b) - left.isIntegralLiteral && right.isIntegralLiteral -> - evalIntegralLiterals(left, right, expr.resultSort, Long::or) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-orA0 - // Or(a, b@(Integral 0)) ---> a - left.isIntegralLiteral && left.toLong() == 0L -> right - right.isIntegralLiteral && right.toLong() == 0L -> left - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-orLiteralToRight - // Or(a@(Integral _),b) ---> Or(b, a) - left.isIntegralLiteral -> Or( - rightPrimitive, - leftPrimitive - ) - else -> Or(leftPrimitive, rightPrimitive) - } - Shl -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-shlConcrete - // Shl(Integral a, Integral b) ---> Integral (a `shl` b) - left.isIntegralLiteral && right.isIntegralLiteral -> - evalIntegralLiterals(left, right, expr.resultSort) { a, b -> - val size: Int = (expr.sort as UtBvSort).size - a shl (b.toInt() % size) - } - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-shlCycle - // Shl(a, Integral b) && b % a.bit_size == 0 ---> a - right.isIntegralLiteral && right.toLong() % (expr.resultSort as UtBvSort).size == 0L -> left - else -> Shl(leftPrimitive, rightPrimitive) - } - Shr -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-shrConcrete - // Shr(Integral a, Integral b) ---> Integral (a `shr` b) - left.isIntegralLiteral && right.isIntegralLiteral -> - evalIntegralLiterals(left, right, expr.resultSort) { a, b -> - val size: Int = (expr.sort as UtBvSort).size - a shr (b.toInt() % size) - } - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-shrCycle - // Shr(a, Integral b) && b % a.bit_size == 0 ---> a - right.isIntegralLiteral && right.toLong() % (expr.resultSort as UtBvSort).size == 0L -> left - else -> Shr(leftPrimitive, rightPrimitive) - } - Ushr -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-ushrConcrete - // Ushr(Integral a, Integral b) ---> Integral (a `ushr` b) - left.isIntegralLiteral && right.isIntegralLiteral -> - evalIntegralLiterals(left, right, expr.resultSort) { a, b -> - val size: Int = (expr.sort as UtBvSort).size - if (a >= 0) { - a ushr (b.toInt() % size) - } else { - // 0b0..01..1 where mask.countOneBits() = size - val mask: Long = ((1L shl size) - 1L) - (a and mask) ushr (b.toInt() % size) - } - } - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-ushrCycle - // Ushr(a, Integral b) && b % a.bit_size == 0 ---> a - right.isIntegralLiteral && right.toLong() % (expr.resultSort as UtBvSort).size == 0L -> left - else -> Ushr(leftPrimitive, rightPrimitive) - } - Xor -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-xorConcrete - // Xor(Integral a, Integral b) ---> Integral (a `xor` b) - left.isIntegralLiteral && right.isIntegralLiteral -> - evalIntegralLiterals(left, right, expr.resultSort, Long::xor) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-xorA0 - // Xor(a, Integral 0) ---> a - right.isIntegralLiteral && right.toLong() == 0L -> left - left.isIntegralLiteral && left.toLong() == 0L -> right - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-xorEqual - // Xor(a, a) ---> Integral 0 - left == right -> (0L).toValueWithSort(expr.resultSort).primitiveToSymbolic().expr - else -> Xor(leftPrimitive, rightPrimitive) - } - Rem -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-remConcrete - // Rem(Integral a, Integral b) ---> Integral (a % b) - left.isIntegralLiteral && right.isIntegralLiteral && right.toLong() != 0L -> - evalIntegralLiterals(left, right, expr.resultSort, Long::rem) - else -> Rem(leftPrimitive, rightPrimitive) - } - } - } - - - override fun visit(expr: UtTrue): UtExpression = expr - - override fun visit(expr: UtFalse): UtExpression = expr - - override fun visit(expr: UtEqExpression): UtExpression = - applySimplification(expr) { - val left = expr.left.accept(this) - val right = expr.right.accept(this) - when { - // Eq(Integral a, Integral b) ---> a == b - left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right, Long::equals) - // Eq(Fp a, Fp b) ---> a == b - left is UtFpLiteral && right is UtFpLiteral -> mkBool(left.value == right.value) - // Eq(Expr(sort = Boolean), Integral _) - left is UtIteExpression -> { - val thenEq = UtEqExpression(left.thenExpr, right).accept(this) - val elseEq = UtEqExpression(left.elseExpr, right).accept(this) - if (thenEq is UtFalse && elseEq is UtFalse) { - UtFalse - } else if (thenEq is UtTrue && elseEq is UtFalse) { - left.condition - } else if (thenEq is UtFalse && elseEq is UtTrue) { - mkNot(left.condition) - } else { - UtEqExpression(left, right) - } - } - right is UtIteExpression -> UtEqExpression(right, left).accept(this) - left is UtBoolExpression && right.isIntegralLiteral -> when { - // Eq(a, true) ---> a - right.toLong() == UtLongTrue -> left - // Eq(a, false) ---> not a - right.toLong() == UtLongFalse -> NotBoolExpression(left).accept(this) - else -> UtEqExpression(left, right) - } - right.isIntegralLiteral -> simplifyEq(left, right) - // Eq(a, Fp NaN) ---> False - right is UtFpLiteral && right.value.toDouble().isNaN() -> UtFalse - // Eq(a@(Concrete _), b) ---> Eq(b, a) - left.isConcrete && !right.isConcrete -> UtEqExpression(right, left).accept(this) - // Eq(a a) && a.sort !is FPSort -> true - left == right && left.sort !is UtFp32Sort && left.sort !is UtFp64Sort -> UtTrue - else -> UtEqExpression(left, right) - } - } - - override fun visit(expr: UtBoolConst): UtExpression = applySimplification(expr, false) - - private fun negate(expr: UtBoolLiteral): UtBoolLiteral = mkBool(expr == UtFalse) - - override fun visit(expr: NotBoolExpression): UtExpression = - applySimplification(expr) { - when (val exp = expr.expr.accept(this) as UtBoolExpression) { - // not true ---> false - // not false ---> true - is UtBoolLiteral -> negate(exp) - is UtBoolOpExpression -> { - val left = exp.left - val right = exp.right - if (left.type is IntegerType && right.type is IntegerType) { - when (exp.operator) { - // not Ne(a, b) ---> Eq(a, b) - Ne -> Eq(left, right) - // not Ge(a, b) ---> Lt(a, b) - Ge -> Lt(left, right) - // not Gt(a, b) ---> Le(a, b) - Gt -> Le(left, right) - // not Lt(a, b) ---> Ge(a, b) - Lt -> Ge(left, right) - // not Le(a, b) ---> Gt(a, b) - Le -> Gt(left, right) - Eq -> NotBoolExpression(exp) - } - } else { - when (exp.operator) { - // simplification not Ne(a, b) ---> Eq(a, b) is always valid - Ne -> Eq(left, right) - else -> NotBoolExpression(exp) - } - } - } - // not not expr -> expr - is NotBoolExpression -> exp.expr - // not (and a_1, a_2, ..., a_n) ---> or (not a_1), (not a_2), ..., (not a_n) - is UtAndBoolExpression -> UtOrBoolExpression(exp.exprs.map { NotBoolExpression(it).accept(this) as UtBoolExpression }) - // not (or a_1, a_2, ..., a_n) ---> and (not a_1), (not a_2), ..., (not a_n) - is UtOrBoolExpression -> UtAndBoolExpression(exp.exprs.map { NotBoolExpression(it).accept(this) as UtBoolExpression }) - else -> NotBoolExpression(exp) - } - } - - override fun visit(expr: UtOrBoolExpression): UtExpression = - applySimplification(expr) { - val exprs = expr.exprs.map { it.accept(this) as UtBoolExpression }.filterNot { it == UtFalse } - .flatMap { splitOr(it) } - when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrEmpty - exprs.isEmpty() -> UtFalse - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrValue - exprs.any { it == UtTrue } -> UtTrue - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andorSingle - exprs.size == 1 -> exprs.single() - else -> UtOrBoolExpression(exprs) - } - } - - override fun visit(expr: UtAndBoolExpression): UtExpression = - applySimplification(expr) { - val exprs = expr.exprs.map { it.accept(this) as UtBoolExpression }.filterNot { it == UtTrue } - .flatMap { splitAnd(it) } - when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrEmpty - exprs.isEmpty() -> UtTrue - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrValue - exprs.any { it == UtFalse } -> UtFalse - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrSingle - exprs.size == 1 -> exprs.single() - else -> UtAndBoolExpression(exprs) - } - } - - override fun visit(expr: UtAddNoOverflowExpression): UtExpression = - UtAddNoOverflowExpression( - applySimplification(expr.left), - applySimplification(expr.right) - ) - - override fun visit(expr: UtSubNoOverflowExpression): UtExpression = - UtSubNoOverflowExpression( - applySimplification(expr.left), - applySimplification(expr.right) - ) - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-negConcrete - // Neg (Concrete a) ---> Concrete (-a) - override fun visit(expr: UtNegExpression): UtExpression = - applySimplification(expr) { - val variable = expr.variable.expr.accept(this) - if (variable.isConcrete) { - val value = variable.toConcrete() - when (variable.sort) { - UtByteSort -> mkInt(-(value as Byte)) - UtShortSort -> mkInt(-(value as Short)) - UtIntSort -> mkInt(-(value as Int)) - UtLongSort -> mkLong(-(value as Long)) - UtFp32Sort -> mkFloat(-(value as Float)) - UtFp64Sort -> mkDouble(-(value as Double)) - else -> UtNegExpression(variable.toPrimitiveValue(expr.variable.type)) - } - } else { - UtNegExpression(variable.toPrimitiveValue(expr.variable.type)) - } - } - - override fun visit(expr: UtCastExpression): UtExpression = - applySimplification(expr) { - val newExpr = expr.variable.expr.accept(this) - when { - // Cast(a@(PrimitiveValue type), type) ---> a - expr.type == expr.variable.type -> newExpr - newExpr.isConcrete -> when (newExpr) { - is UtBoolLiteral -> newExpr.toLong().toValueWithSort(expr.sort).primitiveToSymbolic().expr - is UtBvLiteral -> newExpr.toLong().toValueWithSort(expr.sort).primitiveToSymbolic().expr - is UtFpLiteral -> newExpr.value.toValueWithSort(expr.sort).primitiveToSymbolic().expr - else -> UtCastExpression(newExpr.toPrimitiveValue(expr.variable.type), expr.type) - } - else -> UtCastExpression(newExpr.toPrimitiveValue(expr.variable.type), expr.type) - } - } - - - /** - * Simplify expression [Eq](left, right), where [right] operand is integral literal, - * using information about lower and upper bounds of [left] operand from [lts] and [gts] maps. - * - * @return - * [UtFalse], if lower bound of [left] is greater than [right].value - * or upper bound of [left] is less than [right].value - * - * [Eq](left, right), otherwise - */ - private fun simplifyEq(left: PrimitiveValue, right: PrimitiveValue): UtBoolExpression { - val leftExpr = if (left.expr is UtAddrExpression) left.expr.internal else left.expr - val lowerBound = gts[leftExpr] - val upperBound = lts[leftExpr] - - return if (lowerBound == null && upperBound == null) { - Eq(left, right) - } else { - val rightValue = right.expr.toLong() - when { - lowerBound != null && lowerBound > rightValue -> UtFalse - upperBound != null && upperBound < rightValue -> UtFalse - else -> Eq(left, right) - } - } - } - - private fun simplifyEq(left: UtExpression, right: UtExpression): UtBoolExpression { - val leftExpr = if (left is UtAddrExpression) left.internal else left - val lowerBound = gts[leftExpr] - val upperBound = lts[leftExpr] - return if (lowerBound == null && upperBound == null) { - UtEqExpression(left, right) - } else { - val rightValue = right.toLong() - when { - lowerBound != null && lowerBound > rightValue -> UtFalse - upperBound != null && upperBound < rightValue -> UtFalse - else -> UtEqExpression(left, right) - } - } - } - - - /** - * Simplify expression [Gt](left, right) or [Ge](left, right), where [right] operand is integral literal, - * using information about lower and upper bounds of [left] operand from [lts] and [gts] maps. - * - * @param including - if true than simplified expression is Ge(left,right), else Gt(left, right) - * @return - * [UtTrue], if lower bound of [left] is greater than [right].value. - * - * [UtFalse], if upper bound of [left] is less than [right].value. - * - * [Eq](left, right), if interval [[right].value, upper bound of [left]] has length = 1. - * - * [Gt]|[Ge](left, right), otherwise, depending on value of [including] parameter. - */ - private fun simplifyGreater(left: PrimitiveValue, right: PrimitiveValue, including: Boolean): UtExpression { - val leftExpr = if (left.expr is UtAddrExpression) left.expr.internal else left.expr - val lowerBound = gts[leftExpr] - val upperBound = lts[leftExpr] - val operator = if (including) Ge else Gt - - return if (lowerBound == null && upperBound == null) { - operator(left, right) - } else { - val rightValue = right.expr.toLong() + if (including) 0 else 1 - when { - // left > Long.MAX_VALUE - rightValue == Long.MIN_VALUE && !including -> UtFalse - lowerBound != null && lowerBound >= rightValue -> UtTrue - upperBound != null && upperBound < rightValue -> UtFalse - upperBound != null && rightValue - upperBound == 0L -> Eq( - left, - rightValue.toValueWithSort(right.expr.sort).primitiveToSymbolic() - ) - else -> operator(left, right) - } - } - } - - /** - * Simplify expression [Lt](left, right) or [Le](left, right), where [right] operand is integral literal, - * using information about lower and upper bounds of [left] operand from [lts] and [gts] maps. - * - * @param including - if true than simplified expression is Le(left, right), else Lt(left, right) - * @return - * [UtTrue], if upper bound of [left] is less than [right].value. - * - * [UtFalse], if lower bound of [left] is greater than [right].value. - * - * [Eq](left, right), if interval [lower bound of [left], [right].value] has length = 1. - * - * [Lt]|[Le](left, right), otherwise, depending on value of [including] parameter. - */ - private fun simplifyLess(left: PrimitiveValue, right: PrimitiveValue, including: Boolean): UtExpression { - val leftExpr = if (left.expr is UtAddrExpression) left.expr.internal else left.expr - val lowerBound = gts[leftExpr] - val upperBound = lts[leftExpr] - val operator = if (including) Le else Lt - - return if (upperBound == null && lowerBound == null) { - operator(left, right) - } else { - val rightValue = right.expr.toLong() - if (including) 0 else 1 - when { - // left < Long.MIN_VALUE - rightValue == Long.MAX_VALUE && !including -> UtFalse - upperBound != null && upperBound <= rightValue -> UtTrue - lowerBound != null && lowerBound > rightValue -> UtFalse - lowerBound != null && rightValue - lowerBound == 0L -> Eq( - left, - rightValue.toValueWithSort(right.expr.sort).primitiveToSymbolic() - ) - else -> operator(left, right) - } - } - } - - - override fun visit(expr: UtBoolOpExpression): UtExpression = - applySimplification(expr) { - val left = expr.left.expr.accept(this) - val right = expr.right.expr.accept(this) - val leftPrimitive = left.toPrimitiveValue(expr.left.type) - val rightPrimitive = right.toPrimitiveValue(expr.right.type) - when (expr.operator) { - Eq -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqConcrete - // Eq(Integral a, Integral b) ---> a == b - left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right, Long::equals) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqTrue - // Eq(a, true) ---> a - left is UtBoolExpression && right.isIntegralLiteral && right.toLong() == UtLongTrue -> left - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqFalse - // Eq(a, false) ---> not a - left is UtBoolExpression && right.isIntegralLiteral && right.toLong() == UtLongFalse -> - NotBoolExpression(left).accept(this) - // Eq(Op(_, Integral _), Integral _) - right.isIntegralLiteral && left is UtOpExpression && left.right.expr.isIntegralLiteral -> when (left.operator) { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqAddOpenLiteral - // Eq(Add(a, Integral b), Integral c) ---> Eq(a, Integral (c - b)) - Add -> Eq( - left.left, - evalIntegralLiterals( - right, - left.right.expr, - maxSort(expr.right, left.right), - Long::minus - ).toPrimitiveValue(maxType(expr.right.type, left.right.type)) - ) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqXorOpenLiteral - // Eq(Xor(a, Integral b), Integral c) ---> Eq(a, Integral (c `xor` b)) - Xor -> Eq( - left.left, - evalIntegralLiterals(right, left.right.expr, maxSort(expr.right, left.right), Long::xor) - .toPrimitiveValue(maxType(expr.right.type, left.right.type)) - ) - else -> Eq(leftPrimitive, rightPrimitive) - } - /** - * @see simplifyEq - */ - right.isIntegralLiteral -> simplifyEq(leftPrimitive, rightPrimitive) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqConcrete - // Eq(Fp a, Fp b) ---> a == b - left is UtFpLiteral && right is UtFpLiteral -> mkBool(left.value == right.value) - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqNaN - // Eq(a, Fp NaN) ---> False - left is UtFpLiteral && left.value.toDouble().isNaN() -> UtFalse - right is UtFpLiteral && right.value.toDouble().isNaN() -> UtFalse - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqEqual - // Eq(a a) && a.sort !is FPSort -> true - left == right && expr.left.type !is FloatType && expr.left.type !is DoubleType -> UtTrue - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqLiteralToRight - // Eq(a@(Concrete _), b) ---> Eq(b, a) - left.isConcrete -> Eq( - rightPrimitive, - leftPrimitive - ).accept(this) - else -> Eq(leftPrimitive, rightPrimitive) - } - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-neToNotEq - // Ne -> not Eq - Ne -> NotBoolExpression( - Eq( - leftPrimitive, - rightPrimitive - ) - ).accept(this) - Le -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-leConcrete - // Le(Integral a, Integral b) ---> a <= b - left.isIntegralLiteral && right.isIntegralLiteral -> { - evalIntegralLiterals(left, right) { a, b -> a <= b } - } - /** - * @see simplifyLess - * @see simplifyGreater - */ - right.isIntegralLiteral -> simplifyLess(leftPrimitive, rightPrimitive, true) - left.isIntegralLiteral -> simplifyGreater(rightPrimitive, leftPrimitive, true) - else -> Le(leftPrimitive, rightPrimitive) - } - Lt -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-ltConcrete - // Lt(Integral a, Integral b) ---> a < b - left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right) { a, b -> - a < b - } - /** - * @see simplifyLess - * @see simplifyGreater - */ - right.isIntegralLiteral -> simplifyLess(leftPrimitive, rightPrimitive, false) - left.isIntegralLiteral -> simplifyGreater(rightPrimitive, leftPrimitive, false) - else -> Lt(leftPrimitive, rightPrimitive) - } - Ge -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-geConcrete - // Ge(Integral a, Integral b) ---> a >= b - left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right) { a, b -> - a >= b - } - /** - * @see simplifyLess - * @see simplifyGreater - */ - right.isIntegralLiteral -> simplifyGreater(leftPrimitive, rightPrimitive, true) - left.isIntegralLiteral -> simplifyLess(rightPrimitive, leftPrimitive, true) - else -> Ge(leftPrimitive, rightPrimitive) - } - Gt -> when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-gtConcrete - // Gt(Integral a, Integral b) ---> a > b - left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right) { a, b -> - a > b - } - /** - * @see simplifyLess - * @see simplifyGreater - */ - right.isIntegralLiteral -> simplifyGreater(leftPrimitive, rightPrimitive, false) - left.isIntegralLiteral -> simplifyLess(rightPrimitive, leftPrimitive, false) - else -> Gt(leftPrimitive, rightPrimitive) - } - } - } - - private fun maxType(left: Type, right: Type): Type = when { - left is LongType -> left - right is LongType -> right - else -> IntType.v() - } - - override fun visit(expr: UtIsExpression): UtExpression = applySimplification(expr, false) { - UtIsExpression(expr.addr.accept(this) as UtAddrExpression, expr.typeStorage, expr.numberOfTypes) - } - - override fun visit(expr: UtGenericExpression): UtExpression = applySimplification(expr, false) { - UtGenericExpression(expr.addr.accept(this) as UtAddrExpression, expr.types, expr.numberOfTypes) - } - - override fun visit(expr: UtIsGenericTypeExpression): UtExpression = applySimplification(expr, false) { - UtIsGenericTypeExpression( - expr.addr.accept(this) as UtAddrExpression, - expr.baseAddr.accept(this) as UtAddrExpression, - expr.parameterTypeIndex - ) - } - - override fun visit(expr: UtEqGenericTypeParametersExpression): UtExpression = - applySimplification(expr, false) { - UtEqGenericTypeParametersExpression( - expr.firstAddr.accept(this) as UtAddrExpression, - expr.secondAddr.accept(this) as UtAddrExpression, - expr.indexMapping - ) - } - - override fun visit(expr: UtInstanceOfExpression): UtExpression = applySimplification(expr, false) { - val simplifiedHard = (expr.constraint.accept(this) as UtBoolExpression).asHardConstraint() - UtInstanceOfExpression(expr.symbolicStateUpdate.copy(hardConstraints = simplifiedHard)) - } - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-ite - // ite(true, then, _) ---> then - // ite(false, _, else) ---> else - override fun visit(expr: UtIteExpression): UtExpression = - applySimplification(expr) { - when (val condition = expr.condition.accept(this) as UtBoolExpression) { - UtTrue -> expr.thenExpr.accept(this) - UtFalse -> expr.elseExpr.accept(this) - else -> UtIteExpression(condition, expr.thenExpr.accept(this), expr.elseExpr.accept(this)) - } - } - - override fun visit(expr: UtMkTermArrayExpression): UtExpression = applySimplification(expr, false) - - override fun visit(expr: UtStringConst): UtExpression = expr - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-sConcat - // UtConcat("A_1", "A_2", ..., "A_n") ---> "A_1A_2...A_n" - override fun visit(expr: UtConcatExpression): UtExpression = - applySimplification(expr) { - val parts = expr.parts.map { it.accept(this) as UtStringExpression } - if (parts.all { it.isConcrete }) { - UtSeqLiteral(parts.joinToString { it.toConcrete() as String }) - } else { - UtConcatExpression(parts) - } - } - - override fun visit(expr: UtConvertToString): UtExpression = - applySimplification(expr) { UtConvertToString(expr.expression.accept(this)) } - - override fun visit(expr: UtStringToInt): UtExpression = - applySimplification(expr) { UtStringToInt(expr.expression.accept(this), expr.sort) } - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-sLength - // UtLength "A" ---> "A".length - override fun visit(expr: UtStringLength): UtExpression = - applySimplification(expr) { - val string = expr.string.accept(this) - when { - string is UtArrayToString -> string.length.expr - string.isConcrete -> mkInt((string.toConcrete() as String).length) - else -> UtStringLength(string) - } - } - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-sPositiveLength - // UtPositiveLength "A" ---> UtTrue - override fun visit(expr: UtStringPositiveLength): UtExpression = - applySimplification(expr) { - val string = expr.string.accept(this) - if (string.isConcrete) UtTrue else UtStringPositiveLength(string) - } - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-sCharAt - // UtCharAt "A" (Integral i) ---> "A".charAt(i) - override fun visit(expr: UtStringCharAt): UtExpression = - applySimplification(expr) { - val index = expr.index.accept(this) - val string = withNewSelect(index) { expr.string.accept(this) } - if (allConcrete(string, index)) { - (string.toConcrete() as String)[index.toConcrete() as Int].primitiveToSymbolic().expr - } else { - UtStringCharAt(string, index) - } - } - - override fun visit(expr: UtStringEq): UtExpression = - applySimplification(expr) { - val left = expr.left.accept(this) - val right = expr.right.accept(this) - when { - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqConcrete - // Eq("A", "B") ---> "A" == "B" - allConcrete(left, right) -> mkBool(left.toConcrete() == right.toConcrete()) - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqEqual - // Eq(a, a) ---> true - left == right -> UtTrue - else -> UtStringEq(left, right) - } - } - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-sSubstring - // UtSubstring "A" (Integral begin) (Integral end) ---> "A".substring(begin, end) - override fun visit(expr: UtSubstringExpression): UtExpression = - applySimplification(expr) { - val string = expr.string.accept(this) - val beginIndex = expr.beginIndex.accept(this) - val length = expr.length.accept(this) - if (allConcrete(string, beginIndex, length)) { - val begin = beginIndex.toConcrete() as Int - val end = begin + length.toConcrete() as Int - UtSeqLiteral((string.toConcrete() as String).substring(begin, end)) - } else { - UtSubstringExpression(string, beginIndex, length) - } - } - - override fun visit(expr: UtReplaceExpression): UtExpression = applySimplification(expr, false) - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-sStartsWith - // UtStartsWith "A" "P" ---> "A".startsWith("P") - override fun visit(expr: UtStartsWithExpression): UtExpression = - applySimplification(expr) { - val string = expr.string.accept(this) - val prefix = expr.prefix.accept(this) - if (allConcrete(string, prefix)) { - val concreteString = string.toConcrete() as String - val concretePrefix = prefix.toConcrete() as String - mkBool(concreteString.startsWith(concretePrefix)) - } else { - UtStartsWithExpression(string, prefix) - } - } - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-sEndsWith - // UtEndsWith "A" "S" ---> "A".endsWith("S") - override fun visit(expr: UtEndsWithExpression): UtExpression = - applySimplification(expr) { - val string = expr.string.accept(this) - val suffix = expr.suffix.accept(this) - if (allConcrete(string, suffix)) { - val concreteString = string.toConcrete() as String - val concreteSuffix = suffix.toConcrete() as String - mkBool(concreteString.endsWith(concreteSuffix)) - } else { - UtEndsWithExpression(string, suffix) - } - } - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-sIndexOf - // UtIndexOf "A" "S" ---> "A".indexOf("S") - override fun visit(expr: UtIndexOfExpression): UtExpression = applySimplification(expr) { - val string = expr.string.accept(this) - val substring = expr.substring.accept(this) - if (allConcrete(string, substring)) { - val concreteString = string.toConcrete() as String - val concreteSubstring = substring.toConcrete() as String - mkInt(concreteString.indexOf(concreteSubstring)) - } else { - UtIndexOfExpression(string, substring) - } - } - - // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-sContains - // UtContains "A" "S" ---> "A".contains("S") - override fun visit(expr: UtContainsExpression): UtExpression = - applySimplification(expr) { - val string = expr.string.accept(this) - val substring = expr.substring.accept(this) - if (allConcrete(string, substring)) { - val concreteString = string.toConcrete() as String - val concreteSubstring = substring.toConcrete() as String - mkBool(concreteString.contains(concreteSubstring)) - } else { - UtContainsExpression(string, substring) - } - } - - override fun visit(expr: UtToStringExpression): UtExpression = - applySimplification(expr) { - UtToStringExpression(expr.isNull.accept(this) as UtBoolExpression, expr.notNullExpr.accept(this)) - } - - override fun visit(expr: UtArrayToString): UtExpression = - applySimplification(expr) { - UtArrayToString( - expr.arrayExpression.accept(this), - expr.offset.expr.accept(this).toIntValue(), - expr.length.expr.accept(this).toIntValue() - ) - } - - override fun visit(expr: UtSeqLiteral): UtExpression = expr - - override fun visit(expr: UtConstArrayExpression): UtExpression = - applySimplification(expr) { - UtConstArrayExpression(expr.constValue.accept(this), expr.sort) - } - - override fun visit(expr: UtArrayInsert): UtExpression = applySimplification(expr, false) { - UtArrayInsert( - expr.arrayExpression.accept(this), - expr.index.expr.accept(this).toIntValue(), - expr.element.accept(this) - ) - } - - override fun visit(expr: UtArrayInsertRange): UtExpression = applySimplification(expr, false) { - UtArrayInsertRange( - expr.arrayExpression.accept(this), - expr.index.expr.accept(this).toIntValue(), - expr.elements.accept(this), - expr.from.expr.accept(this).toIntValue(), - expr.length.expr.accept(this).toIntValue() - ) - } - - override fun visit(expr: UtArrayRemove): UtExpression = applySimplification(expr, false) { - UtArrayRemove( - expr.arrayExpression.accept(this), - expr.index.expr.accept(this).toIntValue() - ) - } - - - override fun visit(expr: UtArrayRemoveRange): UtExpression = applySimplification(expr, false) { - UtArrayRemoveRange( - expr.arrayExpression.accept(this), - expr.index.expr.accept(this).toIntValue(), - expr.length.expr.accept(this).toIntValue() - ) - - } - - override fun visit(expr: UtArraySetRange): UtExpression = applySimplification(expr, false) { - UtArraySetRange( - expr.arrayExpression.accept(this), - expr.index.expr.accept(this).toIntValue(), - expr.elements.accept(this), - expr.from.expr.accept(this).toIntValue(), - expr.length.expr.accept(this).toIntValue() - ) - } - - override fun visit(expr: UtArrayShiftIndexes): UtExpression = applySimplification(expr, false) { - UtArrayShiftIndexes( - expr.arrayExpression.accept(this), - expr.offset.expr.accept(this).toIntValue() - ) - } - - override fun visit(expr: UtArrayApplyForAll): UtExpression = applySimplification(expr, false) { - UtArrayApplyForAll( - expr.arrayExpression.accept(this), - expr.constraint - ) - } - - override fun visit(expr: UtStringToArray): UtExpression = applySimplification(expr, false) { - UtStringToArray( - expr.stringExpression.accept(this), - expr.offset.expr.accept(this).toIntValue() - ) - } -} - - -private val arrayExpressionAxiomInstantiationCache = - IdentityHashMap() - -private val stringExpressionAxiomInstantiationCache = - IdentityHashMap() - -private val arrayExpressionAxiomIndex = AtomicInteger(0) - -private fun instantiateArrayAsNewConst(arrayExpression: UtExtendedArrayExpression) = - arrayExpressionAxiomInstantiationCache.getOrPut(arrayExpression) { - val suffix = when (arrayExpression) { - is UtArrayInsert -> "Insert" - is UtArrayInsertRange -> "InsertRange" - is UtArrayRemove -> "Remove" - is UtArrayRemoveRange -> "RemoveRange" - is UtArraySetRange -> "SetRange" - is UtArrayShiftIndexes -> "ShiftIndexes" - is UtStringToArray -> "StringToArray" - is UtArrayApplyForAll -> error("UtArrayApplyForAll cannot be instantiated as new const array") - } - UtMkArrayExpression( - "_array$suffix${arrayExpressionAxiomIndex.getAndIncrement()}", - arrayExpression.sort - ) - } - -private fun instantiateStringAsNewConst(stringExpression: UtStringExpression) = - stringExpressionAxiomInstantiationCache.getOrPut(stringExpression) { - val suffix = when (stringExpression) { - is UtArrayToString -> "ArrayToString" - else -> unreachableBranch("Cannot instantiate new string const for $stringExpression") - } - UtStringConst("_str$suffix${arrayExpressionAxiomIndex.getAndIncrement()}") - } - -/** - * Visitor that applies the same simplifications as [RewritingVisitor] and instantiate axioms for extended array theory. - * - * @see UtExtendedArrayExpression - */ -class AxiomInstantiationRewritingVisitor( - eqs: Map = emptyMap(), - lts: Map = emptyMap(), - gts: Map = emptyMap() -) : RewritingVisitor(eqs, lts, gts) { - private val instantiatedAxiomExpressions = mutableListOf() - - /** - * Select(UtArrayInsert(a, v, i), j) is equivalent to ITE(i = j, v, Select(a, ITE(j < i, j, j - 1)) - */ - override fun visit(expr: UtArrayInsert): UtExpression { - val arrayInstance = instantiateArrayAsNewConst(expr) - val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) - val element = expr.element.accept(this) - val selectedIndex = selectIndexStack.last() - val pushedIndex = UtIteExpression( - Lt(selectedIndex, index), - selectedIndex.expr, - Add(selectedIndex, (-1).toPrimitiveValue()) - ) - val arrayExpression = withNewSelect(pushedIndex) { expr.arrayExpression.accept(this) } - - instantiatedAxiomExpressions += UtEqExpression( - UtIteExpression( - Eq(selectedIndex, index), - element, - arrayExpression.select(pushedIndex), - ).accept(this), arrayInstance.select(selectedIndex.expr) - ) - return arrayInstance - } - - /** - * Select(UtArrayInsertRange(a, i, b, from, length), j) is equivalent to - * ITE(j >= i && j < i + length, Select(b, j - i + from), Select(a, ITE(j < i, j, j - length) - */ - override fun visit(expr: UtArrayInsertRange): UtExpression { - val arrayInstance = instantiateArrayAsNewConst(expr) - val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) - val from = expr.from.expr.accept(this).toPrimitiveValue(expr.index.type) - val length = expr.length.expr.accept(this).toPrimitiveValue(expr.length.type) - val selectedIndex = selectIndexStack.last() - val pushedArrayInstanceIndex = - UtIteExpression(Lt(selectedIndex, index), selectedIndex.expr, Sub(selectedIndex, length)) - val arrayExpression = withNewSelect(pushedArrayInstanceIndex) { expr.arrayExpression.accept(this) } - val pushedElementsIndex = Add(Sub(selectedIndex, index).toIntValue(), from) - val elements = withNewSelect(pushedElementsIndex) { expr.elements.accept(this) } - instantiatedAxiomExpressions += UtEqExpression( - UtIteExpression( - mkAnd(Ge(selectedIndex, index), Lt(selectedIndex, Add(index, length).toIntValue())), - elements.select(pushedElementsIndex), - arrayExpression.select(pushedArrayInstanceIndex), - ).accept(this), arrayInstance.select(selectedIndex.expr) - ) - return arrayInstance - } - - /** - * Select(UtArrayRemove(a, i), j) is equivalent to Select(a, ITE(j < i, j, j + 1)) - */ - override fun visit(expr: UtArrayRemove): UtExpression { - val arrayInstance = instantiateArrayAsNewConst(expr) - val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) - val selectedIndex = selectIndexStack.last() - val pushedIndex = UtIteExpression( - Lt(selectedIndex, index), - selectedIndex.expr, - Add(selectedIndex, 1.toPrimitiveValue()) - ) - val arrayExpression = withNewSelect(pushedIndex) { expr.arrayExpression.accept(this) } - - instantiatedAxiomExpressions += UtEqExpression( - arrayExpression.select(pushedIndex), - arrayInstance.select(selectedIndex.expr) - ) - return arrayInstance - } - - /** - * Select(UtArrayRemoveRange(a, i, length), j) is equivalent to Select(a, ITE(j < i, j, j + length)) - */ - override fun visit(expr: UtArrayRemoveRange): UtExpression { - val arrayInstance = instantiateArrayAsNewConst(expr) - val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) - val length = expr.length.expr.accept(this).toPrimitiveValue(expr.length.type) - val selectedIndex = selectIndexStack.last() - val pushedIndex = UtIteExpression( - Lt(selectedIndex, index), - selectedIndex.expr, - Add(selectedIndex, length) - ) - val arrayExpression = withNewSelect(pushedIndex) { expr.arrayExpression.accept(this) } - - instantiatedAxiomExpressions += UtEqExpression( - arrayExpression.select(pushedIndex), - arrayInstance.select(selectedIndex.expr) - ) - return arrayInstance - } - - /** - * Select(UtSetRange(a, i, b, from, length), j) is equivalent to - * ITE(j >= i && j < i + length, Select(b, j - i + from), Select(a, i)) - */ - override fun visit(expr: UtArraySetRange): UtExpression { - val arrayInstance = instantiateArrayAsNewConst(expr) - val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) - val from = expr.from.expr.accept(this).toPrimitiveValue(expr.index.type) - val length = expr.length.expr.accept(this).toPrimitiveValue(expr.length.type) - val selectedIndex = selectIndexStack.last() - val arrayExpression = expr.arrayExpression.accept(this) - val pushedIndex = Add(Sub(selectedIndex, index).toIntValue(), from) - val elements = withNewSelect(pushedIndex) { expr.elements.accept(this) } - instantiatedAxiomExpressions += UtEqExpression( - UtIteExpression( - mkAnd(Ge(selectedIndex, index), Lt(selectedIndex, Add(index, length).toIntValue())), - elements.select(pushedIndex), - arrayExpression.select(selectedIndex.expr) - ).accept(this), arrayInstance.select(selectedIndex.expr) - ) - return arrayInstance - } - - /** - * Select(UtShiftIndexes(a, offset), j) is equivalent to Select(a, j - offset) - */ - override fun visit(expr: UtArrayShiftIndexes): UtExpression { - val arrayInstance = instantiateArrayAsNewConst(expr) - val offset = expr.offset.expr.accept(this).toIntValue() - val selectedIndex = selectIndexStack.last() - val pushedIndex = Sub(selectedIndex, offset) - val arrayExpression = withNewSelect(pushedIndex) { expr.arrayExpression.accept(this) } - - instantiatedAxiomExpressions += UtEqExpression( - arrayExpression.select(pushedIndex), - arrayInstance.select(selectedIndex.expr) - ) - return arrayInstance - } - - /** - * instantiate expr.constraint on selecting from UtArrayApplyForAll - */ - override fun visit(expr: UtArrayApplyForAll): UtExpression { - val selectedIndex = selectIndexStack.last() - val arrayExpression = expr.arrayExpression.accept(this) - val constraint = expr.constraint(arrayExpression, selectedIndex) - instantiatedAxiomExpressions += constraint - return arrayExpression - } - - override fun visit(expr: UtStringToArray): UtExpression { - val arrayInstance = instantiateArrayAsNewConst(expr) - val offset = expr.offset.expr.accept(this).toIntValue() - val selectedIndex = selectIndexStack.last() - val pushedIndex = Add(selectedIndex, offset) - val stringExpression = withNewSelect(pushedIndex) { expr.stringExpression.accept(this) } - - instantiatedAxiomExpressions += UtEqExpression( - UtStringCharAt(stringExpression, pushedIndex), - arrayInstance.select(selectedIndex.expr) - ) - return arrayInstance - } - - override fun visit(expr: UtArrayToString): UtExpression { - val stringInstance = instantiateStringAsNewConst(expr) - val offset = expr.offset.expr.accept(this).toIntValue() - val selectedIndex = selectIndexStack.last() - val pushedIndex = Add(selectedIndex, offset) - val arrayExpression = withNewSelect(pushedIndex) { expr.arrayExpression.accept(this) } - - instantiatedAxiomExpressions += UtEqExpression( - arrayExpression.select(pushedIndex), - UtStringCharAt(stringInstance, selectedIndex.expr) - ) - return stringInstance - } - - val instantiatedArrayAxioms: List - get() = instantiatedAxiomExpressions -} - -private const val UtLongTrue = 1L -private const val UtLongFalse = 0L diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Simplificator.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Simplificator.kt new file mode 100644 index 0000000000..08845205aa --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Simplificator.kt @@ -0,0 +1,1205 @@ +package org.utbot.engine.pc + +import org.utbot.engine.Add +import org.utbot.engine.And +import org.utbot.engine.Cmp +import org.utbot.engine.Cmpg +import org.utbot.engine.Cmpl +import org.utbot.engine.Div +import org.utbot.engine.Eq +import org.utbot.engine.Ge +import org.utbot.engine.Gt +import org.utbot.engine.Le +import org.utbot.engine.Lt +import org.utbot.engine.Mul +import org.utbot.engine.Ne +import org.utbot.engine.Or +import org.utbot.engine.PrimitiveValue +import org.utbot.engine.Rem +import org.utbot.engine.Shl +import org.utbot.engine.Shr +import org.utbot.engine.Sub +import org.utbot.engine.Ushr +import org.utbot.engine.Xor +import org.utbot.engine.maxSort +import org.utbot.engine.primitiveToLiteral +import org.utbot.engine.primitiveToSymbolic +import org.utbot.engine.symbolic.asHardConstraint +import org.utbot.engine.toIntValue +import org.utbot.engine.toPrimitiveValue +import org.utbot.framework.UtSettings.useExpressionSimplification +import java.util.IdentityHashMap +import java.util.concurrent.atomic.AtomicInteger +import kotlinx.collections.immutable.toPersistentList +import soot.DoubleType +import soot.FloatType +import soot.IntType +import soot.IntegerType +import soot.LongType +import soot.Type + + +/** + * UtExpressionVisitor that performs simple rewritings on expressions with concrete values. + * + */ +open class Simplificator( + private val eqs: Map = emptyMap(), + private val lts: Map = emptyMap(), + private val gts: Map = emptyMap() +) : UtExpressionVisitor { + val axiomInstantiationVisitor: Simplificator + get() = AxiomInstantiationSimplificator(eqs, lts, gts) + protected val selectIndexStack = mutableListOf() + private val simplificationCache = IdentityHashMap() + + protected inline fun withNewSelect(index: UtExpression, block: () -> R): R { + selectIndexStack += index.toIntValue() + return block().also { + selectIndexStack.removeLast() + } + } + + @Suppress("MemberVisibilityCanBePrivate") + protected inline fun withEmptySelects(block: () -> R): R { + val currentSelectStack = selectIndexStack.toList() + selectIndexStack.clear() + return block().also { + selectIndexStack.addAll(currentSelectStack) + } + } + + @Suppress("MemberVisibilityCanBePrivate") + protected inline fun withRemoveSelect(block: () -> R): R { + val lastIndex = selectIndexStack.removeLastOrNull() + return block().also { + if (lastIndex != null) { + selectIndexStack += lastIndex + } + } + } + + /** + * if [useExpressionSimplification] is true then rewrite [expr] and + * substitute concrete values from [eqs] map for equal symbolic expressions. + * + * @param expr - simplified expression + * @param block - simplification function. Identity function by default + * @param substituteResult - if true then try to substitute result of block(expr) + * with equal concrete value from [eqs] map. + */ + private inline fun applySimplification( + expr: UtExpression, + substituteResult: Boolean = true, + crossinline block: (() -> UtExpression) = { expr }, + ): UtExpression { + val value = { + if (!useExpressionSimplification) { + expr + } else { + eqs[expr] ?: block().let { if (substituteResult) eqs[it] ?: it else it } + } + } + return simplificationCache.getOrPut(expr) { + checkSortsEquality(value, expr) + } + } + + private fun checkSortsEquality(value: () -> UtExpression, sourceExpr: UtExpression): UtExpression { + val result = value() + + val sourceSort = sourceExpr.sort + val resultSort = result.sort + + // We should return simplified expression with the same sort as the original one. + // The only exception is a case with two primitive sorts and cast expression below. + // So, expression with, e.g., UtArraySort must be returned here. + if (resultSort == sourceSort) return result + + // There we must have the following situation: original expression is `x = Add(a, b)`, where + // x :: Int32, a = 0 :: Int32, b :: Short16. Simplifier removes the left part of the addition since it is + // equal to zero, now we have just b with a different from the original expression sort. + // We must cast it to the required sort, so the result will be `Cast(b, Int32)`. + require(sourceSort is UtPrimitiveSort && resultSort is UtPrimitiveSort) { + val messageParts = listOf( + "Expr to simplify had sort ${sourceSort}:", sourceExpr, "", + "After simplification expression got sort ${resultSort}:", result + ) + messageParts.joinToString(System.lineSeparator(), prefix = System.lineSeparator()) + } + + val expectedType = sourceSort.type + val resultType = resultSort.type + + return UtCastExpression(result.toPrimitiveValue(resultType), expectedType) + } + + override fun visit(expr: UtArraySelectExpression): UtExpression = + applySimplification(expr) { + val index = withEmptySelects { expr.index.accept(this) } + when (val array = withNewSelect(index) { expr.arrayExpression.accept(this) }) { + // select(constArray(a), i) --> a + is UtConstArrayExpression -> array.constValue + is UtArrayMultiStoreExpression -> { + // select(store(..(store(array, Concrete a, b), Concrete x, _)..), Concrete a) -> b + val newStores = array.stores.filter { UtEqExpression(index, it.index).accept(this) != UtFalse } + + when { + newStores.isEmpty() -> array.initial.select(index).accept(this) + UtEqExpression(index, newStores.last().index).accept(this) == UtTrue -> newStores.last().value + else -> UtArrayMultiStoreExpression(array.initial, newStores.toPersistentList()).select(index) + } + } + else -> array.select(index) + } + } + + override fun visit(expr: UtMkArrayExpression): UtExpression = expr + + // store(store(store(array, 1, b), 2, a), 1, a) ---> store(store(array, 2, a), 1, a) + override fun visit(expr: UtArrayMultiStoreExpression): UtExpression = + applySimplification(expr) { + val initial = expr.initial.accept(this) + val stores = withRemoveSelect { + expr.stores.map { + val index = it.index.accept(this) + val value = it.value.accept(this) + + require(value.sort == expr.sort.itemSort) { + "Unequal sorts occurred during rewriting UtArrayMultiStoreExpression:\n" + + "value with index $index has sort ${value.sort} while " + + "${expr.sort.itemSort} was expected." + } + + UtStore(index, value) + } + }.filterNot { it.value == initial.select(it.index) } + when { + stores.isEmpty() -> initial + initial is UtArrayMultiStoreExpression -> UtArrayMultiStoreExpression( + initial.initial, + (initial.stores + stores).asReversed().distinctBy { it.index }.asReversed().toPersistentList() + ) + else -> UtArrayMultiStoreExpression( + initial, + stores.asReversed().distinctBy { it.index }.asReversed().toPersistentList() + ) + } + } + + override fun visit(expr: UtBvLiteral): UtExpression = expr + + override fun visit(expr: UtBvConst): UtExpression = applySimplification(expr, true) + + override fun visit(expr: UtAddrExpression): UtExpression = + applySimplification(expr) { + UtAddrExpression( + expr.internal.accept(this) + ) + } + + override fun visit(expr: UtFpLiteral): UtExpression = expr + + override fun visit(expr: UtFpConst): UtExpression = applySimplification(expr) + + private fun evalIntegralLiterals(literal: UtExpression, sort: UtSort, block: (Long) -> Number): UtExpression = + block(literal.toLong()).toValueWithSort(sort).primitiveToLiteral() + + private fun evalIntegralLiterals( + left: UtExpression, + right: UtExpression, + sort: UtSort, + block: (Long, Long) -> Number + ): UtExpression = + block(left.toLong(), right.toLong()).toValueWithSort(sort).primitiveToLiteral() + + private fun evalIntegralLiterals( + left: UtExpression, + right: UtExpression, + block: (Long, Long) -> Boolean + ): UtExpression = + block(left.toLong(), right.toLong()).primitiveToLiteral() + + private fun Number.toValueWithSort(sort: UtSort): Any = + when (sort) { + UtBoolSort -> this != UtLongFalse + UtByteSort -> this.toByte() + UtShortSort -> this.toShort() + UtCharSort -> this.toChar() + UtIntSort -> this.toInt() + UtLongSort -> this.toLong() + UtFp32Sort -> this.toFloat() + UtFp64Sort -> this.toDouble() + else -> error("Can convert Number values only to primitive sorts. Sort $sort isn't primitive") + } + + private fun UtBoolLiteral.toLong(): Long = if (value) UtLongTrue else UtLongFalse + + private fun UtBvLiteral.toLong(): Long = value.toLong() + + private fun UtExpression.toLong(): Long = when (this) { + is UtBvLiteral -> toLong() + is UtBoolLiteral -> toLong() + is UtAddrExpression -> internal.toLong() + else -> error("$this isn't IntegralLiteral") + } + + private val UtExpression.isIntegralLiteral: Boolean + get() = when (this) { + is UtBvLiteral -> true + is UtBoolLiteral -> true + is UtAddrExpression -> internal.isIntegralLiteral + else -> false + } + + override fun visit(expr: UtOpExpression): UtExpression = + applySimplification(expr) { + val left = expr.left.expr.accept(this) + val right = expr.right.expr.accept(this) + val leftPrimitive = left.toPrimitiveValue(expr.left.type) + val rightPrimitive = right.toPrimitiveValue(expr.right.type) + + when (expr.operator) { + // Sort of pattern matching... + Add -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-addConcrete + // Add(Integral a, Integral b) ---> Integral (a + b) + left.isIntegralLiteral && right.isIntegralLiteral -> + evalIntegralLiterals(left, right, expr.resultSort, Long::plus) + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-addA0 + // Add(a, Integral 0) ---> a + right.isIntegralLiteral && right.toLong() == 0L -> left + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-addLiteralToRight + // Add(Concrete a, b) ---> Add(b, Concrete a).accept(this) + left.isIntegralLiteral -> Add( + rightPrimitive, + leftPrimitive + ).accept(this) + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-addAddOpenLiteral + // Add(Add(x, Integral a), Integral b)) -> Add(x, Integral (a + b)) + right.isIntegralLiteral && left is UtOpExpression && left.operator is Add && left.right.expr.isIntegralLiteral -> + Add( + left.left, + evalIntegralLiterals(left.right.expr, right, expr.right.expr.sort, Long::plus) + .toPrimitiveValue(expr.right.type) + ) + else -> Add(leftPrimitive, rightPrimitive) + } + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-subToAdd + // Sub(a, b) ---> Add(a, Neg b) + Sub -> Add( + leftPrimitive, + UtNegExpression(rightPrimitive).toPrimitiveValue(expr.right.type) + ).accept(this) + Mul -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulConcrete + // Mul(Integral a, Integral b) ---> Integral (a * b) + left.isIntegralLiteral && right.isIntegralLiteral -> + evalIntegralLiterals(left, right, expr.resultSort, Long::times) + right.isIntegralLiteral -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulA0 + // Mul(a, b@(Integral 0)) ---> b + right.toLong() == 0L -> right + + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulA1 + // Mul(a, Integral 1) ---> a + right.toLong() == 1L -> left + + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulAMinus1 + // Mul(a, Integral -1) ---> Neg a + right.toLong() == -1L -> UtNegExpression(leftPrimitive) + + // Mul(Op(_, Integral _), Integral _) + left is UtOpExpression && left.right.expr.isIntegralLiteral -> when (left.operator) { + + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulDistr + // Mul(Add(x, Integral a), Integral b) ---> Add(Mul(x, b), Integral (a * b)) + is Add -> Add( + Mul( + left.left, + rightPrimitive + ).toPrimitiveValue(expr.left.type), + evalIntegralLiterals(left.right.expr, right, expr.resultSort, Long::times) + .toPrimitiveValue(expr.right.type) + ) + + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulMulOpenLiteral + // Mul(Mul(x, Integral a), Integral b) ---> Mul(x, Integral (a * b)) + is Mul -> Mul( + left.left, + evalIntegralLiterals(left.right.expr, right, expr.right.expr.sort, Long::times) + .toPrimitiveValue(expr.right.type) + ) + else -> Mul(leftPrimitive, rightPrimitive) + } + else -> Mul(leftPrimitive, rightPrimitive) + } + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-mulLiteralToRight + // Mul(a@(Integral _), b) ---> Mul(b, a) + left.isIntegralLiteral -> Mul( + rightPrimitive, + leftPrimitive + ).accept(this) + else -> Mul(leftPrimitive, rightPrimitive) + } + Div -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-divConcrete + // Div(Integral a, Integral b) ---> Integral (a / b) + left.isIntegralLiteral && right.isIntegralLiteral && right.toLong() != 0L -> + evalIntegralLiterals(left, right, expr.resultSort, Long::div) + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-divA1 + // Div(a, Concrete 1) ---> a + right.isIntegralLiteral && right.toLong() == 1L -> left + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-divAMinus1 + // Div(a, Concrete -1) ---> Neg a + right.isIntegralLiteral && right.toLong() == -1L -> UtNegExpression(leftPrimitive) + else -> Div(leftPrimitive, rightPrimitive) + } + And -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andConcrete + // And(Integral a, Integral b) ---> Integral (a `and` b) + left.isIntegralLiteral && right.isIntegralLiteral -> + evalIntegralLiterals(left, right, expr.resultSort, Long::and) + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andA0 + // And(a@(Integral 0), b) ---> a + left.isIntegralLiteral && left.toLong() == 0L -> + evalIntegralLiterals(left, expr.resultSort) { it } + right.isIntegralLiteral && right.toLong() == 0L -> + evalIntegralLiterals(right, expr.resultSort) { it } + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andAtoRight + left.isIntegralLiteral -> And( + rightPrimitive, + leftPrimitive + ) + else -> And(leftPrimitive, rightPrimitive) + } + Cmp, Cmpg, Cmpl -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-cmpConcrete + // Cmp(Integral a, Integral b) ---> Integral (cmp a b) + left.isIntegralLiteral && right.isIntegralLiteral -> + evalIntegralLiterals(left, right, expr.resultSort) { a, b -> + when { + a < b -> -1 + a == b -> 0 + else -> 1 + } + } + else -> expr.operator(leftPrimitive, rightPrimitive) + } + Or -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-orConcrete + // Or(Integral a, Integral b) ---> Integral (a `or` b) + left.isIntegralLiteral && right.isIntegralLiteral -> + evalIntegralLiterals(left, right, expr.resultSort, Long::or) + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-orA0 + // Or(a, b@(Integral 0)) ---> a + left.isIntegralLiteral && left.toLong() == 0L -> right + right.isIntegralLiteral && right.toLong() == 0L -> left + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-orLiteralToRight + // Or(a@(Integral _),b) ---> Or(b, a) + left.isIntegralLiteral -> Or( + rightPrimitive, + leftPrimitive + ) + else -> Or(leftPrimitive, rightPrimitive) + } + Shl -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-shlConcrete + // Shl(Integral a, Integral b) ---> Integral (a `shl` b) + left.isIntegralLiteral && right.isIntegralLiteral -> + evalIntegralLiterals(left, right, expr.resultSort) { a, b -> + val size: Int = (expr.sort as UtBvSort).size + a shl (b.toInt() % size) + } + + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-shlCycle + // Shl(a, Integral b) && b % a.bit_size == 0 ---> a + right.isIntegralLiteral && right.toLong() % (expr.resultSort as UtBvSort).size == 0L -> left + else -> Shl(leftPrimitive, rightPrimitive) + } + Shr -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-shrConcrete + // Shr(Integral a, Integral b) ---> Integral (a `shr` b) + left.isIntegralLiteral && right.isIntegralLiteral -> + evalIntegralLiterals(left, right, expr.resultSort) { a, b -> + val size: Int = (expr.sort as UtBvSort).size + a shr (b.toInt() % size) + } + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-shrCycle + // Shr(a, Integral b) && b % a.bit_size == 0 ---> a + right.isIntegralLiteral && right.toLong() % (expr.resultSort as UtBvSort).size == 0L -> left + else -> Shr(leftPrimitive, rightPrimitive) + } + Ushr -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-ushrConcrete + // Ushr(Integral a, Integral b) ---> Integral (a `ushr` b) + left.isIntegralLiteral && right.isIntegralLiteral -> + evalIntegralLiterals(left, right, expr.resultSort) { a, b -> + val size: Int = (expr.sort as UtBvSort).size + if (a >= 0) { + a ushr (b.toInt() % size) + } else { + // 0b0..01..1 where mask.countOneBits() = size + val mask: Long = ((1L shl size) - 1L) + (a and mask) ushr (b.toInt() % size) + } + } + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-ushrCycle + // Ushr(a, Integral b) && b % a.bit_size == 0 ---> a + right.isIntegralLiteral && right.toLong() % (expr.resultSort as UtBvSort).size == 0L -> left + else -> Ushr(leftPrimitive, rightPrimitive) + } + Xor -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-xorConcrete + // Xor(Integral a, Integral b) ---> Integral (a `xor` b) + left.isIntegralLiteral && right.isIntegralLiteral -> + evalIntegralLiterals(left, right, expr.resultSort, Long::xor) + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-xorA0 + // Xor(a, Integral 0) ---> a + right.isIntegralLiteral && right.toLong() == 0L -> left + left.isIntegralLiteral && left.toLong() == 0L -> right + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-xorEqual + // Xor(a, a) ---> Integral 0 + left == right -> (0L).toValueWithSort(expr.resultSort).primitiveToSymbolic().expr + else -> Xor(leftPrimitive, rightPrimitive) + } + Rem -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-remConcrete + // Rem(Integral a, Integral b) ---> Integral (a % b) + left.isIntegralLiteral && right.isIntegralLiteral && right.toLong() != 0L -> + evalIntegralLiterals(left, right, expr.resultSort, Long::rem) + else -> Rem(leftPrimitive, rightPrimitive) + } + } + } + + + override fun visit(expr: UtTrue): UtExpression = expr + + override fun visit(expr: UtFalse): UtExpression = expr + + override fun visit(expr: UtEqExpression): UtExpression = + applySimplification(expr) { + val left = expr.left.accept(this) + val right = expr.right.accept(this) + when { + // Eq(Integral a, Integral b) ---> a == b + left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right, Long::equals) + // Eq(Fp a, Fp b) ---> a == b + left is UtFpLiteral && right is UtFpLiteral -> mkBool(left.value == right.value) + // Eq(Expr(sort = Boolean), Integral _) + left is UtIteExpression -> { + val thenEq = UtEqExpression(left.thenExpr, right).accept(this) + val elseEq = UtEqExpression(left.elseExpr, right).accept(this) + if (thenEq is UtFalse && elseEq is UtFalse) { + UtFalse + } else if (thenEq is UtTrue && elseEq is UtFalse) { + left.condition + } else if (thenEq is UtFalse && elseEq is UtTrue) { + mkNot(left.condition) + } else { + UtEqExpression(left, right) + } + } + right is UtIteExpression -> UtEqExpression(right, left).accept(this) + left is UtBoolExpression && right.isIntegralLiteral -> when { + // Eq(a, true) ---> a + right.toLong() == UtLongTrue -> left + // Eq(a, false) ---> not a + right.toLong() == UtLongFalse -> NotBoolExpression(left).accept(this) + else -> UtEqExpression(left, right) + } + right.isIntegralLiteral -> simplifyEq(left, right) + // Eq(a, Fp NaN) ---> False + right is UtFpLiteral && right.value.toDouble().isNaN() -> UtFalse + // Eq(a@(Concrete _), b) ---> Eq(b, a) + left.isConcrete && !right.isConcrete -> UtEqExpression(right, left).accept(this) + // Eq(a a) && a.sort !is FPSort -> true + left == right && left.sort !is UtFp32Sort && left.sort !is UtFp64Sort -> UtTrue + else -> UtEqExpression(left, right) + } + } + + override fun visit(expr: UtBoolConst): UtExpression = applySimplification(expr, false) + + private fun negate(expr: UtBoolLiteral): UtBoolLiteral = mkBool(expr == UtFalse) + + override fun visit(expr: NotBoolExpression): UtExpression = + applySimplification(expr) { + when (val exp = expr.expr.accept(this) as UtBoolExpression) { + // not true ---> false + // not false ---> true + is UtBoolLiteral -> negate(exp) + is UtBoolOpExpression -> { + val left = exp.left + val right = exp.right + if (left.type is IntegerType && right.type is IntegerType) { + when (exp.operator) { + // not Ne(a, b) ---> Eq(a, b) + Ne -> Eq(left, right) + // not Ge(a, b) ---> Lt(a, b) + Ge -> Lt(left, right) + // not Gt(a, b) ---> Le(a, b) + Gt -> Le(left, right) + // not Lt(a, b) ---> Ge(a, b) + Lt -> Ge(left, right) + // not Le(a, b) ---> Gt(a, b) + Le -> Gt(left, right) + Eq -> NotBoolExpression(exp) + } + } else { + when (exp.operator) { + // simplification not Ne(a, b) ---> Eq(a, b) is always valid + Ne -> Eq(left, right) + else -> NotBoolExpression(exp) + } + } + } + // not not expr -> expr + is NotBoolExpression -> exp.expr + // not (and a_1, a_2, ..., a_n) ---> or (not a_1), (not a_2), ..., (not a_n) + is UtAndBoolExpression -> UtOrBoolExpression(exp.exprs.map { NotBoolExpression(it).accept(this) as UtBoolExpression }) + // not (or a_1, a_2, ..., a_n) ---> and (not a_1), (not a_2), ..., (not a_n) + is UtOrBoolExpression -> UtAndBoolExpression(exp.exprs.map { NotBoolExpression(it).accept(this) as UtBoolExpression }) + else -> NotBoolExpression(exp) + } + } + + override fun visit(expr: UtOrBoolExpression): UtExpression = + applySimplification(expr) { + val exprs = expr.exprs.map { it.accept(this) as UtBoolExpression }.filterNot { it == UtFalse } + .flatMap { splitOr(it) } + when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrEmpty + exprs.isEmpty() -> UtFalse + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrValue + exprs.any { it == UtTrue } -> UtTrue + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andorSingle + exprs.size == 1 -> exprs.single() + else -> UtOrBoolExpression(exprs) + } + } + + override fun visit(expr: UtAndBoolExpression): UtExpression = + applySimplification(expr) { + val exprs = expr.exprs.map { it.accept(this) as UtBoolExpression }.filterNot { it == UtTrue } + .flatMap { splitAnd(it) } + when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrEmpty + exprs.isEmpty() -> UtTrue + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrValue + exprs.any { it == UtFalse } -> UtFalse + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-andOrSingle + exprs.size == 1 -> exprs.single() + else -> UtAndBoolExpression(exprs) + } + } + + override fun visit(expr: UtAddNoOverflowExpression): UtExpression = + UtAddNoOverflowExpression( + applySimplification(expr.left), + applySimplification(expr.right) + ) + + override fun visit(expr: UtSubNoOverflowExpression): UtExpression = + UtSubNoOverflowExpression( + applySimplification(expr.left), + applySimplification(expr.right) + ) + + override fun visit(expr: UtMulNoOverflowExpression): UtExpression = + UtMulNoOverflowExpression( + applySimplification(expr.left), + applySimplification(expr.right) + ) + + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-negConcrete + // Neg (Concrete a) ---> Concrete (-a) + override fun visit(expr: UtNegExpression): UtExpression = + applySimplification(expr) { + val variable = expr.variable.expr.accept(this) + if (variable.isConcrete) { + val value = variable.toConcrete() + when (variable.sort) { + UtByteSort -> mkInt(-(value as Byte)) + UtShortSort -> mkInt(-(value as Short)) + UtIntSort -> mkInt(-(value as Int)) + UtLongSort -> mkLong(-(value as Long)) + UtFp32Sort -> mkFloat(-(value as Float)) + UtFp64Sort -> mkDouble(-(value as Double)) + else -> UtNegExpression(variable.toPrimitiveValue(expr.variable.type)) + } + } else { + UtNegExpression(variable.toPrimitiveValue(expr.variable.type)) + } + } + + override fun visit(expr: UtBvNotExpression): UtExpression = expr + + override fun visit(expr: UtCastExpression): UtExpression = + applySimplification(expr) { + val newExpr = expr.variable.expr.accept(this) + when { + // Cast(a@(PrimitiveValue type), type) ---> a + expr.type == expr.variable.type -> newExpr + newExpr.isConcrete -> when (newExpr) { + is UtBoolLiteral -> newExpr.toLong().toValueWithSort(expr.sort).primitiveToSymbolic().expr + is UtBvLiteral -> newExpr.toLong().toValueWithSort(expr.sort).primitiveToSymbolic().expr + is UtFpLiteral -> newExpr.value.toValueWithSort(expr.sort).primitiveToSymbolic().expr + else -> UtCastExpression(newExpr.toPrimitiveValue(expr.variable.type), expr.type) + } + else -> UtCastExpression(newExpr.toPrimitiveValue(expr.variable.type), expr.type) + } + } + + + /** + * Simplify expression [Eq](left, right), where [right] operand is integral literal, + * using information about lower and upper bounds of [left] operand from [lts] and [gts] maps. + * + * @return + * [UtFalse], if lower bound of [left] is greater than [right].value + * or upper bound of [left] is less than [right].value + * + * [Eq](left, right), otherwise + */ + private fun simplifyEq(left: PrimitiveValue, right: PrimitiveValue): UtBoolExpression { + val leftExpr = if (left.expr is UtAddrExpression) left.expr.internal else left.expr + val lowerBound = gts[leftExpr] + val upperBound = lts[leftExpr] + + return if (lowerBound == null && upperBound == null) { + Eq(left, right) + } else { + val rightValue = right.expr.toLong() + when { + lowerBound != null && lowerBound > rightValue -> UtFalse + upperBound != null && upperBound < rightValue -> UtFalse + else -> Eq(left, right) + } + } + } + + private fun simplifyEq(left: UtExpression, right: UtExpression): UtBoolExpression { + val leftExpr = if (left is UtAddrExpression) left.internal else left + val lowerBound = gts[leftExpr] + val upperBound = lts[leftExpr] + return if (lowerBound == null && upperBound == null) { + UtEqExpression(left, right) + } else { + val rightValue = right.toLong() + when { + lowerBound != null && lowerBound > rightValue -> UtFalse + upperBound != null && upperBound < rightValue -> UtFalse + else -> UtEqExpression(left, right) + } + } + } + + + /** + * Simplify expression [Gt](left, right) or [Ge](left, right), where [right] operand is integral literal, + * using information about lower and upper bounds of [left] operand from [lts] and [gts] maps. + * + * @param including - if true than simplified expression is Ge(left,right), else Gt(left, right) + * @return + * [UtTrue], if lower bound of [left] is greater than [right].value. + * + * [UtFalse], if upper bound of [left] is less than [right].value. + * + * [Eq](left, right), if interval [[right].value, upper bound of [left]] has length = 1. + * + * [Gt]|[Ge](left, right), otherwise, depending on value of [including] parameter. + */ + private fun simplifyGreater(left: PrimitiveValue, right: PrimitiveValue, including: Boolean): UtExpression { + val leftExpr = if (left.expr is UtAddrExpression) left.expr.internal else left.expr + val lowerBound = gts[leftExpr] + val upperBound = lts[leftExpr] + val operator = if (including) Ge else Gt + + return if (lowerBound == null && upperBound == null) { + operator(left, right) + } else { + val rightValue = right.expr.toLong() + if (including) 0 else 1 + when { + // left > Long.MAX_VALUE + rightValue == Long.MIN_VALUE && !including -> UtFalse + lowerBound != null && lowerBound >= rightValue -> UtTrue + upperBound != null && upperBound < rightValue -> UtFalse + upperBound != null && rightValue - upperBound == 0L -> Eq( + left, + rightValue.toValueWithSort(right.expr.sort).primitiveToSymbolic() + ) + else -> operator(left, right) + } + } + } + + /** + * Simplify expression [Lt](left, right) or [Le](left, right), where [right] operand is integral literal, + * using information about lower and upper bounds of [left] operand from [lts] and [gts] maps. + * + * @param including - if true than simplified expression is Le(left, right), else Lt(left, right) + * @return + * [UtTrue], if upper bound of [left] is less than [right].value. + * + * [UtFalse], if lower bound of [left] is greater than [right].value. + * + * [Eq](left, right), if interval [lower bound of [left], [right].value] has length = 1. + * + * [Lt]|[Le](left, right), otherwise, depending on value of [including] parameter. + */ + private fun simplifyLess(left: PrimitiveValue, right: PrimitiveValue, including: Boolean): UtExpression { + val leftExpr = if (left.expr is UtAddrExpression) left.expr.internal else left.expr + val lowerBound = gts[leftExpr] + val upperBound = lts[leftExpr] + val operator = if (including) Le else Lt + + return if (upperBound == null && lowerBound == null) { + operator(left, right) + } else { + val rightValue = right.expr.toLong() - if (including) 0 else 1 + when { + // left < Long.MIN_VALUE + rightValue == Long.MAX_VALUE && !including -> UtFalse + upperBound != null && upperBound <= rightValue -> UtTrue + lowerBound != null && lowerBound > rightValue -> UtFalse + lowerBound != null && rightValue - lowerBound == 0L -> Eq( + left, + rightValue.toValueWithSort(right.expr.sort).primitiveToSymbolic() + ) + else -> operator(left, right) + } + } + } + + + override fun visit(expr: UtBoolOpExpression): UtExpression = + applySimplification(expr) { + val left = expr.left.expr.accept(this) + val right = expr.right.expr.accept(this) + val leftPrimitive = left.toPrimitiveValue(expr.left.type) + val rightPrimitive = right.toPrimitiveValue(expr.right.type) + when (expr.operator) { + Eq -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqConcrete + // Eq(Integral a, Integral b) ---> a == b + left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right, Long::equals) + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqTrue + // Eq(a, true) ---> a + left is UtBoolExpression && right.isIntegralLiteral && right.toLong() == UtLongTrue -> left + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqFalse + // Eq(a, false) ---> not a + left is UtBoolExpression && right.isIntegralLiteral && right.toLong() == UtLongFalse -> + NotBoolExpression(left).accept(this) + // Eq(Op(_, Integral _), Integral _) + right.isIntegralLiteral && left is UtOpExpression && left.right.expr.isIntegralLiteral -> when (left.operator) { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqAddOpenLiteral + // Eq(Add(a, Integral b), Integral c) ---> Eq(a, Integral (c - b)) + Add -> Eq( + left.left, + evalIntegralLiterals( + right, + left.right.expr, + maxSort(expr.right, left.right), + Long::minus + ).toPrimitiveValue(maxType(expr.right.type, left.right.type)) + ) + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqXorOpenLiteral + // Eq(Xor(a, Integral b), Integral c) ---> Eq(a, Integral (c `xor` b)) + Xor -> Eq( + left.left, + evalIntegralLiterals(right, left.right.expr, maxSort(expr.right, left.right), Long::xor) + .toPrimitiveValue(maxType(expr.right.type, left.right.type)) + ) + else -> Eq(leftPrimitive, rightPrimitive) + } + /** + * @see simplifyEq + */ + right.isIntegralLiteral -> simplifyEq(leftPrimitive, rightPrimitive) + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqConcrete + // Eq(Fp a, Fp b) ---> a == b + left is UtFpLiteral && right is UtFpLiteral -> mkBool(left.value == right.value) + + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqNaN + // Eq(a, Fp NaN) ---> False + left is UtFpLiteral && left.value.toDouble().isNaN() -> UtFalse + right is UtFpLiteral && right.value.toDouble().isNaN() -> UtFalse + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqEqual + // Eq(a a) && a.sort !is FPSort -> true + left == right && expr.left.type !is FloatType && expr.left.type !is DoubleType -> UtTrue + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-eqLiteralToRight + // Eq(a@(Concrete _), b) ---> Eq(b, a) + left.isConcrete -> Eq( + rightPrimitive, + leftPrimitive + ).accept(this) + else -> Eq(leftPrimitive, rightPrimitive) + } + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-neToNotEq + // Ne -> not Eq + Ne -> NotBoolExpression( + Eq( + leftPrimitive, + rightPrimitive + ) + ).accept(this) + Le -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-leConcrete + // Le(Integral a, Integral b) ---> a <= b + left.isIntegralLiteral && right.isIntegralLiteral -> { + evalIntegralLiterals(left, right) { a, b -> a <= b } + } + /** + * @see simplifyLess + * @see simplifyGreater + */ + right.isIntegralLiteral -> simplifyLess(leftPrimitive, rightPrimitive, true) + left.isIntegralLiteral -> simplifyGreater(rightPrimitive, leftPrimitive, true) + else -> Le(leftPrimitive, rightPrimitive) + } + Lt -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-ltConcrete + // Lt(Integral a, Integral b) ---> a < b + left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right) { a, b -> + a < b + } + /** + * @see simplifyLess + * @see simplifyGreater + */ + right.isIntegralLiteral -> simplifyLess(leftPrimitive, rightPrimitive, false) + left.isIntegralLiteral -> simplifyGreater(rightPrimitive, leftPrimitive, false) + else -> Lt(leftPrimitive, rightPrimitive) + } + Ge -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-geConcrete + // Ge(Integral a, Integral b) ---> a >= b + left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right) { a, b -> + a >= b + } + /** + * @see simplifyLess + * @see simplifyGreater + */ + right.isIntegralLiteral -> simplifyGreater(leftPrimitive, rightPrimitive, true) + left.isIntegralLiteral -> simplifyLess(rightPrimitive, leftPrimitive, true) + else -> Ge(leftPrimitive, rightPrimitive) + } + Gt -> when { + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-gtConcrete + // Gt(Integral a, Integral b) ---> a > b + left.isIntegralLiteral && right.isIntegralLiteral -> evalIntegralLiterals(left, right) { a, b -> + a > b + } + /** + * @see simplifyLess + * @see simplifyGreater + */ + right.isIntegralLiteral -> simplifyGreater(leftPrimitive, rightPrimitive, false) + left.isIntegralLiteral -> simplifyLess(rightPrimitive, leftPrimitive, false) + else -> Gt(leftPrimitive, rightPrimitive) + } + } + } + + private fun maxType(left: Type, right: Type): Type = when { + left is LongType -> left + right is LongType -> right + else -> IntType.v() + } + + override fun visit(expr: UtIsExpression): UtExpression = applySimplification(expr, false) { + UtIsExpression(expr.addr.accept(this) as UtAddrExpression, expr.typeStorage, expr.numberOfTypes) + } + + override fun visit(expr: UtGenericExpression): UtExpression = applySimplification(expr, false) { + UtGenericExpression(expr.addr.accept(this) as UtAddrExpression, expr.types, expr.numberOfTypes) + } + + override fun visit(expr: UtIsGenericTypeExpression): UtExpression = applySimplification(expr, false) { + UtIsGenericTypeExpression( + expr.addr.accept(this) as UtAddrExpression, + expr.baseAddr.accept(this) as UtAddrExpression, + expr.parameterTypeIndex + ) + } + + override fun visit(expr: UtEqGenericTypeParametersExpression): UtExpression = + applySimplification(expr, false) { + UtEqGenericTypeParametersExpression( + expr.firstAddr.accept(this) as UtAddrExpression, + expr.secondAddr.accept(this) as UtAddrExpression, + expr.indexMapping + ) + } + + override fun visit(expr: UtInstanceOfExpression): UtExpression = applySimplification(expr, false) { + val simplifiedHard = (expr.constraint.accept(this) as UtBoolExpression).asHardConstraint() + UtInstanceOfExpression(expr.symbolicStateUpdate.copy(hardConstraints = simplifiedHard)) + } + + // CONFLUENCE:UtBot+Expression+Optimizations#UtBotExpressionOptimizations-ite + // ite(true, then, _) ---> then + // ite(false, _, else) ---> else + override fun visit(expr: UtIteExpression): UtExpression = + applySimplification(expr) { + when (val condition = expr.condition.accept(this) as UtBoolExpression) { + UtTrue -> expr.thenExpr.accept(this) + UtFalse -> expr.elseExpr.accept(this) + else -> UtIteExpression(condition, expr.thenExpr.accept(this), expr.elseExpr.accept(this)) + } + } + + override fun visit(expr: UtMkTermArrayExpression): UtExpression = applySimplification(expr, false) + + override fun visit(expr: UtConstArrayExpression): UtExpression = + applySimplification(expr) { + UtConstArrayExpression(expr.constValue.accept(this), expr.sort) + } + + override fun visit(expr: UtArrayInsert): UtExpression = applySimplification(expr, false) { + UtArrayInsert( + expr.arrayExpression.accept(this), + expr.index.expr.accept(this).toIntValue(), + expr.element.accept(this) + ) + } + + override fun visit(expr: UtArrayInsertRange): UtExpression = applySimplification(expr, false) { + UtArrayInsertRange( + expr.arrayExpression.accept(this), + expr.index.expr.accept(this).toIntValue(), + expr.elements.accept(this), + expr.from.expr.accept(this).toIntValue(), + expr.length.expr.accept(this).toIntValue() + ) + } + + override fun visit(expr: UtArrayRemove): UtExpression = applySimplification(expr, false) { + UtArrayRemove( + expr.arrayExpression.accept(this), + expr.index.expr.accept(this).toIntValue() + ) + } + + + override fun visit(expr: UtArrayRemoveRange): UtExpression = applySimplification(expr, false) { + UtArrayRemoveRange( + expr.arrayExpression.accept(this), + expr.index.expr.accept(this).toIntValue(), + expr.length.expr.accept(this).toIntValue() + ) + + } + + override fun visit(expr: UtArraySetRange): UtExpression = applySimplification(expr, false) { + UtArraySetRange( + expr.arrayExpression.accept(this), + expr.index.expr.accept(this).toIntValue(), + expr.elements.accept(this), + expr.from.expr.accept(this).toIntValue(), + expr.length.expr.accept(this).toIntValue() + ) + } + + override fun visit(expr: UtArrayShiftIndexes): UtExpression = applySimplification(expr, false) { + UtArrayShiftIndexes( + expr.arrayExpression.accept(this), + expr.offset.expr.accept(this).toIntValue() + ) + } + + override fun visit(expr: UtArrayApplyForAll): UtExpression = applySimplification(expr, false) { + UtArrayApplyForAll( + expr.arrayExpression.accept(this), + expr.constraint + ) + } +} + +private val arrayExpressionAxiomInstantiationCache = + IdentityHashMap() + +private val arrayExpressionAxiomIndex = AtomicInteger(0) + +private fun instantiateArrayAsNewConst(arrayExpression: UtExtendedArrayExpression) = + arrayExpressionAxiomInstantiationCache.getOrPut(arrayExpression) { + val suffix = when (arrayExpression) { + is UtArrayInsert -> "Insert" + is UtArrayInsertRange -> "InsertRange" + is UtArrayRemove -> "Remove" + is UtArrayRemoveRange -> "RemoveRange" + is UtArraySetRange -> "SetRange" + is UtArrayShiftIndexes -> "ShiftIndexes" + is UtArrayApplyForAll -> error("UtArrayApplyForAll cannot be instantiated as new const array") + } + UtMkArrayExpression( + "_array$suffix${arrayExpressionAxiomIndex.getAndIncrement()}", + arrayExpression.sort + ) + } + +/** + * Visitor that applies the same simplifications as [Simplificator] and instantiate axioms for extended array theory. + * + * @see UtExtendedArrayExpression + */ +class AxiomInstantiationSimplificator( + eqs: Map = emptyMap(), + lts: Map = emptyMap(), + gts: Map = emptyMap() +) : Simplificator(eqs, lts, gts) { + private val instantiatedAxiomExpressions = mutableListOf() + + /** + * Select(UtArrayInsert(a, v, i), j) is equivalent to ITE(i = j, v, Select(a, ITE(j < i, j, j - 1)) + */ + override fun visit(expr: UtArrayInsert): UtExpression { + val arrayInstance = instantiateArrayAsNewConst(expr) + val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) + val element = expr.element.accept(this) + val selectedIndex = selectIndexStack.last() + val pushedIndex = UtIteExpression( + Lt(selectedIndex, index), + selectedIndex.expr, + Add(selectedIndex, (-1).toPrimitiveValue()) + ) + val arrayExpression = withNewSelect(pushedIndex) { expr.arrayExpression.accept(this) } + + instantiatedAxiomExpressions += UtEqExpression( + UtIteExpression( + Eq(selectedIndex, index), + element, + arrayExpression.select(pushedIndex), + ).accept(this), arrayInstance.select(selectedIndex.expr) + ) + return arrayInstance + } + + /** + * Select(UtArrayInsertRange(a, i, b, from, length), j) is equivalent to + * ITE(j >= i && j < i + length, Select(b, j - i + from), Select(a, ITE(j < i, j, j - length) + */ + override fun visit(expr: UtArrayInsertRange): UtExpression { + val arrayInstance = instantiateArrayAsNewConst(expr) + val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) + val from = expr.from.expr.accept(this).toPrimitiveValue(expr.index.type) + val length = expr.length.expr.accept(this).toPrimitiveValue(expr.length.type) + val selectedIndex = selectIndexStack.last() + val pushedArrayInstanceIndex = + UtIteExpression(Lt(selectedIndex, index), selectedIndex.expr, Sub(selectedIndex, length)) + val arrayExpression = withNewSelect(pushedArrayInstanceIndex) { expr.arrayExpression.accept(this) } + val pushedElementsIndex = Add(Sub(selectedIndex, index).toIntValue(), from) + val elements = withNewSelect(pushedElementsIndex) { expr.elements.accept(this) } + instantiatedAxiomExpressions += UtEqExpression( + UtIteExpression( + mkAnd(Ge(selectedIndex, index), Lt(selectedIndex, Add(index, length).toIntValue())), + elements.select(pushedElementsIndex), + arrayExpression.select(pushedArrayInstanceIndex), + ).accept(this), arrayInstance.select(selectedIndex.expr) + ) + return arrayInstance + } + + /** + * Select(UtArrayRemove(a, i), j) is equivalent to Select(a, ITE(j < i, j, j + 1)) + */ + override fun visit(expr: UtArrayRemove): UtExpression { + val arrayInstance = instantiateArrayAsNewConst(expr) + val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) + val selectedIndex = selectIndexStack.last() + val pushedIndex = UtIteExpression( + Lt(selectedIndex, index), + selectedIndex.expr, + Add(selectedIndex, 1.toPrimitiveValue()) + ) + val arrayExpression = withNewSelect(pushedIndex) { expr.arrayExpression.accept(this) } + + instantiatedAxiomExpressions += UtEqExpression( + arrayExpression.select(pushedIndex), + arrayInstance.select(selectedIndex.expr) + ) + return arrayInstance + } + + /** + * Select(UtArrayRemoveRange(a, i, length), j) is equivalent to Select(a, ITE(j < i, j, j + length)) + */ + override fun visit(expr: UtArrayRemoveRange): UtExpression { + val arrayInstance = instantiateArrayAsNewConst(expr) + val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) + val length = expr.length.expr.accept(this).toPrimitiveValue(expr.length.type) + val selectedIndex = selectIndexStack.last() + val pushedIndex = UtIteExpression( + Lt(selectedIndex, index), + selectedIndex.expr, + Add(selectedIndex, length) + ) + val arrayExpression = withNewSelect(pushedIndex) { expr.arrayExpression.accept(this) } + + instantiatedAxiomExpressions += UtEqExpression( + arrayExpression.select(pushedIndex), + arrayInstance.select(selectedIndex.expr) + ) + return arrayInstance + } + + /** + * Select(UtSetRange(a, i, b, from, length), j) is equivalent to + * ITE(j >= i && j < i + length, Select(b, j - i + from), Select(a, i)) + */ + override fun visit(expr: UtArraySetRange): UtExpression { + val arrayInstance = instantiateArrayAsNewConst(expr) + val index = expr.index.expr.accept(this).toPrimitiveValue(expr.index.type) + val from = expr.from.expr.accept(this).toPrimitiveValue(expr.index.type) + val length = expr.length.expr.accept(this).toPrimitiveValue(expr.length.type) + val selectedIndex = selectIndexStack.last() + val arrayExpression = expr.arrayExpression.accept(this) + val pushedIndex = Add(Sub(selectedIndex, index).toIntValue(), from) + val elements = withNewSelect(pushedIndex) { expr.elements.accept(this) } + instantiatedAxiomExpressions += UtEqExpression( + UtIteExpression( + mkAnd(Ge(selectedIndex, index), Lt(selectedIndex, Add(index, length).toIntValue())), + elements.select(pushedIndex), + arrayExpression.select(selectedIndex.expr) + ).accept(this), arrayInstance.select(selectedIndex.expr) + ) + return arrayInstance + } + + /** + * Select(UtShiftIndexes(a, offset), j) is equivalent to Select(a, j - offset) + */ + override fun visit(expr: UtArrayShiftIndexes): UtExpression { + val arrayInstance = instantiateArrayAsNewConst(expr) + val offset = expr.offset.expr.accept(this).toIntValue() + val selectedIndex = selectIndexStack.last() + val pushedIndex = Sub(selectedIndex, offset) + val arrayExpression = withNewSelect(pushedIndex) { expr.arrayExpression.accept(this) } + + instantiatedAxiomExpressions += UtEqExpression( + arrayExpression.select(pushedIndex), + arrayInstance.select(selectedIndex.expr) + ) + return arrayInstance + } + + /** + * instantiate expr.constraint on selecting from UtArrayApplyForAll + */ + override fun visit(expr: UtArrayApplyForAll): UtExpression { + val selectedIndex = selectIndexStack.last() + val arrayExpression = expr.arrayExpression.accept(this) + val constraint = expr.constraint(arrayExpression, selectedIndex) + instantiatedAxiomExpressions += constraint + return arrayExpression + } + + val instantiatedArrayAxioms: List + get() = instantiatedAxiomExpressions +} + +private const val UtLongTrue = 1L +private const val UtLongFalse = 0L diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/StringExpressions.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/StringExpressions.kt deleted file mode 100644 index a06de568a3..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/StringExpressions.kt +++ /dev/null @@ -1,422 +0,0 @@ -package org.utbot.engine.pc - -import org.utbot.common.WorkaroundReason.HACK -import org.utbot.common.workaround -import org.utbot.engine.PrimitiveValue -import java.util.Objects - -sealed class UtStringExpression : UtExpression(UtSeqSort) - -data class UtStringConst(val name: String) : UtStringExpression() { - - override val hashCode = Objects.hash(name) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(String$sort $name)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtStringConst - - if (name != other.name) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtConcatExpression(val parts: List) : UtStringExpression() { - override val hashCode = parts.hashCode() - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.concat ${parts.joinToString()})" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtConcatExpression - - if (parts != other.parts) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtConvertToString(val expression: UtExpression) : UtStringExpression() { - init { - require(expression.sort is UtBvSort) - } - - override val hashCode = expression.hashCode() - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.tostr $expression)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtConvertToString - - if (expression != other.expression) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtStringToInt(val expression: UtExpression, override val sort: UtBvSort) : UtBvExpression(sort) { - init { - require(expression.sort is UtSeqSort) - } - - override val hashCode = Objects.hash(expression, sort) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.toint $expression)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtStringToInt - - if (expression != other.expression) return false - if (sort != other.sort) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtStringLength(val string: UtExpression) : UtBvExpression(UtIntSort) { - override val hashCode = string.hashCode() - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.len $string)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtStringLength - - if (string != other.string) return false - - return true - } - - override fun hashCode() = hashCode -} - -/** - * Creates constraint to get rid of negative string length (z3 bug?) - */ -data class UtStringPositiveLength(val string: UtExpression) : UtBoolExpression() { - override val hashCode = string.hashCode() - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(positive.str.len $string)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtStringPositiveLength - - if (string != other.string) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtStringCharAt( - val string: UtExpression, - val index: UtExpression -) : UtExpression(UtCharSort) { - override val hashCode = Objects.hash(string, index) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.at $string $index)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtStringCharAt - - if (string != other.string) return false - if (index != other.index) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtStringEq(val left: UtExpression, val right: UtExpression) : UtBoolExpression() { - override val hashCode = Objects.hash(left, right) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.eq $left $right)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtStringEq - - if (left != other.left) return false - if (right != other.right) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtSubstringExpression( - val string: UtExpression, - val beginIndex: UtExpression, - val length: UtExpression -) : UtStringExpression() { - override val hashCode = Objects.hash(string, beginIndex, length) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.substr $string $beginIndex $length)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtSubstringExpression - - if (string != other.string) return false - if (beginIndex != other.beginIndex) return false - if (length != other.length) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtReplaceExpression( - val string: UtExpression, - val regex: UtExpression, - val replacement: UtExpression -) : UtStringExpression() { - override val hashCode = Objects.hash(string, regex, replacement) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.replace $string $regex $replacement)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtReplaceExpression - - if (string != other.string) return false - if (regex != other.regex) return false - if (replacement != other.replacement) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtStartsWithExpression( - val string: UtExpression, - val prefix: UtExpression -) : UtBoolExpression() { - override val hashCode = Objects.hash(string, prefix) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.prefixof $string $prefix)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtStartsWithExpression - - if (string != other.string) return false - if (prefix != other.prefix) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtEndsWithExpression( - val string: UtExpression, - val suffix: UtExpression -) : UtBoolExpression() { - override val hashCode = Objects.hash(string, suffix) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.suffixof $string $suffix)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtEndsWithExpression - - if (string != other.string) return false - if (suffix != other.suffix) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtIndexOfExpression( - val string: UtExpression, - val substring: UtExpression -) : UtBoolExpression() { - override val hashCode = Objects.hash(string, substring) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.indexof $string $substring)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtIndexOfExpression - - if (string != other.string) return false - if (substring != other.substring) return false - - return true - } - - override fun hashCode() = hashCode - -} - -data class UtContainsExpression( - val string: UtExpression, - val substring: UtExpression -) : UtBoolExpression() { - override val hashCode = Objects.hash(string, substring) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.contains $string $substring)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtContainsExpression - - if (string != other.string) return false - if (substring != other.substring) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtToStringExpression( - val isNull: UtBoolExpression, - val notNullExpr: UtExpression -) : UtStringExpression() { - override val hashCode = Objects.hash(isNull, notNullExpr) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(ite $isNull 'null' $notNullExpr)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtToStringExpression - - if (isNull != other.isNull) return false - if (notNullExpr != other.notNullExpr) return false - - return true - } - - override fun hashCode() = hashCode -} - -data class UtArrayToString( - val arrayExpression: UtExpression, - val offset: PrimitiveValue, - val length: PrimitiveValue -) : UtStringExpression() { - override val hashCode = Objects.hash(arrayExpression, offset) - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = "(str.array_to_str $arrayExpression, offset = $offset)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtArrayToString - - if (arrayExpression == other.arrayExpression) return false - if (offset == other.offset) return false - - return true - } - - override fun hashCode() = hashCode -} - -// String literal (not a String Java object!) -data class UtSeqLiteral(val value: String) : UtExpression(UtSeqSort) { - override val hashCode = value.hashCode() - - override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) - - override fun toString() = value - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UtSeqLiteral - - if (value != other.value) return false - - return true - } - - override fun hashCode() = hashCode -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtExpression.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtExpression.kt index 94e3944172..ebd6bcdec5 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtExpression.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtExpression.kt @@ -27,7 +27,6 @@ val UtExpression.isConcrete: Boolean get() = when (this) { is UtBvLiteral -> true is UtFpLiteral -> true - is UtSeqLiteral -> true is UtBoolLiteral -> true is UtAddrExpression -> internal.isConcrete else -> false @@ -36,7 +35,6 @@ val UtExpression.isConcrete: Boolean fun UtExpression.toConcrete(): Any = when (this) { is UtBvLiteral -> this.value is UtFpLiteral -> this.value - is UtSeqLiteral -> this.value UtTrue -> true UtFalse -> false is UtAddrExpression -> internal.toConcrete() @@ -457,7 +455,7 @@ class UtIsGenericTypeExpression( override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) override fun toString(): String { - return "(generic-is $addr $baseAddr<\$$parameterTypeIndex>)" + return "(generic-is $addr baseAddr: $baseAddr<\$$parameterTypeIndex>)" } override fun equals(other: Any?): Boolean { @@ -807,6 +805,31 @@ data class UtSubNoOverflowExpression( override fun hashCode() = hashCode } +data class UtMulNoOverflowExpression( + val left: UtExpression, + val right: UtExpression, +) : UtBoolExpression() { + override val hashCode = Objects.hash(left, right) + + override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) + + override fun toString() = "(mulNoOverflow $left $right)" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UtMulNoOverflowExpression + + if (left != other.left) return false + if (right != other.right) return false + + return true + } + + override fun hashCode() = hashCode +} + data class UtNegExpression(val variable: PrimitiveValue) : UtExpression(alignSort(variable.type.toSort())) { override val hashCode = variable.hashCode @@ -828,6 +851,27 @@ data class UtNegExpression(val variable: PrimitiveValue) : UtExpression(alignSor override fun hashCode() = hashCode } +data class UtBvNotExpression(val variable: PrimitiveValue) : UtExpression(alignSort(variable.type.toSort())) { + override val hashCode = variable.hashCode + + override fun accept(visitor: UtExpressionVisitor): TResult = visitor.visit(this) + + override fun toString() = "(bvNot ${variable.expr})" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UtBvNotExpression + + if (variable != other.variable) return false + + return true + } + + override fun hashCode() = hashCode +} + /** * Implements Unary Numeric Promotion. * Converts byte, short and char sort to int sort. @@ -934,7 +978,7 @@ data class UtMkTermArrayExpression(val array: UtArrayExpressionBase, val default override fun hashCode() = hashCode } -data class Z3Variable(val type: Type, val expr: Expr) { +data class Z3Variable(val type: Type, val expr: Expr<*>) { private val hashCode = Objects.hash(type, expr) override fun equals(other: Any?): Boolean { @@ -952,6 +996,6 @@ data class Z3Variable(val type: Type, val expr: Expr) { override fun hashCode() = hashCode } -fun Expr.z3Variable(type: Type) = Z3Variable(type, this) +fun Expr<*>.z3Variable(type: Type) = Z3Variable(type, this) fun UtExpression.isInteger() = this.sort is UtBvSort \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtExpressionVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtExpressionVisitor.kt index 760633ff37..4f22095860 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtExpressionVisitor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtExpressionVisitor.kt @@ -19,6 +19,7 @@ interface UtExpressionVisitor { fun visit(expr: UtOrBoolExpression): TResult fun visit(expr: UtAndBoolExpression): TResult fun visit(expr: UtNegExpression): TResult + fun visit(expr: UtBvNotExpression): TResult fun visit(expr: UtCastExpression): TResult fun visit(expr: UtBoolOpExpression): TResult fun visit(expr: UtIsExpression): TResult @@ -29,25 +30,6 @@ interface UtExpressionVisitor { fun visit(expr: UtIteExpression): TResult fun visit(expr: UtMkTermArrayExpression): TResult - // UtString expressions - fun visit(expr: UtStringConst): TResult - fun visit(expr: UtConcatExpression): TResult - fun visit(expr: UtConvertToString): TResult - fun visit(expr: UtStringToInt): TResult - fun visit(expr: UtStringLength): TResult - fun visit(expr: UtStringPositiveLength): TResult - fun visit(expr: UtStringCharAt): TResult - fun visit(expr: UtStringEq): TResult - fun visit(expr: UtSubstringExpression): TResult - fun visit(expr: UtReplaceExpression): TResult - fun visit(expr: UtStartsWithExpression): TResult - fun visit(expr: UtEndsWithExpression): TResult - fun visit(expr: UtIndexOfExpression): TResult - fun visit(expr: UtContainsExpression): TResult - fun visit(expr: UtToStringExpression): TResult - fun visit(expr: UtSeqLiteral): TResult - fun visit(expr: UtArrayToString): TResult - // UtArray expressions from extended array theory fun visit(expr: UtArrayInsert): TResult fun visit(expr: UtArrayInsertRange): TResult @@ -56,9 +38,9 @@ interface UtExpressionVisitor { fun visit(expr: UtArraySetRange): TResult fun visit(expr: UtArrayShiftIndexes): TResult fun visit(expr: UtArrayApplyForAll): TResult - fun visit(expr: UtStringToArray): TResult // Add and Sub with overflow detection fun visit(expr: UtAddNoOverflowExpression): TResult fun visit(expr: UtSubNoOverflowExpression): TResult + fun visit(expr: UtMulNoOverflowExpression): TResult } \ No newline at end of file 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 5f226f8524..7d98030f0c 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 @@ -3,12 +3,12 @@ package org.utbot.engine.pc import org.utbot.analytics.IncrementalData import org.utbot.analytics.Predictors import org.utbot.analytics.learnOn -import org.utbot.common.bracket +import org.utbot.common.measureTime import org.utbot.common.md5 import org.utbot.common.trace import org.utbot.engine.Eq import org.utbot.engine.PrimitiveValue -import org.utbot.engine.TypeRegistry +import org.utbot.engine.types.TypeRegistry import org.utbot.engine.pc.UtSolverStatusKind.SAT import org.utbot.engine.pc.UtSolverStatusKind.UNKNOWN import org.utbot.engine.pc.UtSolverStatusKind.UNSAT @@ -96,8 +96,6 @@ fun UtExpression.select(outerIndex: UtExpression, nestedIndex: UtExpression) = fun UtExpression.store(index: UtExpression, elem: UtExpression) = UtArrayMultiStoreExpression(this, index, elem) -fun mkString(value: String): UtStringConst = UtStringConst(value) - fun PrimitiveValue.align(): PrimitiveValue = when (type) { is ByteType, is ShortType, is CharType -> UtCastExpression(this, IntType.v()).toIntValue() else -> this @@ -153,8 +151,8 @@ data class UtSolver constructor( //protection against solver reusage private var canBeCloned: Boolean = true - val rewriter: RewritingVisitor - get() = constraints.let { if (it is Query) it.rewriter else RewritingVisitor() } + val simplificator: Simplificator + get() = constraints.let { if (it is Query) it.simplificator else Simplificator() } /** * Returns the current status of the constraints. @@ -224,7 +222,7 @@ data class UtSolver constructor( val translatedAssumes = assumption.constraints.translate() - val statusHolder = logger.trace().bracket("High level check(): ", { it }) { + val statusHolder = logger.trace().measureTime({ "High level check(): " }, { it }) { Predictors.smtIncremental.learnOn(IncrementalData(constraints.hard, hardConstraintsNotYetAddedToZ3Solver)) { hardConstraintsNotYetAddedToZ3Solver.forEach { z3Solver.add(translator.translate(it) as BoolExpr) } @@ -257,7 +255,7 @@ data class UtSolver constructor( val assumptionsInUnsatCore = mutableListOf() while (true) { - val res = logger.trace().bracket("Low level check(): ", { it }) { + val res = logger.trace().measureTime({ "Low level check(): " }, { it }) { val constraintsToCheck = translatedSoft.keys + translatedAssumptions.keys z3Solver.check(*constraintsToCheck.toTypedArray()) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSolverStatus.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSolverStatus.kt index 0df5e2b37c..f0b6def89e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSolverStatus.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSolverStatus.kt @@ -40,18 +40,14 @@ data class UtSolverStatusUNSAT(override val statusKind: UtSolverStatusKind) : Ut } class UtSolverStatusSAT( - private val translator: Z3TranslatorVisitor, + translator: Z3TranslatorVisitor, z3Solver: Solver ) : UtSolverStatus(SAT) { private val model = z3Solver.model private val evaluator: Z3EvaluatorVisitor = translator.evaluator(model) - //TODO put special markers inside expressionTranslationCache for detecting circular dependencies - fun translate(expression: UtExpression): Expr = - translator.translate(expression) - - fun eval(expression: UtExpression): Expr = evaluator.eval(expression) + fun eval(expression: UtExpression): Expr<*> = evaluator.eval(expression) fun concreteAddr(expression: UtAddrExpression): Address = eval(expression).intValue() @@ -69,7 +65,7 @@ class UtSolverStatusSAT( * - val arrayInterpretationFuncDecl: FuncDecl = mfd.parameters[[0]].funcDecl * - val interpretation: FuncInterp = z3Solver.model.getFuncInterp(arrayInterpretationFuncDecl) */ - internal fun evalArrayDescriptor(mval: Expr, unsigned: Boolean, filter: (Int) -> Boolean): ArrayDescriptor { + internal fun evalArrayDescriptor(mval: Expr<*>, unsigned: Boolean, filter: (Int) -> Boolean): ArrayDescriptor { var next = mval val stores = mutableMapOf() var const: Any? = null @@ -83,7 +79,7 @@ class UtSolverStatusSAT( next = next.args[0] } Z3_OP_UNINTERPRETED -> next = model.eval(next) - Z3_OP_CONST_ARRAY -> const = if (next.args[0] is ArrayExpr) { + Z3_OP_CONST_ARRAY -> const = if (next.args[0] is ArrayExpr<*, *>) { // if we have an array as const value, create a corresponding descriptor for it evalArrayDescriptor(next.args[0], unsigned, filter) } else { diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSort.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSort.kt index c30c20eb12..f5af719fb8 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSort.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSort.kt @@ -1,6 +1,6 @@ package org.utbot.engine.pc -import org.utbot.engine.SeqType +import org.utbot.engine.types.SeqType import com.microsoft.z3.Context import com.microsoft.z3.Sort import java.util.Objects diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Util.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Util.kt new file mode 100644 index 0000000000..ad8f055be7 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Util.kt @@ -0,0 +1,7 @@ +package org.utbot.engine.pc + +import com.microsoft.z3.Expr +import com.microsoft.z3.Sort + +@Suppress("UNCHECKED_CAST") +fun Expr.cast(): Expr = this as Expr diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Z3EvaluatorVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Z3EvaluatorVisitor.kt index 1f0e886fd8..5ac6df2e16 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Z3EvaluatorVisitor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Z3EvaluatorVisitor.kt @@ -20,38 +20,38 @@ import soot.ByteType import soot.CharType class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3TranslatorVisitor) : - UtExpressionVisitor by translator { + UtExpressionVisitor> by translator { // stack of indexes that are visited in expression in derivation tree. // For example, we call eval(select(select(a, i), j) // On visiting term a, the stack will be equals to [i, j] - private val selectIndexStack = mutableListOf() + private val selectIndexStack = mutableListOf>() - private inline fun withPushedSelectIndex(index: UtExpression, block: () -> Expr): Expr { + private inline fun withPushedSelectIndex(index: UtExpression, block: () -> Expr<*>): Expr<*> { selectIndexStack += eval(index) return block().also { selectIndexStack.removeLast() } } - private inline fun withPoppedSelectIndex(block: () -> Expr): Expr { + private inline fun withPoppedSelectIndex(block: () -> Expr<*>): Expr<*> { val lastIndex = selectIndexStack.removeLast() return block().also { selectIndexStack += lastIndex } } - private fun foldSelectsFromStack(expr: Expr): Expr { + private fun foldSelectsFromStack(expr: Expr<*>): Expr<*> { var result = expr var i = selectIndexStack.size - while (i > 0 && result.sort is ArraySort) { - result = translator.withContext { mkSelect(result as ArrayExpr, selectIndexStack[--i]) } + while (i > 0 && result.sort is ArraySort<*, *>) { + result = translator.withContext { mkSelect(result as ArrayExpr<*, *>, selectIndexStack[--i].cast()) } } return result } - fun eval(expr: UtExpression): Expr { + fun eval(expr: UtExpression): Expr<*> { val translated = if (expr.sort is UtArraySort) { translator.lookUpCache(expr)?.let { foldSelectsFromStack(it) } ?: expr.accept(this) } else { @@ -60,14 +60,14 @@ class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3Tra return model.eval(translated) } - override fun visit(expr: UtArraySelectExpression): Expr = expr.run { + override fun visit(expr: UtArraySelectExpression): Expr<*> = expr.run { // translate arrayExpression here will return evaluation of arrayExpression.select(index) withPushedSelectIndex(index) { eval(arrayExpression) } } - override fun visit(expr: UtConstArrayExpression): Expr = expr.run { + override fun visit(expr: UtConstArrayExpression): Expr<*> = expr.run { // expr.select(index) = constValue for any index if (selectIndexStack.size == 0) { translator.withContext { mkConstArray(sort.indexSort.toZ3Sort(this), eval(constValue)) } @@ -78,17 +78,17 @@ class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3Tra } } - override fun visit(expr: UtMkArrayExpression): Expr = + override fun visit(expr: UtMkArrayExpression): Expr<*> = // mkArray expression can have more than one dimension // so for such case select(select(mkArray(...), i), j)) // indices i and j are currently in the stack and we need to fold them. foldSelectsFromStack(translator.translate(expr)) - override fun visit(expr: UtArrayMultiStoreExpression): Expr = expr.run { + override fun visit(expr: UtArrayMultiStoreExpression): Expr<*> = expr.run { val lastIndex = selectIndexStack.lastOrNull() ?: return translator.withContext { - stores.fold(translator.translate(initial) as ArrayExpr) { acc, (index, item) -> - mkStore(acc, eval(index), eval(item)) + stores.fold(translator.translate(initial) as ArrayExpr<*, *>) { acc, (index, item) -> + mkStore(acc, eval(index).cast(), eval(item).cast()) } } for (store in stores.asReversed()) { @@ -99,12 +99,12 @@ class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3Tra eval(initial) } - override fun visit(expr: UtMkTermArrayExpression): Expr = expr.run { + override fun visit(expr: UtMkTermArrayExpression): Expr<*> = expr.run { // should we make eval(mkTerm) always true?? translator.translate(UtTrue) } - override fun visit(expr: UtArrayInsert): Expr = expr.run { + override fun visit(expr: UtArrayInsert): Expr<*> = expr.run { val lastIndex = selectIndexStack.last().intValue() val index = eval(index.expr).intValue() when { @@ -114,7 +114,7 @@ class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3Tra } } - override fun visit(expr: UtArrayInsertRange): Expr = expr.run { + override fun visit(expr: UtArrayInsertRange): Expr<*> = expr.run { val lastIndex = selectIndexStack.last().intValue() val index = eval(index.expr).intValue() val length = eval(length.expr).intValue() @@ -128,7 +128,7 @@ class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3Tra } } - override fun visit(expr: UtArrayRemove): Expr = expr.run { + override fun visit(expr: UtArrayRemove): Expr<*> = expr.run { val lastIndex = selectIndexStack.last().intValue() val index = eval(index.expr).intValue() if (lastIndex < index) { @@ -138,7 +138,7 @@ class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3Tra } } - override fun visit(expr: UtArrayRemoveRange): Expr = expr.run { + override fun visit(expr: UtArrayRemoveRange): Expr<*> = expr.run { val lastIndex = selectIndexStack.last().intValue() val index = eval(index.expr).intValue() if (lastIndex < index) { @@ -149,7 +149,7 @@ class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3Tra } } - override fun visit(expr: UtArraySetRange): Expr = expr.run { + override fun visit(expr: UtArraySetRange): Expr<*> = expr.run { val lastIndex = selectIndexStack.last().intValue() val index = eval(index.expr).intValue() val length = eval(length.expr).intValue() @@ -162,21 +162,15 @@ class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3Tra } } - override fun visit(expr: UtArrayShiftIndexes): Expr = expr.run { + override fun visit(expr: UtArrayShiftIndexes): Expr<*> = expr.run { val lastIndex = selectIndexStack.last().intValue() val offset = eval(offset.expr).intValue() eval(arrayExpression.select(mkInt(lastIndex - offset))) } - override fun visit(expr: UtStringToArray): Expr = expr.run { - val lastIndex = selectIndexStack.last().intValue() - val offset = eval(offset.expr).intValue() - eval(UtStringCharAt(stringExpression, mkInt(lastIndex + offset))) - } - - override fun visit(expr: UtAddrExpression): Expr = eval(expr.internal) + override fun visit(expr: UtAddrExpression): Expr<*> = eval(expr.internal) - override fun visit(expr: UtOpExpression): Expr = expr.run { + override fun visit(expr: UtOpExpression): Expr<*> = expr.run { val leftResolve = eval(left.expr).z3Variable(left.type) val rightResolve = eval(right.expr).z3Variable(right.type) translator.withContext { @@ -184,160 +178,79 @@ class Z3EvaluatorVisitor(private val model: Model, private val translator: Z3Tra } } - override fun visit(expr: UtEqExpression): Expr = expr.run { + override fun visit(expr: UtEqExpression): Expr<*> = expr.run { translator.withContext { mkEq(eval(left), eval(right)) } } - override fun visit(expr: NotBoolExpression): Expr = + override fun visit(expr: NotBoolExpression): Expr<*> = translator.withContext { mkNot(eval(expr.expr) as BoolExpr) } - override fun visit(expr: UtOrBoolExpression): Expr = expr.run { + override fun visit(expr: UtOrBoolExpression): Expr<*> = expr.run { translator.withContext { mkOr(*expr.exprs.map { eval(it) as BoolExpr }.toTypedArray()) } } - override fun visit(expr: UtAndBoolExpression): Expr = expr.run { + override fun visit(expr: UtAndBoolExpression): Expr<*> = expr.run { translator.withContext { mkAnd(*expr.exprs.map { eval(it) as BoolExpr }.toTypedArray()) } } - override fun visit(expr: UtAddNoOverflowExpression): Expr = expr.run { + override fun visit(expr: UtAddNoOverflowExpression): Expr<*> = expr.run { translator.withContext { mkBVAddNoOverflow(eval(expr.left) as BitVecExpr, eval(expr.right) as BitVecExpr, true) } } - override fun visit(expr: UtSubNoOverflowExpression): Expr = expr.run { + override fun visit(expr: UtSubNoOverflowExpression): Expr<*> = expr.run { translator.withContext { // For some reason mkBVSubNoOverflow does not take "signed" as an argument, yet still works for signed integers mkBVSubNoOverflow(eval(expr.left) as BitVecExpr, eval(expr.right) as BitVecExpr) //, true) } } - override fun visit(expr: UtNegExpression): Expr = expr.run { - translator.withContext { - negate(this, eval(variable.expr).z3Variable(variable.type)) - } - } - - override fun visit(expr: UtCastExpression): Expr = expr.run { - val z3var = eval(variable.expr).z3Variable(variable.type) - translator.withContext { convertVar(z3var, type).expr } - } - - override fun visit(expr: UtBoolOpExpression): Expr = expr.run { - val leftResolve = eval(left.expr).z3Variable(left.type) - val rightResolve = eval(right.expr).z3Variable(right.type) + override fun visit(expr: UtMulNoOverflowExpression): Expr<*> = expr.run { translator.withContext { - operator.delegate(this, leftResolve, rightResolve) - } - } - - override fun visit(expr: UtIsExpression): Expr = translator.translate(expr) - - override fun visit(expr: UtInstanceOfExpression): Expr = - expr.run { eval(expr.constraint) } - - override fun visit(expr: UtIteExpression): Expr = expr.run { - if (eval(condition).value() as Boolean) eval(thenExpr) else eval(elseExpr) - } - - override fun visit(expr: UtConcatExpression): Expr = expr.run { - translator.withContext { mkConcat(*parts.map { eval(it) as SeqExpr }.toTypedArray()) } - } - - override fun visit(expr: UtStringLength): Expr = expr.run { - translator.withContext { - if (string is UtArrayToString) { - eval(string.length.expr) - } else { - mkInt2BV(MAX_STRING_LENGTH_SIZE_BITS, mkLength(eval(string) as SeqExpr)) - } + mkBVMulNoOverflow(eval(expr.left) as BitVecExpr, eval(expr.right) as BitVecExpr , true) } } - override fun visit(expr: UtStringPositiveLength): Expr = expr.run { + override fun visit(expr: UtNegExpression): Expr<*> = expr.run { translator.withContext { - mkGe(mkLength(eval(string) as SeqExpr), mkInt(0)) - } - } - - override fun visit(expr: UtStringCharAt): Expr = expr.run { - translator.withContext { - val charAtExpr = mkSeqNth(eval(string) as SeqExpr, mkBV2Int(eval(index) as BitVecExpr, true)) - val z3var = charAtExpr.z3Variable(ByteType.v()) - convertVar(z3var, CharType.v()).expr - } - } - - override fun visit(expr: UtStringEq): Expr = expr.run { - translator.withContext { - mkEq(eval(left), eval(right)) + negate(this, eval(variable.expr).z3Variable(variable.type)) } } - override fun visit(expr: UtSubstringExpression): Expr = expr.run { + override fun visit(expr: UtBvNotExpression): Expr<*> = expr.run { translator.withContext { - mkExtract( - eval(string) as SeqExpr, - mkBV2Int(eval(beginIndex) as BitVecExpr, true), - mkBV2Int(eval(length) as BitVecExpr, true) - ) + mkBVNot(eval(variable.expr) as BitVecExpr) } } - override fun visit(expr: UtReplaceExpression): Expr = expr.run { - workaround(WorkaroundReason.HACK) { // mkReplace replaces first occasion only - translator.withContext { - mkReplace( - eval(string) as SeqExpr, - eval(regex) as SeqExpr, - eval(replacement) as SeqExpr - ) - } - } - } - - // Attention, prefix is a first argument! - override fun visit(expr: UtStartsWithExpression): Expr = expr.run { - translator.withContext { - mkPrefixOf(eval(prefix) as SeqExpr, eval(string) as SeqExpr) - } + override fun visit(expr: UtCastExpression): Expr<*> = expr.run { + val z3var = eval(variable.expr).z3Variable(variable.type) + translator.withContext { convertVar(z3var, type).expr } } - // Attention, suffix is a first argument! - override fun visit(expr: UtEndsWithExpression): Expr = expr.run { + override fun visit(expr: UtBoolOpExpression): Expr<*> = expr.run { + val leftResolve = eval(left.expr).z3Variable(left.type) + val rightResolve = eval(right.expr).z3Variable(right.type) translator.withContext { - mkSuffixOf(eval(suffix) as SeqExpr, eval(string) as SeqExpr) + operator.delegate(this, leftResolve, rightResolve) } } - override fun visit(expr: UtIndexOfExpression): Expr = expr.run { - val string = eval(string) as SeqExpr - val substring = eval(substring) as SeqExpr - translator.withContext { - mkInt2BV( - MAX_STRING_LENGTH_SIZE_BITS, - mkIndexOf(string, substring, mkInt(0)) - ) - } - } + override fun visit(expr: UtIsExpression): Expr<*> = translator.translate(expr) - override fun visit(expr: UtContainsExpression): Expr = expr.run { - val substring = eval(substring) as SeqExpr - val string = eval(string) as SeqExpr - translator.withContext { - mkGe(mkIndexOf(string, substring, mkInt(0)), mkInt(0)) - } - } + override fun visit(expr: UtInstanceOfExpression): Expr<*> = + expr.run { eval(expr.constraint) } - override fun visit(expr: UtToStringExpression): Expr = expr.run { - if (eval(isNull).value() as Boolean) translator.withContext { mkString("null") } else eval(notNullExpr) + override fun visit(expr: UtIteExpression): Expr<*> = expr.run { + if (eval(condition).value() as Boolean) eval(thenExpr) else eval(elseExpr) } - override fun visit(expr: UtArrayApplyForAll): Expr = expr.run { + override fun visit(expr: UtArrayApplyForAll): Expr<*> = expr.run { eval(expr.arrayExpression.select(mkInt(selectIndexStack.last().intValue()))) } } 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 4f0d7516d4..638ebb1419 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 @@ -1,9 +1,9 @@ package org.utbot.engine.pc +import com.microsoft.z3.* import org.utbot.common.WorkaroundReason import org.utbot.common.workaround -import org.utbot.engine.MAX_STRING_LENGTH_SIZE_BITS -import org.utbot.engine.TypeRegistry +import org.utbot.engine.types.TypeRegistry import org.utbot.engine.TypeStorage import org.utbot.engine.baseType import org.utbot.engine.defaultValue @@ -14,19 +14,11 @@ import org.utbot.engine.z3.convertVar import org.utbot.engine.z3.makeBV import org.utbot.engine.z3.makeFP import org.utbot.engine.z3.negate -import com.microsoft.z3.ArrayExpr -import com.microsoft.z3.BitVecExpr -import com.microsoft.z3.BoolExpr -import com.microsoft.z3.Context -import com.microsoft.z3.Expr -import com.microsoft.z3.FPSort -import com.microsoft.z3.IntExpr -import com.microsoft.z3.Model -import com.microsoft.z3.SeqExpr -import com.microsoft.z3.mkSeqNth +import org.utbot.engine.types.TypeRegistry.Companion.numberOfTypes +import org.utbot.framework.UtSettings +import org.utbot.framework.UtSettings.maxTypeNumberForEnumeration +import org.utbot.framework.UtSettings.useBitVecBasedTypeSystem import java.util.IdentityHashMap -import soot.ByteType -import soot.CharType import soot.PrimType import soot.RefType import soot.Type @@ -35,101 +27,110 @@ import soot.Type open class Z3TranslatorVisitor( private val z3Context: Context, private val typeRegistry: TypeRegistry -) : UtExpressionVisitor { +) : UtExpressionVisitor> { //thread-safe - private val expressionTranslationCache = IdentityHashMap() + private val expressionTranslationCache = IdentityHashMap>() fun evaluator(model: Model): Z3EvaluatorVisitor = Z3EvaluatorVisitor(model, this) /** * Translate [expr] from [UtExpression] to [Expr] */ - fun translate(expr: UtExpression): Expr = + fun translate(expr: UtExpression): Expr<*> = expressionTranslationCache.getOrPut(expr) { expr.accept(this) } - fun lookUpCache(expr: UtExpression): Expr? = expressionTranslationCache[expr] + fun lookUpCache(expr: UtExpression): Expr<*>? = expressionTranslationCache[expr] - fun withContext(block: Context.() -> Expr): Expr { + fun withContext(block: Context.() -> Expr<*>): Expr<*> { return z3Context.block() } - override fun visit(expr: UtArraySelectExpression): Expr = expr.run { - z3Context.mkSelect(translate(arrayExpression) as ArrayExpr, translate(index)) + override fun visit(expr: UtArraySelectExpression): Expr<*> = expr.run { + z3Context.mkSelect(translate(arrayExpression) as ArrayExpr<*, *>, translate(index).cast()) } + /** * Creates a const array filled with a fixed value */ - override fun visit(expr: UtConstArrayExpression): Expr = expr.run { + override fun visit(expr: UtConstArrayExpression): Expr<*> = expr.run { z3Context.mkConstArray(sort.indexSort.toZ3Sort(z3Context), translate(constValue)) } - override fun visit(expr: UtMkArrayExpression): Expr = expr.run { - z3Context.run { mkConst(name, sort.toZ3Sort(this)) } as ArrayExpr + override fun visit(expr: UtMkArrayExpression): Expr<*> = expr.run { + z3Context.run { mkConst(name, sort.toZ3Sort(this)) } as ArrayExpr<*, *> } - override fun visit(expr: UtArrayMultiStoreExpression): Expr = expr.run { - stores.fold(translate(initial) as ArrayExpr) { acc, (index, item) -> - z3Context.mkStore(acc, translate(index), translate(item)) + override fun visit(expr: UtArrayMultiStoreExpression): Expr<*> = expr.run { + stores.fold(translate(initial) as ArrayExpr<*, *>) { acc, (index, item) -> + z3Context.mkStore(acc, translate(index).cast(), translate(item).cast()) } } - override fun visit(expr: UtBvLiteral): Expr = expr.run { z3Context.makeBV(value, size) } + override fun visit(expr: UtBvLiteral): Expr<*> = expr.run { z3Context.makeBV(value, size) } - override fun visit(expr: UtBvConst): Expr = expr.run { z3Context.mkBVConst(name, size) } + override fun visit(expr: UtBvConst): Expr<*> = expr.run { z3Context.mkBVConst(name, size) } - override fun visit(expr: UtAddrExpression): Expr = expr.run { translate(internal) } + override fun visit(expr: UtAddrExpression): Expr<*> = expr.run { translate(internal) } - override fun visit(expr: UtFpLiteral): Expr = + override fun visit(expr: UtFpLiteral): Expr<*> = expr.run { z3Context.makeFP(value, sort.toZ3Sort(z3Context) as FPSort) } - override fun visit(expr: UtFpConst): Expr = expr.run { z3Context.mkConst(name, sort.toZ3Sort(z3Context)) } + override fun visit(expr: UtFpConst): Expr<*> = expr.run { z3Context.mkConst(name, sort.toZ3Sort(z3Context)) } - override fun visit(expr: UtOpExpression): Expr = expr.run { + override fun visit(expr: UtOpExpression): Expr<*> = expr.run { val leftResolve = translate(left.expr).z3Variable(left.type) val rightResolve = translate(right.expr).z3Variable(right.type) operator.delegate(z3Context, leftResolve, rightResolve) } - override fun visit(expr: UtTrue): Expr = expr.run { z3Context.mkBool(true) } + override fun visit(expr: UtTrue): Expr<*> = expr.run { z3Context.mkBool(true) } - override fun visit(expr: UtFalse): Expr = expr.run { z3Context.mkBool(false) } + override fun visit(expr: UtFalse): Expr<*> = expr.run { z3Context.mkBool(false) } - override fun visit(expr: UtEqExpression): Expr = expr.run { z3Context.mkEq(translate(left), translate(right)) } + override fun visit(expr: UtEqExpression): Expr<*> = expr.run { z3Context.mkEq(translate(left), translate(right)) } - override fun visit(expr: UtBoolConst): Expr = expr.run { z3Context.mkBoolConst(name) } + override fun visit(expr: UtBoolConst): Expr<*> = expr.run { z3Context.mkBoolConst(name) } - override fun visit(expr: NotBoolExpression): Expr = + override fun visit(expr: NotBoolExpression): Expr<*> = expr.run { z3Context.mkNot(translate(expr.expr) as BoolExpr) } - override fun visit(expr: UtOrBoolExpression): Expr = expr.run { + override fun visit(expr: UtOrBoolExpression): Expr<*> = expr.run { z3Context.mkOr(*exprs.map { translate(it) as BoolExpr }.toTypedArray()) } - override fun visit(expr: UtAndBoolExpression): Expr = expr.run { + override fun visit(expr: UtAndBoolExpression): Expr<*> = expr.run { z3Context.mkAnd(*exprs.map { translate(it) as BoolExpr }.toTypedArray()) } - override fun visit(expr: UtAddNoOverflowExpression): Expr = expr.run { + override fun visit(expr: UtAddNoOverflowExpression): Expr<*> = expr.run { z3Context.mkBVAddNoOverflow(translate(expr.left) as BitVecExpr, translate(expr.right) as BitVecExpr, true) } - override fun visit(expr: UtSubNoOverflowExpression): Expr = expr.run { + override fun visit(expr: UtSubNoOverflowExpression): Expr<*> = expr.run { // For some reason mkBVSubNoOverflow does not take "signed" as an argument, yet still works for signed integers z3Context.mkBVSubNoOverflow(translate(expr.left) as BitVecExpr, translate(expr.right) as BitVecExpr) // , true) } - override fun visit(expr: UtNegExpression): Expr = expr.run { + override fun visit(expr: UtMulNoOverflowExpression): Expr<*> = expr.run { + z3Context.mkBVMulNoOverflow(translate(expr.left) as BitVecExpr, translate(expr.right) as BitVecExpr , true) + } + + override fun visit(expr: UtNegExpression): Expr<*> = expr.run { negate(z3Context, translate(variable.expr).z3Variable(variable.type)) } - override fun visit(expr: UtCastExpression): Expr = expr.run { + override fun visit(expr: UtBvNotExpression): Expr<*> = expr.run { + z3Context.mkBVNot(translate(variable.expr) as BitVecExpr) + } + + override fun visit(expr: UtCastExpression): Expr<*> = expr.run { val z3var = translate(variable.expr).z3Variable(variable.type) z3Context.convertVar(z3var, type).expr } - override fun visit(expr: UtBoolOpExpression): Expr = expr.run { + override fun visit(expr: UtBoolOpExpression): Expr<*> = expr.run { val leftResolve = translate(left.expr).z3Variable(left.type) val rightResolve = translate(right.expr).z3Variable(right.type) operator.delegate(z3Context, leftResolve, rightResolve) @@ -153,9 +154,9 @@ open class Z3TranslatorVisitor( * @param expr the type expression * @return the type expression translated into z3 assertions * @see UtIsExpression - * @see MAX_TYPE_NUMBER_FOR_ENUMERATION + * @see UtSettings.maxTypeNumberForEnumeration */ - override fun visit(expr: UtIsExpression): Expr = expr.run { + override fun visit(expr: UtIsExpression): Expr<*> = expr.run { val symNumDimensions = translate(typeRegistry.symNumDimensions(addr)) as BitVecExpr val symTypeId = translate(typeRegistry.symTypeId(addr)) as BitVecExpr @@ -164,33 +165,8 @@ open class Z3TranslatorVisitor( // TODO remove it JIRA:1321 val filteredPossibleTypes = workaround(WorkaroundReason.HACK) { typeStorage.filterInappropriateTypes() } - // add constraints for typeId - if (typeStorage.possibleConcreteTypes.size < MAX_TYPE_NUMBER_FOR_ENUMERATION) { - val symType = translate(typeRegistry.symTypeId(addr)) - val possibleBaseTypes = filteredPossibleTypes.map { it.baseType } - - val typeConstraint = z3Context.mkOr( - *possibleBaseTypes - .map { z3Context.mkEq(z3Context.mkBV(typeRegistry.findTypeId(it), Int.SIZE_BITS), symType) } - .toTypedArray() - ) - - constraints += typeConstraint - } else { - val shiftedExpression = z3Context.mkBVSHL( - z3Context.mkZeroExt(numberOfTypes - 1, z3Context.mkBV(1, 1)), - z3Context.mkZeroExt(numberOfTypes - Int.SIZE_BITS, symTypeId) - ) - - val bitVecString = typeRegistry.constructBitVecString(filteredPossibleTypes) - val possibleTypesBitVector = z3Context.mkBV(bitVecString, numberOfTypes) - - val typeConstraint = z3Context.mkEq( - z3Context.mkBVAND(shiftedExpression, z3Context.mkBVNot(possibleTypesBitVector)), - z3Context.mkBV(0, numberOfTypes) - ) - - constraints += typeConstraint + if (filteredPossibleTypes.size > UtSettings.maxNumberOfTypesToEncode) { + return@run z3Context.mkTrue() } val exprBaseType = expr.type.baseType @@ -202,6 +178,8 @@ open class Z3TranslatorVisitor( z3Context.mkEq(symNumDimensions, numDimensions) } + constraints += encodePossibleTypes(symTypeId, filteredPossibleTypes) + z3Context.mkAnd(*constraints.toTypedArray()) } @@ -223,27 +201,26 @@ open class Z3TranslatorVisitor( } - override fun visit(expr: UtGenericExpression): Expr = expr.run { + override fun visit(expr: UtGenericExpression): Expr<*> = expr.run { val constraints = mutableListOf() for (i in types.indices) { val symType = translate(typeRegistry.genericTypeId(addr, i)) + val genericNumDimensions = translate(typeRegistry.genericNumDimensions(addr, i)) as BitVecExpr - if (types[i].leastCommonType.isJavaLangObject()) { - continue - } + val possibleConcreteTypes = types[i].possibleConcreteTypes + val leastCommonType = types[i].leastCommonType - val possibleBaseTypes = types[i].possibleConcreteTypes.map { it.baseType } + val numDimensions = z3Context.mkBV(leastCommonType.numDimensions, Int.SIZE_BITS) - val typeConstraint = z3Context.mkOr( - *possibleBaseTypes.map { - z3Context.mkEq( - z3Context.mkBV(typeRegistry.findTypeId(it), Int.SIZE_BITS), - symType - ) - }.toTypedArray() - ) + if (possibleConcreteTypes.size > UtSettings.maxNumberOfTypesToEncode) continue + + constraints += if (leastCommonType.isJavaLangObject()) { + z3Context.mkBVSGE(genericNumDimensions, numDimensions) + } else { + z3Context.mkEq(genericNumDimensions, numDimensions) + } - constraints += typeConstraint + constraints += encodePossibleTypes(symType, possibleConcreteTypes) } z3Context.mkOr( @@ -252,7 +229,36 @@ open class Z3TranslatorVisitor( ) } - override fun visit(expr: UtIsGenericTypeExpression): Expr = expr.run { + private fun encodePossibleTypes(symType: Expr<*>, possibleConcreteTypes: Collection): BoolExpr { + val possibleBaseTypes = possibleConcreteTypes.map { it.baseType } + + return if (!useBitVecBasedTypeSystem || possibleConcreteTypes.size < maxTypeNumberForEnumeration) { + z3Context.mkOr( + *possibleBaseTypes + .map { + val typeId = typeRegistry.findTypeId(it) + val typeIdBv = z3Context.mkBV(typeId, Int.SIZE_BITS) + z3Context.mkEq(typeIdBv, symType) + } + .toTypedArray() + ) + } else { + val shiftedExpression = z3Context.mkBVSHL( + z3Context.mkZeroExt(numberOfTypes - 1, z3Context.mkBV(1, 1)), + z3Context.mkZeroExt(numberOfTypes - Int.SIZE_BITS, symType as BitVecExpr) + ) + + val bitVecString = typeRegistry.constructBitVecString(possibleBaseTypes) + val possibleTypesBitVector = z3Context.mkBV(bitVecString, numberOfTypes) + + z3Context.mkEq( + z3Context.mkBVAND(shiftedExpression, z3Context.mkBVNot(possibleTypesBitVector)), + z3Context.mkBV(0, numberOfTypes) + ) + } + } + + override fun visit(expr: UtIsGenericTypeExpression): Expr<*> = expr.run { val symType = translate(typeRegistry.symTypeId(addr)) val symNumDimensions = translate(typeRegistry.symNumDimensions(addr)) @@ -274,7 +280,7 @@ open class Z3TranslatorVisitor( z3Context.mkAnd(typeConstraint, dimensionsConstraint) } - override fun visit(expr: UtEqGenericTypeParametersExpression): Expr = expr.run { + override fun visit(expr: UtEqGenericTypeParametersExpression): Expr<*> = expr.run { val constraints = mutableListOf() for ((i, j) in indexMapping) { val firstSymType = translate(typeRegistry.genericTypeId(firstAddr, i)) @@ -288,15 +294,15 @@ open class Z3TranslatorVisitor( z3Context.mkAnd(*constraints.toTypedArray()) } - override fun visit(expr: UtInstanceOfExpression): Expr = + override fun visit(expr: UtInstanceOfExpression): Expr<*> = expr.run { translate(expr.constraint) } - override fun visit(expr: UtIteExpression): Expr = + override fun visit(expr: UtIteExpression): Expr<*> = expr.run { z3Context.mkITE(translate(condition) as BoolExpr, translate(thenExpr), translate(elseExpr)) } - override fun visit(expr: UtMkTermArrayExpression): Expr = expr.run { + override fun visit(expr: UtMkTermArrayExpression): Expr<*> = expr.run { z3Context.run { - val ourArray = translate(expr.array) as ArrayExpr + val ourArray = translate(expr.array) as ArrayExpr<*, *> val arraySort = expr.array.sort val defaultValue = expr.defaultValue ?: arraySort.itemSort.defaultValue val translated = translate(defaultValue) @@ -305,34 +311,6 @@ open class Z3TranslatorVisitor( } } - override fun visit(expr: UtStringConst): Expr = expr.run { z3Context.mkConst(name, sort.toZ3Sort(z3Context)) } - - override fun visit(expr: UtConcatExpression): Expr = - expr.run { z3Context.mkConcat(*parts.map { translate(it) as SeqExpr }.toTypedArray()) } - - override fun visit(expr: UtConvertToString): Expr = expr.run { - when (expression) { - is UtBvLiteral -> z3Context.mkString(expression.value.toString()) - else -> { - val intValue = z3Context.mkBV2Int(translate(expression) as BitVecExpr, true) - z3Context.intToString(intValue) - } - } - } - - override fun visit(expr: UtStringToInt): Expr = expr.run { - val intValue = z3Context.stringToInt(translate(expression)) - z3Context.mkInt2BV(size, intValue) - } - - override fun visit(expr: UtStringLength): Expr = expr.run { - z3Context.mkInt2BV(MAX_STRING_LENGTH_SIZE_BITS, z3Context.mkLength(translate(string) as SeqExpr)) - } - - override fun visit(expr: UtStringPositiveLength): Expr = expr.run { - z3Context.mkGe(z3Context.mkLength(translate(string) as SeqExpr), z3Context.mkInt(0)) - } - private fun Context.mkBV2Int(expr: UtExpression): IntExpr = if (expr is UtBvLiteral) { mkInt(expr.value as Long) @@ -340,83 +318,19 @@ open class Z3TranslatorVisitor( mkBV2Int(translate(expr) as BitVecExpr, true) } + override fun visit(expr: UtArrayInsert): Expr<*> = error("translate of UtArrayInsert expression") - override fun visit(expr: UtStringCharAt): Expr = expr.run { - val charAtExpr = z3Context.mkSeqNth(translate(string) as SeqExpr, z3Context.mkBV2Int(index)) - val z3var = charAtExpr.z3Variable(ByteType.v()) - z3Context.convertVar(z3var, CharType.v()).expr - } - - override fun visit(expr: UtStringEq): Expr = expr.run { z3Context.mkEq(translate(left), translate(right)) } - - override fun visit(expr: UtSubstringExpression): Expr = expr.run { - z3Context.mkExtract( - translate(string) as SeqExpr, - z3Context.mkBV2Int(beginIndex), - z3Context.mkBV2Int(length) - ) - } - - override fun visit(expr: UtReplaceExpression): Expr = expr.run { - workaround(WorkaroundReason.HACK) { // mkReplace replaces first occasion only - z3Context.mkReplace( - translate(string) as SeqExpr, - translate(regex) as SeqExpr, - translate(replacement) as SeqExpr - ) - } - } - - // Attention, prefix is a first argument! - override fun visit(expr: UtStartsWithExpression): Expr = expr.run { - z3Context.mkPrefixOf(translate(prefix) as SeqExpr, translate(string) as SeqExpr) - } - - // Attention, suffix is a first argument! - override fun visit(expr: UtEndsWithExpression): Expr = expr.run { - z3Context.mkSuffixOf(translate(suffix) as SeqExpr, translate(string) as SeqExpr) - } - - override fun visit(expr: UtIndexOfExpression): Expr = expr.run { - z3Context.mkInt2BV( - MAX_STRING_LENGTH_SIZE_BITS, - z3Context.mkIndexOf(translate(string) as SeqExpr, translate(substring) as SeqExpr, z3Context.mkInt(0)) - ) - } - - override fun visit(expr: UtContainsExpression): Expr = expr.run { - z3Context.mkGe( - z3Context.mkIndexOf(translate(string) as SeqExpr, translate(substring) as SeqExpr, z3Context.mkInt(0)), - z3Context.mkInt(0) - ) - } - - override fun visit(expr: UtToStringExpression): Expr = expr.run { - z3Context.mkITE(translate(isNull) as BoolExpr, z3Context.mkString("null"), translate(notNullExpr)) - - } - - override fun visit(expr: UtSeqLiteral): Expr = expr.run { z3Context.mkString(value) } - - companion object { - const val MAX_TYPE_NUMBER_FOR_ENUMERATION = 64 - } - - override fun visit(expr: UtArrayInsert): Expr = error("translate of UtArrayInsert expression") - - override fun visit(expr: UtArrayInsertRange): Expr = error("translate of UtArrayInsertRange expression") + override fun visit(expr: UtArrayInsertRange): Expr<*> = error("translate of UtArrayInsertRange expression") - override fun visit(expr: UtArrayRemove): Expr = error("translate of UtArrayRemove expression") + override fun visit(expr: UtArrayRemove): Expr<*> = error("translate of UtArrayRemove expression") - override fun visit(expr: UtArrayRemoveRange): Expr = error("translate of UtArrayRemoveRange expression") + override fun visit(expr: UtArrayRemoveRange): Expr<*> = error("translate of UtArrayRemoveRange expression") - override fun visit(expr: UtArraySetRange): Expr = error("translate of UtArraySetRange expression") + override fun visit(expr: UtArraySetRange): Expr<*> = error("translate of UtArraySetRange expression") - override fun visit(expr: UtArrayShiftIndexes): Expr = error("translate of UtArrayShiftIndexes expression") + override fun visit(expr: UtArrayShiftIndexes): Expr<*> = error("translate of UtArrayShiftIndexes expression") - override fun visit(expr: UtArrayApplyForAll): Expr = error("translate of UtArrayApplyForAll expression") + override fun visit(expr: UtArrayApplyForAll): Expr<*> = error("translate of UtArrayApplyForAll expression") - override fun visit(expr: UtStringToArray): Expr = error("translate of UtStringToArray expression") - override fun visit(expr: UtArrayToString): Expr = error("translate of UtArrayToString expression") } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/BFSSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/BFSSelector.kt index 6feb62b352..f28d2449c0 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/BFSSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/BFSSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy 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 0e400f527a..945d903945 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,6 +1,6 @@ package org.utbot.engine.selectors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.isPreconditionCheckMethod import org.utbot.engine.pathLogger import org.utbot.engine.pc.UtSolver @@ -80,7 +80,7 @@ abstract class BasePathSelector( pathLogger.trace { "poll next state (lastStatus=${state.solver.lastStatus}): " + state.prettifiedPathLog() } current = null - if (choosingStrategy.shouldDrop(state) || checkUnsatIfFork(state)) { + if (choosingStrategy.shouldDrop(state) || (!UtSettings.disableUnsatChecking && checkUnsatIfFork(state))) { state.close() continue } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/DFSSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/DFSSelector.kt index 9e1eba3c1b..92ad407d12 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/DFSSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/DFSSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/InterleavedSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/InterleavedSelector.kt index b46f933d66..401d9094a7 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/InterleavedSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/InterleavedSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState /** * Retrieves states from different pathSelectors in rotation. diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/MLSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/MLSelector.kt index 1e4b17de0a..78f12dceb1 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/MLSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/MLSelector.kt @@ -2,7 +2,7 @@ package org.utbot.engine.selectors import org.utbot.analytics.EngineAnalyticsContext import org.utbot.analytics.Predictors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import org.utbot.engine.selectors.nurs.GreedySearch import org.utbot.engine.selectors.strategies.ChoosingStrategy diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelector.kt index 9e98743b20..9406d03b78 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.pc.UtSolverStatusKind /** 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 b529e6d456..5516d5480b 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 @@ -4,7 +4,7 @@ package org.utbot.engine.selectors import org.utbot.analytics.EngineAnalyticsContext import org.utbot.engine.InterProceduralUnitGraph -import org.utbot.engine.TypeRegistry +import org.utbot.engine.types.TypeRegistry import org.utbot.engine.selectors.StrategyOption.DISTANCE import org.utbot.engine.selectors.StrategyOption.VISIT_COUNTING import org.utbot.engine.selectors.nurs.CPInstSelector diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathsTree.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathsTree.kt index f908eb4fdb..7618877531 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathsTree.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathsTree.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import java.util.NoSuchElementException import kotlin.random.Random diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/RandomPathSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/RandomPathSelector.kt index 844dcce9c8..64f48f544b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/RandomPathSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/RandomPathSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy import kotlin.random.Random diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/RandomSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/RandomSelector.kt index 76245cb4a7..c27156f1ad 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/RandomSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/RandomSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy import kotlin.random.Random 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 index 261dfc2fc2..e39e7456f0 100644 --- 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 @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StatementsStatistics import org.utbot.engine.selectors.strategies.StoppingStrategy diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/CoveredNewSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/CoveredNewSelector.kt index 01a98b4e82..5e3e4b5ffc 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/CoveredNewSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/CoveredNewSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.DistanceStatistics import org.utbot.engine.selectors.strategies.StoppingStrategy diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/DepthSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/DepthSelector.kt index 5962db8768..5885df02c7 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/DepthSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/DepthSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy 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 index 7c5e27aaf7..78ed6a93c0 100644 --- 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 @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy 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 index b00d67e871..1fe1985489 100644 --- 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 @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.BasePathSelector import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/InheritorsSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/InheritorsSelector.kt index 8550c3c46e..317a436736 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/InheritorsSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/InheritorsSelector.kt @@ -1,7 +1,7 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState -import org.utbot.engine.TypeRegistry +import org.utbot.engine.state.ExecutionState +import org.utbot.engine.types.TypeRegistry import org.utbot.engine.selectors.strategies.DistanceStatistics import org.utbot.engine.selectors.strategies.StoppingStrategy import kotlin.math.pow diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/MinimalDistanceToUncovered.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/MinimalDistanceToUncovered.kt index 3f56367ead..a3ab7f8f58 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/MinimalDistanceToUncovered.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/MinimalDistanceToUncovered.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.DistanceStatistics import org.utbot.engine.selectors.strategies.StoppingStrategy diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/NeuroSatSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/NeuroSatSelector.kt index 3736227a06..596c91492d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/NeuroSatSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/NeuroSatSelector.kt @@ -1,7 +1,7 @@ package org.utbot.engine.selectors.nurs import org.utbot.analytics.Predictors -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/NonUniformRandomSearch.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/NonUniformRandomSearch.kt index 4e2d741240..6c10f233a0 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/NonUniformRandomSearch.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/NonUniformRandomSearch.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.BasePathSelector import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/RPSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/RPSelector.kt index d7e06e2d37..b201104802 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/RPSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/RPSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy import kotlin.math.pow 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 index a17e94a6a2..f6a43fa0a5 100644 --- 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 @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy import org.utbot.engine.selectors.strategies.SubpathStatistics diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/VisitCountingSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/VisitCountingSelector.kt index 6e69e29dbb..e455d2d124 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/VisitCountingSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/VisitCountingSelector.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.nurs -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.selectors.strategies.EdgeVisitCountingStatistics import org.utbot.engine.selectors.strategies.StoppingStrategy diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/ChoosingStrategy.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/ChoosingStrategy.kt index f52de7e622..0d08a7f0dd 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/ChoosingStrategy.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/ChoosingStrategy.kt @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.strategies -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph /** 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 448bdc82b3..9c1cfc16c1 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 @@ -1,7 +1,7 @@ package org.utbot.engine.selectors.strategies -import org.utbot.engine.Edge -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.Edge +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import org.utbot.engine.isReturn import org.utbot.engine.pathLogger @@ -9,6 +9,7 @@ import org.utbot.engine.stmts import kotlin.math.min import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf +import org.utbot.framework.UtSettings.enableLoggingForDroppedStates import soot.jimple.Stmt import soot.toolkits.graph.ExceptionalUnitGraph @@ -43,8 +44,11 @@ class DistanceStatistics( 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()}" + if (enableLoggingForDroppedStates) { + pathLogger.debug { + val lastStatus = state.solver.lastStatus + "Dropping state (lastStatus=$lastStatus) by the distance statistics. MD5: ${state.md5()}" + } } } 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 1a0ad21348..8813973475 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 @@ -1,9 +1,10 @@ package org.utbot.engine.selectors.strategies -import org.utbot.engine.Edge -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.Edge +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import org.utbot.engine.pathLogger +import org.utbot.framework.UtSettings.enableLoggingForDroppedStates import soot.jimple.Stmt import soot.jimple.internal.JReturnStmt import soot.jimple.internal.JReturnVoidStmt @@ -35,9 +36,12 @@ class EdgeVisitCountingStatistics( 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()}" + if (enableLoggingForDroppedStates) { + pathLogger.debug { + val lastStatus = state.solver.lastStatus + val md5 = state.md5() + "Dropping state (lastStatus=$lastStatus) by the edge visit counting statistics. MD5: $md5" + } } } 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 index 315673e042..88163e49a1 100644 --- 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 @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.strategies -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph class GeneratedTestCountingStatistics( 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 fb603aad14..1cf9c38ec7 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 @@ -2,9 +2,9 @@ package org.utbot.engine.selectors.strategies import mu.KotlinLogging import org.utbot.common.FileUtil.createNewFileWithParentDirectories -import org.utbot.engine.CALL_DECISION_NUM -import org.utbot.engine.Edge -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.CALL_DECISION_NUM +import org.utbot.engine.state.Edge +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import org.utbot.engine.isLibraryNonOverriddenClass import org.utbot.engine.isReturn @@ -17,7 +17,6 @@ import soot.toolkits.graph.ExceptionalUnitGraph import java.awt.Toolkit import java.awt.datatransfer.StringSelection import java.io.FileWriter -import java.nio.file.Files import java.nio.file.Paths import org.utbot.common.FileUtil 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 index 375462191c..88d140b6ab 100644 --- 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 @@ -1,6 +1,6 @@ package org.utbot.engine.selectors.strategies -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import soot.SootMethod import soot.jimple.Stmt 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 f17d2b6750..a520afa1d5 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 @@ -1,7 +1,7 @@ package org.utbot.engine.selectors.strategies -import org.utbot.engine.Edge -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.Edge +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import org.utbot.engine.pathLogger 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 index 0110cf03c9..2ecefd06e3 100644 --- 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 @@ -1,8 +1,8 @@ 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.state.CALL_DECISION_NUM +import org.utbot.engine.state.Edge +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import org.utbot.framework.UtSettings import kotlin.math.pow diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/TraverseGraphStatistics.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/TraverseGraphStatistics.kt index 7ec9abedc0..2b4feac95e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/TraverseGraphStatistics.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/TraverseGraphStatistics.kt @@ -1,7 +1,7 @@ package org.utbot.engine.selectors.strategies -import org.utbot.engine.Edge -import org.utbot.engine.ExecutionState +import org.utbot.engine.state.Edge +import org.utbot.engine.state.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import soot.jimple.Stmt import soot.toolkits.graph.ExceptionalUnitGraph diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/simplificators/MemoryUpdateSimplificator.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/simplificators/MemoryUpdateSimplificator.kt new file mode 100644 index 0000000000..cbf4de006c --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/simplificators/MemoryUpdateSimplificator.kt @@ -0,0 +1,195 @@ +package org.utbot.engine.simplificators + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.PersistentSet +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.collections.immutable.toPersistentSet +import org.utbot.engine.Concrete +import org.utbot.engine.InstanceFieldReadOperation +import org.utbot.engine.MemoryChunkDescriptor +import org.utbot.engine.MemoryUpdate +import org.utbot.engine.MockInfoEnriched +import org.utbot.engine.ObjectValue +import org.utbot.engine.StaticFieldMemoryUpdateInfo +import org.utbot.engine.SymbolicValue +import org.utbot.engine.TypeStorage +import org.utbot.engine.UtMockInfo +import org.utbot.engine.UtNamedStore +import org.utbot.engine.pc.Simplificator +import org.utbot.engine.pc.UtAddrExpression +import org.utbot.engine.pc.UtExpression +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId +import soot.ArrayType +import soot.SootField + +typealias StoresType = PersistentList +typealias TouchedChunkDescriptorsType = PersistentSet +typealias ConcreteType = PersistentMap +typealias MockInfosType = PersistentList +typealias StaticInstanceStorageType = PersistentMap +typealias InitializedStaticFieldsType = PersistentSet +typealias StaticFieldsUpdatesType = PersistentList +typealias MeaningfulStaticFieldsType = PersistentSet +typealias FieldValuesType = PersistentMap> +typealias AddrToArrayTypeType = PersistentMap +typealias AddrToGenericTypeInfo = PersistentList>> +typealias AddrToMockInfoType = PersistentMap +typealias VisitedValuesType = PersistentList +typealias TouchedAddressesType = PersistentList +typealias ClassIdToClearStaticsType = ClassId? +typealias InstanceFieldReadsType = PersistentSet +typealias SpeculativelyNotNullAddressesType = PersistentList +typealias SymbolicEnumValuesType = PersistentList +typealias TaintArrayUpdateType = PersistentList> + +class MemoryUpdateSimplificator( + private val simplificator: Simplificator +) : CachingSimplificatorAdapter() { + override fun simplifyImpl(expression: MemoryUpdate): MemoryUpdate = with(expression) { + val stores = simplifyStores(stores) + val touchedChunkDescriptors = simplifyTouchedChunkDescriptors(touchedChunkDescriptors) + val concrete = simplifyConcrete(concrete) + val mockInfos = simplifyMockInfos(mockInfos) + val staticInstanceStorage = simplifyStaticInstanceStorage(staticInstanceStorage) + val initializedStaticFields = simplifyInitializedStaticFields(initializedStaticFields) + val staticFieldsUpdates = simplifyStaticFieldsUpdates(staticFieldsUpdates) + val meaningfulStaticFields = simplifyMeaningfulStaticFields(meaningfulStaticFields) + val fieldValues = simplifyFieldValues(fieldValues) + val addrToArrayType = simplifyAddrToArrayType(addrToArrayType) + val genericTypeStorageByAddr = simplifyGenericTypeStorageByAddr(genericTypeStorageByAddr) + val addrToMockInfo = simplifyAddrToMockInfo(addrToMockInfo) + val visitedValues = simplifyVisitedValues(visitedValues) + val touchedAddresses = simplifyTouchedAddresses(touchedAddresses) + val classIdToClearStatics = simplifyClassIdToClearStatics(classIdToClearStatics) + val instanceFieldReads = simplifyInstanceFieldReads(instanceFieldReads) + val speculativelyNotNullAddresses = + simplifySpeculativelyNotNullAddresses(speculativelyNotNullAddresses) + val symbolicEnumValues = simplifyEnumValues(symbolicEnumValues) + val taintArrayUpdate = simplifyTaintArrayUpdate(taintArrayUpdate) + return MemoryUpdate( + stores, + touchedChunkDescriptors, + concrete, + mockInfos, + staticInstanceStorage, + initializedStaticFields, + staticFieldsUpdates, + meaningfulStaticFields, + fieldValues, + addrToArrayType, + genericTypeStorageByAddr, + addrToMockInfo, + visitedValues, + touchedAddresses, + classIdToClearStatics, + instanceFieldReads, + speculativelyNotNullAddresses, + taintArrayUpdate, + symbolicEnumValues + ) + } + + private fun simplifyStores(stores: StoresType): StoresType = + stores + .mutate { prevStores -> + prevStores.replaceAll { store -> + store.copy( + index = store.index.accept(simplificator), + value = store.value.accept(simplificator) + ) + } + } + + private fun simplifyTouchedChunkDescriptors(touchedChunkDescriptors: TouchedChunkDescriptorsType): TouchedChunkDescriptorsType = + touchedChunkDescriptors + + private fun simplifyConcrete(concrete: ConcreteType): ConcreteType = + concrete + .mapKeys { (k, _) -> k.accept(simplificator) as UtAddrExpression } + .toPersistentMap() + + private fun simplifyMockInfos(mockInfos: MockInfosType): MockInfosType = + mockInfos.mutate { prevMockInfos -> + prevMockInfos.replaceAll { + with(simplificator) { + simplifyMockInfoEnriched(it) + } + } + } + + + private fun simplifyStaticInstanceStorage(staticInstanceStorage: StaticInstanceStorageType): StaticInstanceStorageType = + staticInstanceStorage.mutate { prevStorage -> + prevStorage.replaceAll { _, v -> with(simplificator) { simplifySymbolicValue(v) as ObjectValue } } + } + + private fun simplifyInitializedStaticFields(initializedStaticFields: InitializedStaticFieldsType): InitializedStaticFieldsType = + initializedStaticFields + + private fun simplifyStaticFieldsUpdates(staticFieldsUpdates: StaticFieldsUpdatesType): StaticFieldsUpdatesType = + staticFieldsUpdates.mutate { prevUpdates -> + prevUpdates.replaceAll { with(simplificator) { staticFieldMemoryUpdateInfo(it) } } + } + + private fun simplifyMeaningfulStaticFields(meaningfulStaticFields: MeaningfulStaticFieldsType): MeaningfulStaticFieldsType = + meaningfulStaticFields + + private fun simplifyFieldValues(fieldValues: FieldValuesType): FieldValuesType = fieldValues + + private fun simplifyAddrToArrayType(addrToArrayType: AddrToArrayTypeType): AddrToArrayTypeType = + addrToArrayType + .mapKeys { (k, _) -> k.accept(simplificator) as UtAddrExpression } + .toPersistentMap() + + private fun simplifyGenericTypeStorageByAddr(genericTypeStorageByAddr: AddrToGenericTypeInfo): AddrToGenericTypeInfo = + genericTypeStorageByAddr + .map { (k, v) -> k.accept(simplificator) as UtAddrExpression to v } + .toPersistentList() + + private fun simplifyAddrToMockInfo(addrToMockInfo: AddrToMockInfoType): AddrToMockInfoType = + addrToMockInfo + .mapKeys { (k, _) -> k.accept(simplificator) as UtAddrExpression } + .toPersistentMap() + + private fun simplifyVisitedValues(visitedValues: VisitedValuesType): VisitedValuesType = + visitedValues.mutate { prevValues -> + prevValues.replaceAll { it.accept(simplificator) as UtAddrExpression } + } + + private fun simplifyTouchedAddresses(touchedAddresses: TouchedAddressesType): TouchedAddressesType = + touchedAddresses.mutate { prevAddresses -> + prevAddresses.replaceAll { it.accept(simplificator) as UtAddrExpression } + } + + private fun simplifyClassIdToClearStatics(classIdToClearStatics: ClassIdToClearStaticsType): ClassIdToClearStaticsType = + classIdToClearStatics + + private fun simplifyInstanceFieldReads(instanceFieldReads: InstanceFieldReadsType): InstanceFieldReadsType = + instanceFieldReads + .map { it.copy(addr = it.addr.accept(simplificator) as UtAddrExpression) } + .toPersistentSet() + + private fun simplifySpeculativelyNotNullAddresses(speculativelyNotNullAddresses: SpeculativelyNotNullAddressesType): SpeculativelyNotNullAddressesType = + speculativelyNotNullAddresses.mutate { prevAddresses -> + prevAddresses.replaceAll { it.accept(simplificator) as UtAddrExpression } + } + + private fun simplifyEnumValues(symbolicEnumValues: SymbolicEnumValuesType): SymbolicEnumValuesType = + symbolicEnumValues.mutate { values -> + values.replaceAll { with(simplificator) { simplifySymbolicValue(it) as ObjectValue } } + } + + private fun simplifyTaintArrayUpdate(taintArrayUpdate: TaintArrayUpdateType): TaintArrayUpdateType = + taintArrayUpdate.mutate { values -> + values.replaceAll { (addr, expr) -> + val simplifiedAddr = addr.accept(simplificator) as UtAddrExpression + val simplifiedExpr = expr.accept(simplificator) + + simplifiedAddr to simplifiedExpr + } + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/simplificators/SimplificatorAdapter.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/simplificators/SimplificatorAdapter.kt new file mode 100644 index 0000000000..5faf5b5b32 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/simplificators/SimplificatorAdapter.kt @@ -0,0 +1,18 @@ +package org.utbot.engine.simplificators + +import java.util.IdentityHashMap + + +interface SimplificatorAdapter { + fun simplify(expression: T): T +} + +abstract class CachingSimplificatorAdapter : SimplificatorAdapter { + private val cache: IdentityHashMap = IdentityHashMap() + + final override fun simplify(expression: T): T = + cache.getOrPut(expression) { simplifyImpl(expression) } + + protected abstract fun simplifyImpl(expression: T): T +} + diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/simplificators/Util.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/simplificators/Util.kt new file mode 100644 index 0000000000..8403bc3c8b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/simplificators/Util.kt @@ -0,0 +1,68 @@ +package org.utbot.engine.simplificators + +import org.utbot.engine.ArrayValue +import org.utbot.engine.MockInfoEnriched +import org.utbot.engine.ObjectValue +import org.utbot.engine.PrimitiveValue +import org.utbot.engine.StaticFieldMemoryUpdateInfo +import org.utbot.engine.SymbolicValue +import org.utbot.engine.UtFieldMockInfo +import org.utbot.engine.UtMockInfo +import org.utbot.engine.UtNewInstanceMockInfo +import org.utbot.engine.UtObjectMockInfo +import org.utbot.engine.UtStaticMethodMockInfo +import org.utbot.engine.UtStaticObjectMockInfo +import org.utbot.engine.pc.Simplificator +import org.utbot.engine.pc.UtAddrExpression +import org.utbot.engine.symbolic.SymbolicStateUpdate + +context(Simplificator) +fun staticFieldMemoryUpdateInfo(staticFieldMemoryUpdateInfo: StaticFieldMemoryUpdateInfo) = + with(staticFieldMemoryUpdateInfo) { + copy(value = simplifySymbolicValue(value)) + } + +context(Simplificator) +fun simplifyMockInfoEnriched(mockInfoEnriched: MockInfoEnriched): MockInfoEnriched { + val mockInfo: UtMockInfo = simplifyUtMockInfo(mockInfoEnriched.mockInfo) + val executables = mockInfoEnriched.executables.mapValues { (_, mockExecutableInstances) -> + mockExecutableInstances.map { mockExecutableInstance -> + with(mockExecutableInstance) { + val symbolicValue = simplifySymbolicValue(value) + copy(value = symbolicValue) + } + } + } + + return MockInfoEnriched(mockInfo, executables) +} + +context(Simplificator) +fun simplifyUtMockInfo(mockInfo: UtMockInfo): UtMockInfo = + with(mockInfo) { + when (this) { + is UtFieldMockInfo -> copy(ownerAddr = ownerAddr?.accept(this@Simplificator) as UtAddrExpression?) + is UtNewInstanceMockInfo -> copy(addr = addr.accept(this@Simplificator) as UtAddrExpression) + is UtObjectMockInfo -> copy(addr = addr.accept(this@Simplificator) as UtAddrExpression) + is UtStaticMethodMockInfo -> copy(addr = addr.accept(this@Simplificator) as UtAddrExpression) + is UtStaticObjectMockInfo -> copy(addr = addr.accept(this@Simplificator) as UtAddrExpression) + } + } + +context(Simplificator) +fun simplifySymbolicValue(value: SymbolicValue): SymbolicValue = + with(value) { + when (this) { + is PrimitiveValue -> copy(expr = expr.accept(this@Simplificator)) + is ArrayValue -> copy(addr = addr.accept(this@Simplificator) as UtAddrExpression) + is ObjectValue -> copy(addr = addr.accept(this@Simplificator) as UtAddrExpression) + } + } + + +context(MemoryUpdateSimplificator) +fun simplifySymbolicStateUpdate(update: SymbolicStateUpdate) = + with(update) { + val memoryUpdates = simplify(memoryUpdates) + copy(memoryUpdates = memoryUpdates) + } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/state/ExecutionStackElement.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/state/ExecutionStackElement.kt new file mode 100644 index 0000000000..378da0f7f0 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/state/ExecutionStackElement.kt @@ -0,0 +1,52 @@ +package org.utbot.engine.state + +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentHashMapOf +import org.utbot.engine.LocalMemoryUpdate +import org.utbot.engine.LocalVariable +import org.utbot.engine.Parameter +import org.utbot.engine.SymbolicValue +import org.utbot.engine.update +import soot.SootMethod +import soot.jimple.Stmt + +/** + * The stack element of the [ExecutionState]. + * Contains properties, that are suitable for specified method in call stack. + * + * @param doesntThrow if true, then engine should drop states with throwing exceptions. + * @param localVariableMemory the local memory associated with the current stack element. + */ +data class ExecutionStackElement( + val caller: Stmt?, + val localVariableMemory: LocalVariableMemory = LocalVariableMemory(), + val parameters: MutableList = mutableListOf(), + val inputArguments: ArrayDeque = ArrayDeque(), + val doesntThrow: Boolean = false, + val method: SootMethod, +) { + fun update(memoryUpdate: LocalMemoryUpdate, doesntThrow: Boolean = this.doesntThrow) = + this.copy(localVariableMemory = localVariableMemory.update(memoryUpdate), doesntThrow = doesntThrow) +} + +/** + * Represents a memory associated with a certain method call. For now consists only of local variables mapping. + * TODO: think on other fields later: [#339](https://github.com/UnitTestBot/UTBotJava/issues/339) [#340](https://github.com/UnitTestBot/UTBotJava/issues/340) + * + * @param [locals] represents a mapping from [LocalVariable]s of a specific method call to [SymbolicValue]s. + */ +data class LocalVariableMemory( + private val locals: PersistentMap = persistentHashMapOf() +) { + fun memoryForNestedMethod(): LocalVariableMemory = this.copy(locals = persistentHashMapOf()) + + fun update(update: LocalMemoryUpdate): LocalVariableMemory = this.copy(locals = locals.update(update.locals)) + + /** + * Returns local variable value. + */ + fun local(variable: LocalVariable): SymbolicValue? = locals[variable] + + val localValues: Set + get() = locals.values.toSet() +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/state/ExecutionState.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/state/ExecutionState.kt new file mode 100644 index 0000000000..6bb6f88f80 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/state/ExecutionState.kt @@ -0,0 +1,280 @@ +package org.utbot.engine.state + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.PersistentSet +import kotlinx.collections.immutable.persistentHashMapOf +import kotlinx.collections.immutable.persistentHashSetOf +import kotlinx.collections.immutable.persistentListOf +import org.utbot.common.md5 +import org.utbot.engine.Memory +import org.utbot.engine.MethodResult +import org.utbot.engine.SymbolicFailure +import org.utbot.engine.pc.UtSolver +import org.utbot.engine.pc.UtSolverStatusUNDEFINED +import org.utbot.engine.state.StateLabel.CONCRETE +import org.utbot.engine.state.StateLabel.INTERMEDIATE +import org.utbot.engine.state.StateLabel.TERMINAL +import org.utbot.engine.symbolic.Assumption +import org.utbot.engine.symbolic.SymbolicState +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 + +data class Edge(val src: Stmt, val dst: Stmt, val decisionNum: Int) { + private val hashCode: Int = Objects.hash(src, dst, decisionNum) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Edge + + if (src != other.src) return false + if (dst != other.dst) return false + if (decisionNum != other.decisionNum) return false + + return true + } + + override fun hashCode(): Int = hashCode +} + +/** + * Possible state types. Engine matches on them and processes differently. + * + * [INTERMEDIATE] is a label for an intermediate state which is suitable for further symbolic analysis. + * + * [TERMINAL] is a label for a terminal state from which we might (or might not) execute concretely and construct + * [UtExecution]. This state represents the final state of the program execution, that is a throw or return from the outer + * method. + * + * [CONCRETE] is a label for a state which is not suitable for further symbolic analysis, and it is also not a terminal + * state. Such states are only suitable for a concrete execution and may result from [Assumption]s. + */ +enum class StateLabel { + INTERMEDIATE, + TERMINAL, + CONCRETE +} + +/** + * 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 + * inside a cycle. To improve performance it is a persistent map using state's hashcode to imitate an identity hashmap. + * + * @param symbolicState the current symbolic state. + */ +data class ExecutionState( + val stmt: Stmt, + val symbolicState: SymbolicState, + val executionStack: PersistentList, + val path: PersistentList = persistentListOf(), + val visitedStatementsHashesToCountInPath: PersistentMap = persistentHashMapOf(), + val decisionPath: PersistentList = persistentListOf(0), + val edges: PersistentSet = persistentHashSetOf(), + val stmts: PersistentMap = persistentHashMapOf(), + val pathLength: Int = 0, + val lastEdge: Edge? = null, + val lastMethod: SootMethod? = null, + val methodResult: MethodResult? = null, + val exception: SymbolicFailure? = null, + val label: StateLabel = INTERMEDIATE, + var stateAnalyticsProperties: StateAnalyticsProperties = StateAnalyticsProperties() +) : AutoCloseable { + val solver: UtSolver by symbolicState::solver + + val memory: Memory by symbolicState::memory + + var outgoingEdges = 0 + + fun isInNestedMethod() = executionStack.size > 1 + + val localVariableMemory + get() = executionStack.last().localVariableMemory + + val inputArguments + get() = executionStack.last().inputArguments + + val parameters + get() = executionStack.last().parameters + + /** + * Retrieves MUT parameters. + */ + val methodUnderTestParameters + get() = executionStack.firstOrNull()?.parameters?.map { it.value } + ?: error("Cannot retrieve MUT parameters from empty execution stack") + + val isThrowException: Boolean + get() = (lastEdge?.decisionNum ?: 0) < CALL_DECISION_NUM + + val isInsideStaticInitializer + get() = executionStack.any { it.method.isStaticInitializer } + + /** + * Tell to solver that states with status [UtSolverStatusUNDEFINED] can be created from current state. + * + * Note: Solver optimize cloning respect this flag. + */ + fun expectUndefined() { + solver.expectUndefined = true + } + + override fun close() { + solver.close() + } + + + /** + * Collects full statement path from method entry point to current statement, including current statement. + * + * Each step contains statement, call depth for nested calls (returns belong to called method) and decision. + * Decision for current statement is zero. + * + * Note: calculates depth wrongly for thrown exception, check SAT-811, SAT-812 + * TODO: fix SAT-811, SAT-812 + */ + fun fullPath(): List { + var depth = 0 + val path = path.zip( + decisionPath.subList(1, decisionPath.size) + ).map { (stmt, decision) -> + val stepDepth = when (decision) { + CALL_DECISION_NUM -> depth++ + RETURN_DECISION_NUM -> depth-- + else -> depth + } + Step(stmt, stepDepth, decision) + } + return path + Step(stmt, depth, 0) + } + + /** + * Prettifies full statement path for logging. + * + * Note: marks return statements with *depth-1* to pair with call statement. + */ + fun prettifiedPathLog(): String { + val path = fullPath() + 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, ' ') + } + "$prefix$stmt" + } + + 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, 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/state/ExecutionStateUpdates.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/state/ExecutionStateUpdates.kt new file mode 100644 index 0000000000..c7052a3016 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/state/ExecutionStateUpdates.kt @@ -0,0 +1,152 @@ +package org.utbot.engine.state + +import kotlinx.collections.immutable.plus +import org.utbot.engine.MethodResult +import org.utbot.engine.SymbolicFailure +import org.utbot.engine.SymbolicValue +import org.utbot.engine.putIfAbsent +import org.utbot.engine.symbolic.SymbolicStateUpdate +import soot.SootMethod +import soot.jimple.Stmt + +fun ExecutionState.createExceptionState( + exception: SymbolicFailure, + update: SymbolicStateUpdate +): ExecutionState { + val last = executionStack.last() + // go to negative indexing below CALL_DECISION_NUM for exceptions + val edge = Edge(stmt, stmt, CALL_DECISION_NUM - (++outgoingEdges)) + val localMemory = last.update(update.localMemoryUpdates) + return ExecutionState( + stmt = stmt, + symbolicState = symbolicState + update, + executionStack = executionStack.set(executionStack.lastIndex, localMemory), + path = path, + visitedStatementsHashesToCountInPath = visitedStatementsHashesToCountInPath, + decisionPath = decisionPath + edge.decisionNum, + edges = edges + edge, + stmts = stmts, + pathLength = pathLength + 1, + lastEdge = edge, + lastMethod = executionStack.last().method, + exception = exception, + label = label, + stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) + ) +} + +fun ExecutionState.update( + edge: Edge, + symbolicStateUpdate: SymbolicStateUpdate, + doesntThrow: Boolean, +): ExecutionState { + val last = executionStack.last() + val stackElement = last.update( + symbolicStateUpdate.localMemoryUpdates, + last.doesntThrow || doesntThrow + ) + outgoingEdges++ + + val stmtHashCode = stmt.hashCode() + val stmtCountInPath = (visitedStatementsHashesToCountInPath[stmtHashCode] ?: 0) + 1 + + return ExecutionState( + stmt = edge.dst, + symbolicState = symbolicState + symbolicStateUpdate, + executionStack = executionStack.set(executionStack.lastIndex, stackElement), + path = path + stmt, + 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, + label = label, + stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) + ) +} + +fun ExecutionState.withLabel(newLabel: StateLabel) = copy(label = newLabel) + + +fun ExecutionState.update( + stateUpdate: SymbolicStateUpdate +): ExecutionState { + val last = executionStack.last() + val stackElement = last.update(stateUpdate.localMemoryUpdates) + return copy( + symbolicState = symbolicState + stateUpdate, + executionStack = executionStack.set(executionStack.lastIndex, stackElement) + ) +} + +fun ExecutionState.push( + stmt: Stmt, + inputArguments: ArrayDeque, + update: SymbolicStateUpdate, + method: SootMethod +): ExecutionState { + val edge = Edge(this.stmt, stmt, CALL_DECISION_NUM) + val stackElement = ExecutionStackElement( + this.stmt, + localVariableMemory.memoryForNestedMethod().update(update.localMemoryUpdates), + inputArguments = inputArguments, + method = method, + doesntThrow = executionStack.last().doesntThrow + ) + outgoingEdges++ + + val stmtHashCode = this.stmt.hashCode() + val stmtCountInPath = (visitedStatementsHashesToCountInPath[stmtHashCode] ?: 0) + 1 + + return ExecutionState( + stmt = stmt, + symbolicState = symbolicState.stateForNestedMethod() + update, + executionStack = executionStack + stackElement, + path = path + this.stmt, + 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, + label = label, + stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) + ) +} + +fun ExecutionState.pop(methodResult: MethodResult): ExecutionState { + val caller = executionStack.last().caller!! + val edge = Edge(stmt, caller, RETURN_DECISION_NUM) + + val stmtHashcode = stmt.hashCode() + val stmtCountInPath = (visitedStatementsHashesToCountInPath[stmtHashcode] ?: 0) + 1 + + return ExecutionState( + stmt = caller, + symbolicState = symbolicState, + executionStack = executionStack.removeAt(executionStack.lastIndex), + path = path + stmt, + 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, + label = label, + stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) + ) +} \ 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 cbe2d5c7ef..facd1b8f39 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 @@ -35,5 +35,4 @@ data class SymbolicState( fun stateForNestedMethod() = copy( memory = memory.memoryForNestedMethod() ) - -} \ No newline at end of file +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/types/TypeRegistry.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/types/TypeRegistry.kt new file mode 100644 index 0000000000..6c3d1f7865 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/types/TypeRegistry.kt @@ -0,0 +1,585 @@ +package org.utbot.engine.types + +import com.google.common.collect.HashBiMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf +import org.utbot.common.WorkaroundReason +import org.utbot.common.doNotRun +import org.utbot.common.workaround +import org.utbot.engine.ChunkId +import org.utbot.engine.MAX_NUM_DIMENSIONS +import org.utbot.engine.MemoryUpdate +import org.utbot.engine.MethodResult +import org.utbot.engine.ObjectValue +import org.utbot.engine.TypeConstraint +import org.utbot.engine.TypeStorage +import org.utbot.engine.baseType +import org.utbot.engine.classBytecodeSignatureToClassNameOrNull +import org.utbot.engine.findMockAnnotationOrNull +import org.utbot.engine.getByValue +import org.utbot.engine.isAnonymous +import org.utbot.engine.isInappropriate +import org.utbot.engine.isJavaLangObject +import org.utbot.engine.nullObjectAddr +import org.utbot.engine.numDimensions +import org.utbot.engine.pc.UtAddrExpression +import org.utbot.engine.pc.UtAddrSort +import org.utbot.engine.pc.UtArrayExpressionBase +import org.utbot.engine.pc.UtArraySort +import org.utbot.engine.pc.UtBoolExpression +import org.utbot.engine.pc.UtBoolSort +import org.utbot.engine.pc.UtEqGenericTypeParametersExpression +import org.utbot.engine.pc.UtFalse +import org.utbot.engine.pc.UtGenericExpression +import org.utbot.engine.pc.UtInt32Sort +import org.utbot.engine.pc.UtIsExpression +import org.utbot.engine.pc.UtIsGenericTypeExpression +import org.utbot.engine.pc.UtMkTermArrayExpression +import org.utbot.engine.pc.UtTrue +import org.utbot.engine.pc.mkAnd +import org.utbot.engine.pc.mkArrayConst +import org.utbot.engine.pc.mkArrayWithConst +import org.utbot.engine.pc.mkEq +import org.utbot.engine.pc.mkInt +import org.utbot.engine.pc.mkNot +import org.utbot.engine.pc.mkOr +import org.utbot.engine.pc.select +import org.utbot.engine.pc.store +import org.utbot.engine.namedStore +import org.utbot.engine.symbolic.asHardConstraint +import org.utbot.engine.toIntValue +import org.utbot.engine.toPrimitiveValue +import soot.ArrayType +import soot.BooleanType +import soot.ByteType +import soot.CharType +import soot.DoubleType +import soot.FloatType +import soot.IntType +import soot.LongType +import soot.RefType +import soot.Scene +import soot.ShortType +import soot.SootClass +import soot.SootField +import soot.SootMethod +import soot.Type +import soot.tagkit.AnnotationClassElem +import java.math.BigInteger +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong + +/** + * Types/classes registry. + * + * Registers and keeps two mappings: + * - Type <-> unique type id (int) + * - Object address -> to type id + */ +class TypeRegistry { + init { + // initializes type storage for OBJECT_TYPE from current scene + objectTypeStorage = TypeStorage.constructTypeStorageUnsafe( + OBJECT_TYPE, + Scene.v().classes.mapTo(mutableSetOf()) { it.type } + ) + } + + private val typeIdBiMap = HashBiMap.create() + + // A cache for strings representing bit-vectors for some collection of types. + private val typesToBitVecString = mutableMapOf, String>() + private val typeToRating = mutableMapOf() + private val typeToInheritorsTypes = mutableMapOf>() + private val typeToAncestorsTypes = 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() + + /** + * Contains mapping from a class to the class containing substitutions for its methods. + */ + private val targetToSubstitution: Map by lazy { + val classesWithTargets = Scene.v().classes.mapNotNull { clazz -> + val annotation = clazz.findMockAnnotationOrNull + ?.elems + ?.singleOrNull { it.name == "target" } as? AnnotationClassElem + + val classNameFromSignature = classBytecodeSignatureToClassNameOrNull(annotation?.desc) + + if (classNameFromSignature == null) { + null + } else { + val target = Scene.v().getSootClass(classNameFromSignature) + target to clazz + } + } + classesWithTargets.toMap() + } + + /** + * Contains mapping from a class with substitutions of the methods of the target class to the target class itself. + */ + private val substitutionToTarget: Map by lazy { + targetToSubstitution.entries.associate { (k, v) -> v to k } + } + + private val typeToFields = mutableMapOf>() + + /** + * An array containing information about whether the object with particular addr could throw a [ClassCastException]. + * + * Note: all objects can throw it by default. + * @see disableCastClassExceptionCheck + */ + private var isClassCastExceptionAllowed: UtArrayExpressionBase = + mkArrayWithConst(UtArraySort(UtAddrSort, UtBoolSort), UtTrue) + + + /** + * Contains information about types for ReferenceValues. + * An element on some position k contains information about type for an object with address == k + * Each element in addrToTypeId is in range [1..numberOfTypes] + */ + private val addrToTypeId: UtArrayExpressionBase by lazy { + mkArrayConst( + "addrToTypeId", + UtAddrSort, + UtInt32Sort + ) + } + + private val genericAddrToTypeArrays = mutableMapOf() + + private fun genericAddrToType(i: Int) = genericAddrToTypeArrays.getOrPut(i) { + mkArrayConst( + "genericAddrToTypeId_$i", + UtAddrSort, + UtInt32Sort + ) + } + + /** + * Contains information about number of dimensions for ReferenceValues. + */ + private val addrToNumDimensions: UtArrayExpressionBase by lazy { + mkArrayConst( + "addrToNumDimensions", + UtAddrSort, + UtInt32Sort + ) + } + + private val genericAddrToNumDimensionsArrays = mutableMapOf() + + private fun genericAddrToNumDimensions(i: Int) = genericAddrToNumDimensionsArrays.getOrPut(i) { + mkArrayConst( + "genericAddrToNumDimensions_$i", + UtAddrSort, + UtInt32Sort + ) + } + + /** + * Contains information about whether the object with some addr is a mock or not. + */ + private val isMockArray: UtArrayExpressionBase by lazy { + mkArrayConst( + "isMock", + UtAddrSort, + UtBoolSort + ) + } + + /** + * Takes information about whether the object with [addr] is mock or not. + * + * @see isMockArray + */ + fun isMock(addr: UtAddrExpression) = isMockArray.select(addr) + + private fun mockCorrectnessConstraint(addr: UtAddrExpression) = + mkOr( + mkEq(isMock(addr), UtFalse), + mkNot(mkEq(addr, nullObjectAddr)) + ) + + fun isMockConstraint(addr: UtAddrExpression) = mkAnd( + mkOr( + mkEq(isMock(addr), UtTrue), + mkEq(addr, nullObjectAddr) + ), + mockCorrectnessConstraint(addr) + ) + + /** + * Makes the numbers of dimensions for every object in the program equal to zero by default + */ + fun softZeroNumDimensions() = UtMkTermArrayExpression(addrToNumDimensions) + + /** + * addrToTypeId is created as const array of the emptyType. If such type occurs anywhere in the program, it means + * we haven't touched the element that this type belongs to + * @see emptyTypeId + */ + fun softEmptyTypes() = UtMkTermArrayExpression(addrToTypeId, mkInt(emptyTypeId)) + + /** + * Calculates type 'rating' for a particular type. Used for ordering ObjectValue's possible types. + * The type with a higher rating is more likely than the one with a lower rating. + */ + fun findRating(type: RefType) = typeToRating.getOrPut(type) { + var finalCost = 0 + + val sootClass = type.sootClass + + // TODO: let's have "preferred types" + if (sootClass.name == "java.util.ArrayList") finalCost += 4096 + if (sootClass.name == "java.util.LinkedList") finalCost += 2048 + if (sootClass.name == "java.util.HashMap") finalCost += 4096 + if (sootClass.name == "java.util.TreeMap") finalCost += 2048 + if (sootClass.name == "java.util.HashSet") finalCost += 2048 + if (sootClass.name == "java.lang.Integer") finalCost += 8192 + if (sootClass.name == "java.lang.Character") finalCost += 8192 + if (sootClass.name == "java.lang.Double") finalCost += 8192 + if (sootClass.name == "java.lang.Long") finalCost += 8192 + + if (sootClass.packageName.startsWith("java.lang")) finalCost += 1024 + + if (sootClass.packageName.startsWith("java.util")) finalCost += 512 + + if (sootClass.packageName.startsWith("java")) finalCost += 128 + + if (sootClass.isPublic) finalCost += 16 + + if (sootClass.isPrivate) finalCost += -16 + + if ("blocking" in sootClass.name.toLowerCase()) finalCost -= 32 + + if (sootClass.type.isJavaLangObject()) finalCost += -32 + + if (sootClass.isAnonymous) finalCost += -128 + + if (sootClass.name.contains("$")) finalCost += -4096 + + if (sootClass.type.sootClass.isInappropriate) finalCost += -8192 + + finalCost + } + + private val classRefCounter = AtomicInteger(classRefAddrsInitialValue) + private fun nextClassRefAddr() = UtAddrExpression(classRefCounter.getAndIncrement()) + + private val symbolicReturnNameCounter = AtomicLong(symbolicReturnNameCounterInitialValue) + fun findNewSymbolicReturnValueName() = + workaround(WorkaroundReason.MAKE_SYMBOLIC) { "symbolicReturnValue\$${symbolicReturnNameCounter.incrementAndGet()}" } + + private val typeCounter = AtomicInteger(typeCounterInitialValue) + private fun nextTypeId() = typeCounter.getAndIncrement() + + /** + * Returns unique typeId for the given type + */ + fun findTypeId(type: Type): Int = typeIdBiMap.getOrPut(type) { nextTypeId() } + + /** + * Returns type for the given typeId + * + * @return If there is such typeId in the program, returns the corresponding type, otherwise returns null + */ + fun typeByIdOrNull(typeId: Int): Type? = typeIdBiMap.getByValue(typeId) + + /** + * Returns symbolic representation for a typeId corresponding to the given address + */ + fun symTypeId(addr: UtAddrExpression) = addrToTypeId.select(addr) + + /** + * Returns a symbolic representation for an [i]th type parameter + * corresponding to the given address + */ + fun genericTypeId(addr: UtAddrExpression, i: Int) = genericAddrToType(i).select(addr) + + /** + * Returns symbolic representation for a number of dimensions corresponding to the given address + */ + 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 + */ + fun zeroDimensionConstraint(addr: UtAddrExpression) = mkEq(symNumDimensions(addr), mkInt(objectNumDimensions)) + + /** + * Constructs a binary bit-vector by the given types with length 'numberOfTypes'. Each position + * corresponding to one of the typeId. + * + * @param types the collection of possible type + * @return decimal string representing the binary bit-vector + */ + fun constructBitVecString(types: Collection) = typesToBitVecString.getOrPut(types) { + val initialValue = BigInteger(ByteArray(numberOfTypes) { 0 }) + + return types.fold(initialValue) { acc, type -> + val typeId = if (type is ArrayType) findTypeId(type.baseType) else findTypeId(type) + acc.setBit(typeId) + }.toString() + } + + /** + * Creates class reference, i.e. Class<Integer> + * + * Note: Uses type id as an address to have the one and the same class reference for all objects of one class + */ + fun createClassRef(baseType: Type, numDimensions: Int = 0): MethodResult { + val addr = classRefBiMap.getOrPut(baseType) { nextClassRefAddr() } + + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(CLASS_REF_TYPE) + val objectValue = ObjectValue(typeStorage, addr) + + val typeConstraint = typeConstraint(addr, typeStorage).all() + + val typeId = mkInt(findTypeId(baseType)) + val symNumDimensions = mkInt(numDimensions) + + val stores = persistentListOf( + namedStore(CLASS_REF_TYPE_DESCRIPTOR, addr, typeId), + namedStore(CLASS_REF_NUM_DIMENSIONS_DESCRIPTOR, addr, symNumDimensions) + ) + + val touchedDescriptors = persistentSetOf(CLASS_REF_TYPE_DESCRIPTOR, CLASS_REF_NUM_DIMENSIONS_DESCRIPTOR) + + val memoryUpdate = MemoryUpdate( + stores = stores, + touchedChunkDescriptors = touchedDescriptors, + ) + + return MethodResult(objectValue, typeConstraint.asHardConstraint(), memoryUpdates = memoryUpdate) + } + + /** + * Returns a list of inheritors for the given [type], including itself. + */ + fun findInheritorsIncludingTypes(type: RefType, defaultValue: () -> Set) = + typeToInheritorsTypes.getOrPut(type, defaultValue) + + /** + * Returns a list of ancestors for the given [type], including itself. + */ + fun findAncestorsIncludingTypes(type: RefType, defaultValue: () -> Set) = + typeToAncestorsTypes.getOrPut(type, defaultValue) + + fun findFields(type: RefType, defaultValue: () -> List) = + typeToFields.getOrPut(type, defaultValue) + + /** + * Returns a [TypeConstraint] instance for the given [addr] and [typeStorage]. + */ + fun typeConstraint(addr: UtAddrExpression, typeStorage: TypeStorage): TypeConstraint = + TypeConstraint( + constructIsExpression(addr, typeStorage), + mkEq(addr, nullObjectAddr), + constructCorrectnessConstraint(addr, typeStorage) + ) + + private fun constructIsExpression(addr: UtAddrExpression, typeStorage: TypeStorage): UtIsExpression = + UtIsExpression(addr, typeStorage, numberOfTypes) + + /** + * Returns a conjunction of the constraints responsible for the type construction: + * * typeId must be in range [[emptyTypeId]..[numberOfTypes]]; + * * numDimensions must be in range [0..[MAX_NUM_DIMENSIONS]]; + * * if the baseType for [TypeStorage.leastCommonType] is a [java.lang.Object], + * should be added constraints for primitive arrays to prevent + * impossible resolved types: Object[] must be at least primType[][]. + */ + private fun constructCorrectnessConstraint(addr: UtAddrExpression, typeStorage: TypeStorage): UtBoolExpression { + val symType = symTypeId(addr) + val symNumDimensions = symNumDimensions(addr) + val type = typeStorage.leastCommonType + + val constraints = mutableListOf() + + // add constraints for typeId, it must be in 0..numberOfTypes + constraints += org.utbot.engine.Ge(symType.toIntValue(), emptyTypeId.toPrimitiveValue()) + constraints += org.utbot.engine.Le(symType.toIntValue(), numberOfTypes.toPrimitiveValue()) + + // add constraints for number of dimensions, it must be in 0..MAX_NUM_DIMENSIONS + constraints += org.utbot.engine.Ge(symNumDimensions.toIntValue(), 0.toPrimitiveValue()) + constraints += org.utbot.engine.Le(symNumDimensions.toIntValue(), MAX_NUM_DIMENSIONS.toPrimitiveValue()) + + doNotRun { + // add constraints for object and arrays of primitives + if (type.baseType.isJavaLangObject()) { + primTypes.forEach { + val typesAreEqual = mkEq(symType, mkInt(findTypeId(it))) + val numDimensions = + org.utbot.engine.Gt(symNumDimensions.toIntValue(), type.numDimensions.toPrimitiveValue()) + constraints += mkOr(mkNot(typesAreEqual), numDimensions) + } + } + + // there are no arrays of anonymous classes + typeStorage.possibleConcreteTypes + .mapNotNull { (it.baseType as? RefType) } + .filter { it.sootClass.isAnonymous } + .forEach { + val typesAreEqual = mkEq(symType, mkInt(findTypeId(it))) + val numDimensions = mkEq(symNumDimensions.toIntValue(), mkInt(objectNumDimensions).toIntValue()) + constraints += mkOr(mkNot(typesAreEqual), numDimensions) + } + } + + return mkAnd(constraints) + } + + /** + * returns constraint representing, that object with address [addr] is parametrized by [types] type parameters. + * @see UtGenericExpression + */ + fun genericTypeParameterConstraint(addr: UtAddrExpression, types: List) = + UtGenericExpression(addr, types, numberOfTypes) + + /** + * Returns constraint representing that an object with address [addr] has the same type as the type parameter + * with index [i] of an object with address [baseAddr]. + * + * For a SomeCollection the type parameters are [A, B], where A and B are type variables + * with indices zero and one respectively. To connect some element of the collection with its generic type + * add to the constraints `typeConstraintToGenericTypeParameter(elementAddr, collectionAddr, typeParamIndex)`. + * + * @see UtIsGenericTypeExpression + */ + fun typeConstraintToGenericTypeParameter( + addr: UtAddrExpression, + baseAddr: UtAddrExpression, + i: Int + ): UtIsGenericTypeExpression = UtIsGenericTypeExpression(addr, baseAddr, i) + + /** + * Looks for a substitution for the given [method]. + * + * @param method a method to be substituted. + * @return substituted method if the given [method] has substitution, null otherwise. + * + * Note: all the methods in the class with substitutions will be returned instead of methods of the target class + * with the same name and parameters' types without any additional annotations. The only exception is `` + * method, substitutions will be returned only for constructors marked by [org.utbot.api.annotation.UtConstructorMock] + * annotation. + */ + fun findSubstitutionOrNull(method: SootMethod): SootMethod? { + val declaringClass = method.declaringClass + val classWithSubstitutions = targetToSubstitution[declaringClass] + + val substitutedMethod = classWithSubstitutions + ?.methods + ?.singleOrNull { it.name == method.name && it.parameterTypes == method.parameterTypes } + // Note: subSignature is not used in order to support `this` as method's return value. + // Otherwise we'd have to check for wrong `this` type in the subSignature + + if (method.isConstructor) { + // if the constructor doesn't have the mock annotation do not substitute it + substitutedMethod?.findMockAnnotationOrNull ?: return null + } + return substitutedMethod + } + + /** + * Returns a class containing substitutions for the methods belong to the target class, null if there is not such class. + */ + @Suppress("unused") + fun findSubstitutionByTargetOrNull(targetClass: SootClass): SootClass? = targetToSubstitution[targetClass] + + /** + * Returns a target class by given class with methods substitutions. + */ + @Suppress("MemberVisibilityCanBePrivate") + fun findTargetBySubstitutionOrNull(classWithSubstitutions: SootClass): SootClass? = + substitutionToTarget[classWithSubstitutions] + + /** + * Looks for 'real' type. + * + * For example, we have two classes: A and B, B contains substitutions for A's methods. + * `findRealType(a.type)` will return `a.type`, but `findRealType(b.type)` will return `a.type` as well. + * + * Returns: + * * [type] if it is not a RefType; + * * [type] if it is a RefType and it doesn't have a target class to substitute; + * * otherwise a type of the target class, which methods should be substituted. + */ + fun findRealType(type: Type): Type = + if (type !is RefType) type else findTargetBySubstitutionOrNull(type.sootClass)?.type ?: type + + /** + * Returns a select expression containing information about whether [ClassCastException] is allowed or not + * for an object with the given [addr]. + * + * True means that [ClassCastException] might be thrown, false will restrict it. + */ + fun isClassCastExceptionAllowed(addr: UtAddrExpression) = isClassCastExceptionAllowed.select(addr) + + /** + * Modify [isClassCastExceptionAllowed] to make impossible for a [ClassCastException] to be thrown for an object + * with the given [addr]. + */ + fun disableCastClassExceptionCheck(addr: UtAddrExpression) { + isClassCastExceptionAllowed = isClassCastExceptionAllowed.store(addr, UtFalse) + } + + /** + * Returns chunkId for the given [arrayType]. + * + * Examples: + * * Object[] -> RefValues_Arrays + * * int[] -> intArrays + * * int[][] -> MultiArrays + */ + fun arrayChunkId(arrayType: ArrayType) = when (arrayType.numDimensions) { + 1 -> if (arrayType.baseType is RefType) { + ChunkId("RefValues", "Arrays") + } else { + ChunkId("${findRealType(arrayType.baseType)}", "Arrays") + } + else -> ChunkId("Multi", "Arrays") + } + + companion object { + // we use different shifts to distinguish easily types from objects in z3 listings + const val objectCounterInitialValue = 0x00000001 // 0x00000000 is reserved for NULL + + // we want to reserve addresses for every ClassRef in the program starting from this one + // Note: the number had been chosen randomly and can be changes without any consequences + const val classRefAddrsInitialValue = -16777216 // -(2 ^ 24) + + // since we use typeId as addr for ConstRef, we can not use 0x00000000 because of NULL value + const val typeCounterInitialValue = 0x00000001 + const val symbolicReturnNameCounterInitialValue = 0x80000000 + const val objectNumDimensions = 0 + const val emptyTypeId = 0 + private const val primitivesNumber = 8 + + internal val primTypes + get() = listOf( + ByteType.v(), + ShortType.v(), + IntType.v(), + LongType.v(), + FloatType.v(), + DoubleType.v(), + BooleanType.v(), + CharType.v() + ) + + val numberOfTypes get() = Scene.v().classes.size + primitivesNumber + typeCounterInitialValue + + /** + * Stores [TypeStorage] for [OBJECT_TYPE]. As it should be changed when Soot scene changes, + * it is loaded each time when [TypeRegistry] is created in init section. + */ + lateinit var objectTypeStorage: TypeStorage + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/types/TypeResolver.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/types/TypeResolver.kt new file mode 100644 index 0000000000..ca88797cb3 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/types/TypeResolver.kt @@ -0,0 +1,609 @@ +package org.utbot.engine.types + +import kotlinx.collections.immutable.persistentListOf +import org.utbot.common.WorkaroundReason +import org.utbot.common.workaround +import org.utbot.engine.ArrayValue +import org.utbot.engine.ChunkId +import org.utbot.engine.Hierarchy +import org.utbot.engine.Memory +import org.utbot.engine.MemoryChunkDescriptor +import org.utbot.engine.MemoryUpdate +import org.utbot.engine.ObjectValue +import org.utbot.engine.ReferenceValue +import org.utbot.engine.TypeStorage +import org.utbot.engine.appropriateClasses +import org.utbot.engine.baseType +import org.utbot.engine.findMockAnnotationOrNull +import org.utbot.engine.isAppropriate +import org.utbot.engine.isArtificialEntity +import org.utbot.engine.isInappropriate +import org.utbot.engine.isJavaLangObject +import org.utbot.engine.isLambda +import org.utbot.engine.isOverridden +import org.utbot.engine.isUtMock +import org.utbot.engine.makeArrayType +import org.utbot.engine.nullObjectAddr +import org.utbot.engine.numDimensions +import org.utbot.engine.pc.UtAddrExpression +import org.utbot.engine.pc.UtBoolExpression +import org.utbot.engine.pc.UtEqGenericTypeParametersExpression +import org.utbot.engine.pc.mkAnd +import org.utbot.engine.pc.mkEq +import org.utbot.engine.rawType +import org.utbot.engine.wrapper +import org.utbot.engine.wrapperToClass +import org.utbot.framework.plugin.api.id +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.util.UTBOT_FRAMEWORK_API_VISIBLE_PACKAGE +import soot.ArrayType +import soot.IntType +import soot.NullType +import soot.PrimType +import soot.RefType +import soot.Scene +import soot.SootClass +import soot.SootField +import soot.Type +import soot.VoidType +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy: Hierarchy) { + + fun findOrConstructInheritorsIncludingTypes(type: RefType) = typeRegistry.findInheritorsIncludingTypes(type) { + hierarchy.inheritors(type.sootClass.id).mapTo(mutableSetOf()) { it.type } + } + + fun findOrConstructAncestorsIncludingTypes(type: RefType) = typeRegistry.findAncestorsIncludingTypes(type) { + hierarchy.ancestors(type.sootClass.id).mapTo(mutableSetOf()) { it.type } + } + + /** + * Finds all the inheritors for each type from the [types] and returns their intersection. + * + * Note: every type from the result satisfies [isAppropriate] condition. + */ + fun intersectInheritors(types: Array): Set = intersectTypes(types) { + findOrConstructInheritorsIncludingTypes(it) + } + + /** + * Finds all the ancestors for each type from the [types] and return their intersection. + * + * Note: every type from the result satisfies [isAppropriate] condition. + */ + fun intersectAncestors(types: Array): Set = intersectTypes(types) { + findOrConstructAncestorsIncludingTypes(it) + } + + private fun intersectTypes( + types: Array, + retrieveFunction: (RefType) -> Set + ): Set { + val allObjects = findOrConstructInheritorsIncludingTypes(OBJECT_TYPE) + + // TODO we do not support constructions like List here, be aware of it + // TODO JIRA:1446 + + return types + .map { classOrDefault(it.rawType.typeName) } + .fold(allObjects) { acc, value -> acc.intersect(retrieveFunction(value)) } + .filter { it.sootClass.isAppropriate } + .toSet() + } + + private fun classOrDefault(typeName: String): RefType = + runCatching { Scene.v().getRefType(typeName) }.getOrDefault(OBJECT_TYPE) + + fun findFields(type: RefType) = typeRegistry.findFields(type) { + hierarchy + .ancestors(type.sootClass.id) + .flatMap { it.fields } + } + + /** + * Returns given number of appropriate types that have the highest rating. + * + * @param types Collection of types to sort + * @param take Number of types to take + * + * @see TypeRegistry.findRating + * @see appropriateClasses + */ + fun findTopRatedTypes(types: Collection, take: Int = Int.MAX_VALUE) = + types.appropriateClasses() + .sortedByDescending { type -> + val baseType = if (type is ArrayType) type.baseType else type + // primitive baseType has the highest possible rating + if (baseType is RefType) typeRegistry.findRating(baseType) else Int.MAX_VALUE + } + .take(take) + + /** + * Constructs a [TypeStorage] instance containing [type] as its most common type and + * appropriate types from [possibleTypes] in its [TypeStorage.possibleConcreteTypes]. + * + * @param type the most common type of the constructed type storage. + * @param possibleTypes a list of types to be contained in the constructed type storage. + * + * @return constructed type storage. + * + * Note: [TypeStorage.possibleConcreteTypes] of the type storage returned by this method contains only + * classes we can instantiate: there will be no interfaces, abstract or local classes. + * If there are no such classes, [TypeStorage.possibleConcreteTypes] is an empty set. + * + * @see isAppropriate + */ + fun constructTypeStorage(type: Type, possibleTypes: Collection): TypeStorage { + val concretePossibleTypes = possibleTypes + .map { (if (it is ArrayType) it.baseType else it) to it.numDimensions } + .filterNot { (baseType, numDimensions) -> isInappropriateOrArrayOfMocks(numDimensions, baseType) } + .mapTo(mutableSetOf()) { (baseType, numDimensions) -> + if (numDimensions == 0) baseType else baseType.makeArrayType(numDimensions) + } + + return TypeStorage.constructTypeStorageUnsafe(type, concretePossibleTypes).removeInappropriateTypes() + } + + private fun isInappropriateOrArrayOfMocks(numDimensions: Int, baseType: Type?): Boolean { + if (baseType !is RefType) { + return false + } + + val baseSootClass = baseType.sootClass + + // We don't want to have our wrapper's classes as a part of a regular TypeStorage instance + // Note that we cannot have here 'isOverridden' since iterators of our wrappers are not wrappers + if (wrapperToClass[baseType] != null) { + return true + } + + if (numDimensions == 0 && baseSootClass.isInappropriate) { + // interface, abstract class, or mock could not be constructed + return true + } + + if (numDimensions > 0 && baseSootClass.findMockAnnotationOrNull != null) { + // array of mocks 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. + * + * @param type a type for which we want to construct type storage. + * @param useConcreteType a boolean parameter to determine whether we want to include inheritors of the type or not. + * + * @return constructed type storage. + * + * Note: [TypeStorage.possibleConcreteTypes] of the type storage returned by this method contains only + * classes we can instantiate: there will be no interfaces, abstract or local classes. + * If there are no such classes, [TypeStorage.possibleConcreteTypes] is an empty set. + * + * @see isAppropriate + */ + fun constructTypeStorage(type: Type, useConcreteType: Boolean): TypeStorage { + // create a typeStorage with concreteType even if the type belongs to an interface or an abstract class + if (useConcreteType) return TypeStorage.constructTypeStorageWithSingleType(type) + + val baseType = type.baseType + + val inheritors = if (baseType !is RefType) { + setOf(baseType) + } else { + // use only 'appropriate' classes in the TypeStorage construction + val allInheritors = findOrConstructInheritorsIncludingTypes(baseType) + + // if the type is ArrayType, we don't have to filter abstract classes and interfaces from the inheritors + // because we still can instantiate, i.e., Number[]. + if (type is ArrayType) { + allInheritors + } else { + allInheritors.filterTo(mutableSetOf()) { it.sootClass.isAppropriate } + } + } + + val extendedInheritors = if (baseType.isJavaLangObject()) inheritors + TypeRegistry.primTypes else inheritors + + val possibleTypes = when (type) { + is RefType, is PrimType -> extendedInheritors + is ArrayType -> when (baseType) { + is RefType -> extendedInheritors.map { it.makeArrayType(type.numDimensions) }.toSet() + else -> setOf(baseType.makeArrayType(type.numDimensions)) + } + else -> error("Unexpected type $type") + } + + return TypeStorage.constructTypeStorageUnsafe(type, possibleTypes).removeInappropriateTypes() + } + + /** + * Remove wrapper types, classes from the visible for Soot package and, if any other type is available, artificial entities. + */ + private fun TypeStorage.removeInappropriateTypes(): TypeStorage { + val leastCommonSootClass = (leastCommonType as? RefType)?.sootClass + val keepArtificialEntities = leastCommonSootClass?.isArtificialEntity == true + + val appropriateTypes = possibleConcreteTypes.filter { + // All not RefType should be included in the concreteTypes, e.g., arrays + val sootClass = (it.baseType as? RefType)?.sootClass ?: return@filter true + + // All artificial entities except anonymous functions should be filtered out if we have another types + if (sootClass.isArtificialEntity) { + if (sootClass.isLambda) { + return@filter true + } + + return@filter keepArtificialEntities + } + + // All wrappers and classes from the visible for Soot package should be filtered out because they could not be instantiated + workaround(WorkaroundReason.HACK) { + if (leastCommonSootClass == OBJECT_TYPE && sootClass.isOverridden) { + return@filter false + } + + if (sootClass.packageName == UTBOT_FRAMEWORK_API_VISIBLE_PACKAGE) { + return@filter false + } + } + + return@filter true + }.toSet() + + return TypeStorage.constructTypeStorageUnsafe(leastCommonType, appropriateTypes) + } + + /** + * Constructs a nullObject with TypeStorage containing all the inheritors for the given type + */ + fun nullObject(type: Type): ReferenceValue { + val typeStorage = TypeStorage.constructTypeStorageWithSingleType(type) + + return when (type) { + is RefType, is NullType, is VoidType -> ObjectValue(typeStorage, nullObjectAddr) + is ArrayType -> ArrayValue(typeStorage, nullObjectAddr) + else -> error("Unsupported nullType $type") + } + } + + fun downCast(arrayValue: ArrayValue, typeToCast: ArrayType): ArrayValue { + val typesBeforeCast = findOrConstructInheritorsIncludingTypes(arrayValue.type.baseType as RefType) + val typesAfterCast = findOrConstructInheritorsIncludingTypes(typeToCast.baseType as RefType) + val possibleTypes = typesBeforeCast.filter { it in typesAfterCast }.map { + it.makeArrayType(arrayValue.type.numDimensions) + } + + return arrayValue.copy(typeStorage = constructTypeStorage(typeToCast, possibleTypes)) + } + + fun downCast(objectValue: ObjectValue, typeToCast: RefType): ObjectValue { + val inheritorsTypes = findOrConstructInheritorsIncludingTypes(typeToCast) + val possibleTypes = objectValue.possibleConcreteTypes.filter { it in inheritorsTypes } + + return wrapper(typeToCast, objectValue.addr) ?: objectValue.copy( + typeStorage = constructTypeStorage( + typeToCast, + possibleTypes + ) + ) + } + + /** + * Connects types and number of dimensions for the two given addresses. Uses for reading from arrays: + * cell should have the same type and number of dimensions as the objects taken/put from/in it. + * It is a simplification, because the object can be subtype of the type of the cell, but it is ignored for now. + */ + fun connectArrayCeilType(ceilAddr: UtAddrExpression, valueAddr: UtAddrExpression): UtBoolExpression { + val ceilSymType = typeRegistry.symTypeId(ceilAddr) + val valueSymType = typeRegistry.symTypeId(valueAddr) + val ceilSymDimension = typeRegistry.symNumDimensions(ceilAddr) + val valueSymDimension = typeRegistry.symNumDimensions(valueAddr) + + return mkAnd(mkEq(ceilSymType, valueSymType), mkEq(ceilSymDimension, valueSymDimension)) + } + + fun findAnyConcreteInheritorIncludingOrDefaultUnsafe(evaluatedType: RefType, defaultType: RefType): RefType = + findAnyConcreteInheritorIncluding(evaluatedType) ?: findAnyConcreteInheritorIncluding(defaultType) + ?: error("No concrete types found neither for $evaluatedType, nor for $defaultType") + + fun findAnyConcreteInheritorIncludingOrDefault(evaluatedType: RefType, defaultType: RefType): RefType? = + findAnyConcreteInheritorIncluding(evaluatedType) ?: findAnyConcreteInheritorIncluding(defaultType) + + private fun findAnyConcreteInheritorIncluding(type: RefType): RefType? = + if (type.sootClass.isAppropriate) { + type + } else { + findOrConstructInheritorsIncludingTypes(type) + .filterNot { !it.hasSootClass() && (it.sootClass.isOverridden || it.sootClass.isUtMock) } + .sortedByDescending { typeRegistry.findRating(it) } + .firstOrNull { it.sootClass.isAppropriate } + } + + companion object { + private fun createUpdateForGenericTypeInfo( + addr: UtAddrExpression, + typeStorages: List + ) = MemoryUpdate(genericTypeStorageByAddr = persistentListOf(addr to typeStorages)) + + /** + * Extracts type information for an object with specified [addr] from the [memory]. + * If it contains more type storages than one, or it contains an empty list of storages, + * an error will be thrown. + * + * [objectClassName] is used for an error message. + */ + fun extractTypeStorageForObjectWithSingleTypeParameter( + addr: UtAddrExpression, + objectClassName: String, + memory: Memory + ): TypeStorage? { + val valueTypeFromGenerics = memory.getTypeStoragesForObjectTypeParameters(addr) + + if (valueTypeFromGenerics != null && valueTypeFromGenerics.size != 1) { + error("$objectClassName must have only one type parameter, but it got ${valueTypeFromGenerics.size}") + } + + return valueTypeFromGenerics?.single() + } + + /** + * Creates a memory update for setting types storages for [firstAddr]'s + * type parameters equal to type storages for [secondAddr]'s type parameters + * according to provided types injection represented by [indexInjection]. + * + * [genericTypeStorageByAddr] is a storage from which this type information is extracted. + */ + private fun setParameterTypeStoragesEquality( + firstAddr: UtAddrExpression, + secondAddr: UtAddrExpression, + indexInjection: Array>, + genericTypeStorageByAddr: Map> + ): MemoryUpdate { + val existingGenericTypes = genericTypeStorageByAddr[secondAddr] ?: return MemoryUpdate() + + 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] + } + + return createUpdateForGenericTypeInfo( + firstAddr, + currentGenericTypes + .entries + .sortedBy { it.key } + .mapTo(mutableListOf()) { it.value } + ) + } + + /** + * Returns a constraint representing that type parameters of an object + * with address [firstAddr] are equal to type parameters of an object + * with address [secondAddr], corresponding to [indexInjection], + * and a memory update for it. + * + * [genericTypeStorageByAddr] is a storage from which this type information is extracted. + * + * @see UtEqGenericTypeParametersExpression + */ + @Suppress("unused") + fun eqGenericTypeParametersConstraint( + firstAddr: UtAddrExpression, + secondAddr: UtAddrExpression, + genericTypeStorageByAddr: Map>, + vararg indexInjection: Pair + ): Pair { + val memoryUpdate = setParameterTypeStoragesEquality( + firstAddr, + secondAddr, + indexInjection, + genericTypeStorageByAddr + ) + + return UtEqGenericTypeParametersExpression(firstAddr, secondAddr, mapOf(*indexInjection)) to memoryUpdate + } + + /** + * Returns a constraint representing that type parameters of an object with address [firstAddr] + * are equal to the corresponding type parameters of an object with address [secondAddr] + * and a corresponding memory update. + * + * [genericTypeStorageByAddr] is a storage from which this type information is extracted. + * + * @see UtEqGenericTypeParametersExpression + */ + fun eqGenericTypeParametersConstraint( + firstAddr: UtAddrExpression, + secondAddr: UtAddrExpression, + parameterSize: Int, + genericTypeStorageByAddr: Map> + ) : Pair { + val injections = Array(parameterSize) { it to it } + + return eqGenericTypeParametersConstraint(firstAddr, secondAddr, genericTypeStorageByAddr, *injections) + } + + /** + * Returns a constraint representing that the first type parameter + * of an object with address [firstAddr] is equal to the first + * type parameter of an object with address [secondAddr]. + * + * [genericTypeStorageByAddr] is a storage from which this type information is extracted. + * + * @see UtEqGenericTypeParametersExpression + */ + fun eqGenericSingleTypeParameterConstraint( + firstAddr: UtAddrExpression, + secondAddr: UtAddrExpression, + genericTypeStorageByAddr: Map> + ): Pair = + eqGenericTypeParametersConstraint(firstAddr, secondAddr, genericTypeStorageByAddr, 0 to 0) + + /** + * Creates a memory update for associating provided [typeStorages] with an object with the provided [addr]. + */ + fun createGenericTypeInfoUpdate( + addr: UtAddrExpression, + typeStorages: List, + genericTypeStorageByAddr: Map> + ): MemoryUpdate { + if (addr !in genericTypeStorageByAddr.keys) { + return createUpdateForGenericTypeInfo(addr, typeStorages) + } + + val alreadyAddedTypeStorages = genericTypeStorageByAddr.getValue(addr) + + // Because of the design decision for genericTypeStorage map, it contains a + // mapping from addresses to associated with them type arguments. + // Therefore, first element of the list is a first type argument for the instance, and so on. + // To update type information, we have to update a corresponding type storage. + // Because of that, update is only possible when we have information about all type arguments. + require(typeStorages.size == alreadyAddedTypeStorages.size) { + "Wrong number of type storages is provided," + + " expected ${alreadyAddedTypeStorages.size} arguments," + + " but only ${typeStorages.size} found" + } + + val modifiedTypeStorages = alreadyAddedTypeStorages.mapIndexed { index, typeStorage -> + val newTypeStorage = typeStorages[index] + + val updatedTypes = typeStorage.possibleConcreteTypes.intersect(newTypeStorage.possibleConcreteTypes) + + // TODO should be really the least common type + // we have two type storages and know that one of them is subset of another one. + // Therefore, when we intersect them, we should chose correct least common type among them, + // but we don't do it here since it is not obvious, what is a correct way to do it. + // There is no access from here to typeResolver or Hierarchy, so it need to be + // reconsidered in the future, how to intersect type storages here or extract this function. + // For now we just take a leastCommonType from a type storage that contains less + // possible concrete types. + val alreadyAddedSize = typeStorage.possibleConcreteTypes.size + val newTypesSize = newTypeStorage.possibleConcreteTypes.size + val leastCommonType = if (alreadyAddedSize < newTypesSize) { + typeStorage.leastCommonType + } else { + newTypeStorage.leastCommonType + } + + TypeStorage.constructTypeStorageUnsafe(leastCommonType, updatedTypes) + } + + return createUpdateForGenericTypeInfo(addr, modifiedTypeStorages) + } + } +} + +internal const val NUMBER_OF_PREFERRED_TYPES = 3 + +internal val SootField.isEnumOrdinal + get() = this.name == "ordinal" && this.declaringClass.name == ENUM_CLASSNAME + +internal val ENUM_CLASSNAME: String = java.lang.Enum::class.java.canonicalName +internal val ENUM_ORDINAL = ChunkId(ENUM_CLASSNAME, "ordinal") +internal val CLASS_REF_CLASSNAME: String = Class::class.java.canonicalName +internal val CLASS_REF_CLASS_ID = Class::class.java.id + +internal val CLASS_REF_TYPE_DESCRIPTOR: MemoryChunkDescriptor + get() = MemoryChunkDescriptor( + ChunkId(CLASS_REF_CLASSNAME, "modeledType"), + CLASS_REF_TYPE, + IntType.v() + ) + +internal val CLASS_REF_NUM_DIMENSIONS_DESCRIPTOR: MemoryChunkDescriptor + get() = MemoryChunkDescriptor( + ChunkId(CLASS_REF_CLASSNAME, "modeledNumDimensions"), + CLASS_REF_TYPE, + IntType.v() + ) + +internal val CLASS_REF_SOOT_CLASS: SootClass + get() = Scene.v().getSootClass(CLASS_REF_CLASSNAME) +internal val ARRAYS_SOOT_CLASS: SootClass + get() = Scene.v().getSootClass(java.util.Arrays::class.java.canonicalName) + +internal val OBJECT_TYPE: RefType + get() = Scene.v().getSootClass(Object::class.java.canonicalName).type +internal val THROWABLE_TYPE: RefType + get() = Scene.v().getSootClass(Throwable::class.java.canonicalName).type +internal val STRING_TYPE: RefType + get() = Scene.v().getSootClass(String::class.java.canonicalName).type +internal val STRING_BUILDER_TYPE: RefType + get() = Scene.v().getSootClass(java.lang.StringBuilder::class.java.canonicalName).type +internal val STRING_BUFFER_TYPE: RefType + get() = Scene.v().getSootClass(java.lang.StringBuffer::class.java.canonicalName).type +internal val OPTIONAL_TYPE: RefType + get() = Scene.v().getSootClass(java.util.Optional::class.java.canonicalName).type +internal val OPTIONAL_INT_TYPE: RefType + get() = Scene.v().getSootClass(java.util.OptionalInt::class.java.canonicalName).type +internal val OPTIONAL_LONG_TYPE: RefType + get() = Scene.v().getSootClass(java.util.OptionalLong::class.java.canonicalName).type +internal val OPTIONAL_DOUBLE_TYPE: RefType + get() = Scene.v().getSootClass(java.util.OptionalDouble::class.java.canonicalName).type +internal val CLASS_REF_TYPE: RefType + get() = CLASS_REF_SOOT_CLASS.type +internal val THREAD_TYPE: RefType + get() = Scene.v().getSootClass(Thread::class.java.canonicalName).type +internal val THREAD_GROUP_TYPE: RefType + get() = Scene.v().getSootClass(ThreadGroup::class.java.canonicalName).type +internal val COMPLETABLE_FUTURE_TYPE: RefType + get() = Scene.v().getSootClass(CompletableFuture::class.java.canonicalName).type +internal val EXECUTORS_TYPE: RefType + get() = Scene.v().getSootClass(Executors::class.java.canonicalName).type +internal val EXECUTOR_SERVICE_TYPE: RefType + get() = Scene.v().getSootClass(ExecutorService::class.java.canonicalName).type +internal val COUNT_DOWN_LATCH_TYPE: RefType + get() = Scene.v().getSootClass(CountDownLatch::class.java.canonicalName).type +internal val SECURITY_MANAGER_TYPE: RefType + get() = Scene.v().getSootClass(SecurityManager::class.java.canonicalName).type + +internal val NEW_INSTANCE_SIGNATURE: String = CLASS_REF_SOOT_CLASS.getMethodByName("newInstance").subSignature + +internal val HASHCODE_SIGNATURE: String = + Scene.v() + .getSootClass(Object::class.java.canonicalName) + .getMethodByName(Object::hashCode.name) + .subSignature + +internal val EQUALS_SIGNATURE: String = + Scene.v() + .getSootClass(Object::class.java.canonicalName) + .getMethodByName(Object::equals.name) + .subSignature + +/** + * Represents [java.lang.System.security] field signature. + * Hardcoded string literal because it is differently processed in Android. + */ +internal const val SECURITY_FIELD_SIGNATURE: String = "" + +/** + * Represents [sun.reflect.Reflection.fieldFilterMap] field signature. + * Hardcoded string literal because [sun.reflect.Reflection] is removed in Java 11. + */ +internal const val FIELD_FILTER_MAP_FIELD_SIGNATURE: String = "" + +/** + * Represents [sun.reflect.Reflection.methodFilterMap] field signature. + * Hardcoded string literal because [sun.reflect.Reflection] is removed in Java 11. + */ +internal const val METHOD_FILTER_MAP_FIELD_SIGNATURE: String = "" + +/** + * Special type represents string literal, which is not String Java object + */ +object SeqType : Type() { + override fun toString() = "SeqType" +} 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 index 557c1c7199..ac9001dd39 100644 --- 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 @@ -8,15 +8,11 @@ import org.utbot.framework.util.ConflictTriggers /** * 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(triggers: ConflictTriggers): MockListener(triggers) { +class ForceMockListener private constructor(triggers: ConflictTriggers): MockListener(triggers) { override fun onShouldMock(controller: EngineController, strategy: MockStrategy, mockInfo: UtMockInfo) { - // If force mocking happened -- сancel engine job - controller.job?.cancel(ForceMockCancellationException()) - triggers[Conflict.ForceMockHappened] = true } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/ForceStaticMockListener.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/ForceStaticMockListener.kt index 77ad602e27..4bd3330c46 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/ForceStaticMockListener.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/ForceStaticMockListener.kt @@ -12,21 +12,13 @@ import org.utbot.framework.util.ConflictTriggers /** * Listener for mocker events in [org.utbot.engine.UtBotSymbolicEngine]. - * If forced static mock happened, cancels the engine job. * * Supposed to be created only if Mockito inline is not installed. */ -class ForceStaticMockListener(triggers: ConflictTriggers): MockListener(triggers) { +class ForceStaticMockListener private constructor(triggers: ConflictTriggers): MockListener(triggers) { override fun onShouldMock(controller: EngineController, strategy: MockStrategy, mockInfo: UtMockInfo) { - if (mockInfo is UtNewInstanceMockInfo - || mockInfo is UtStaticMethodMockInfo - || mockInfo is UtStaticObjectMockInfo) { - // If force static mocking happened -- сancel engine job - controller.job?.cancel(ForceStaticMockCancellationException()) - triggers[Conflict.ForceStaticMockHappened] = true } - } companion object { fun create(testCaseGenerator: TestCaseGenerator, conflictTriggers: ConflictTriggers) : ForceStaticMockListener { 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 index f4ca0a88be..4838dbe940 100644 --- 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 @@ -9,7 +9,9 @@ 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.engine.types.TypeResolver import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.fieldId import org.utbot.framework.plugin.api.util.jField import soot.SootClass import soot.SootField diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/util/trusted/TrustedPackages.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/util/trusted/TrustedPackages.kt deleted file mode 100644 index 78584ae2c2..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/util/trusted/TrustedPackages.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.utbot.engine.util.trusted - -import org.utbot.framework.TrustedLibraries -import soot.SootClass - -/** - * Cache for already discovered trusted/untrusted packages. - */ -private val isPackageTrusted: MutableMap = mutableMapOf() - -/** - * Determines whether [this] class is from trusted libraries as defined in [TrustedLibraries]. - */ -fun SootClass.isFromTrustedLibrary(): Boolean { - isPackageTrusted[packageName]?.let { - return it - } - - val isTrusted = TrustedLibraries.trustedLibraries.any { packageName.startsWith(it, ignoreCase = false) } - - return isTrusted.also { isPackageTrusted[packageName] = it } -} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Extensions.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Extensions.kt index 7fc3dc0904..179bf82133 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Extensions.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Extensions.kt @@ -32,15 +32,15 @@ import soot.Type private val Z3Variable.unsigned: Boolean get() = this.type is CharType -fun Expr.value(unsigned: Boolean = false): Any = when (this) { +fun Expr<*>.value(unsigned: Boolean = false): Any = when (this) { is BitVecNum -> this.toIntNum(unsigned) is FPNum -> this.toFloatingPointNum() is BoolExpr -> this.boolValue == Z3_lbool.Z3_L_TRUE - is SeqExpr -> convertSolverString(this.string) + is SeqExpr<*> -> convertSolverString(this.string) else -> error("${this::class}: $this") } -internal fun Expr.intValue() = this.value() as Int +internal fun Expr<*>.intValue() = this.value() as Int /** * Converts a variable to given type. @@ -64,7 +64,7 @@ fun Context.convertVar(variable: Z3Variable, toType: Type): Z3Variable { * * @throws IllegalStateException if given expression cannot be convert to given sort */ -internal fun Context.convertVar(variable: Z3Variable, sort: Sort): Expr { +internal fun Context.convertVar(variable: Z3Variable, sort: Sort): Expr<*> { val expr = variable.expr return when { sort == expr.sort -> expr @@ -94,7 +94,7 @@ internal fun Context.convertVar(variable: Z3Variable, sort: Sort): Expr { * - convert to int for other types, with second step conversion by taking lowest bits * (can lead to significant changes if value outside of target type range). */ -private fun Context.convertFPtoBV(variable: Z3Variable, sortSize: Int): Expr = when (sortSize) { +private fun Context.convertFPtoBV(variable: Z3Variable, sortSize: Int): Expr<*> = when (sortSize) { Long.SIZE_BITS -> convertFPtoBV(variable, sortSize, Long.MIN_VALUE, Long.MAX_VALUE) else -> doNarrowConversion( convertFPtoBV(variable, Int.SIZE_BITS, Int.MIN_VALUE, Int.MAX_VALUE) as BitVecExpr, @@ -105,7 +105,7 @@ private fun Context.convertFPtoBV(variable: Z3Variable, sortSize: Int): Expr = w /** * Converts FP to BV covering special cases for NaN, Infinity and values outside of [minValue, maxValue] range. */ -private fun Context.convertFPtoBV(variable: Z3Variable, sortSize: Int, minValue: Number, maxValue: Number): Expr = +private fun Context.convertFPtoBV(variable: Z3Variable, sortSize: Int, minValue: Number, maxValue: Number): Expr<*> = mkITE( mkFPIsNaN(variable.expr as FPExpr), makeConst(0, mkBitVecSort(sortSize)), mkITE( @@ -128,7 +128,7 @@ internal fun Context.doNarrowConversion(expr: BitVecExpr, sortSize: Int): BitVec /** * Converts a constant value to Z3 constant of given sort - BitVec or FP. */ -fun Context.makeConst(const: Number, sort: Sort): Expr = when (sort) { +fun Context.makeConst(const: Number, sort: Sort): Expr<*> = when (sort) { is BitVecSort -> this.makeBV(const, sort.size) is FPSort -> this.makeFP(const, sort) else -> error("Wrong sort $sort") diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Operators.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Operators.kt index 01ed0cb2e6..38f120c674 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Operators.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Operators.kt @@ -15,9 +15,9 @@ import soot.CharType import soot.IntType import soot.ShortType -typealias BinOperator = Operator +typealias BinOperator = Operator> -sealed class Operator( +sealed class Operator>( val onBitVec: (Context, BitVecExpr, BitVecExpr) -> T, val onFP: (Context, FPExpr, FPExpr) -> T = { _, _, _ -> TODO() }, val onBool: (Context, BoolExpr, BoolExpr) -> T = { _, _, _ -> TODO() } @@ -32,7 +32,7 @@ sealed class Operator( doAction(context, aleft, aright) } - protected fun doAction(context: Context, left: Expr, right: Expr): T = when { + protected fun doAction(context: Context, left: Expr<*>, right: Expr<*>): T = when { left is BitVecExpr && right is BitVecExpr -> onBitVec(context, left, right) left is FPExpr && right is FPExpr -> onFP(context, left, right) left is BoolExpr && right is BoolExpr -> onBool(context, left, right) @@ -55,7 +55,7 @@ object Gt : BoolOperator(Context::mkBVSGT, Context::mkFPGt) object Eq : BoolOperator(Context::mkEq, Context::mkFPEq, Context::mkEq) object Ne : BoolOperator(::ne, ::fpNe, ::ne) -private fun ne(context: Context, left: Expr, right: Expr): BoolExpr = context.mkNot(context.mkEq(left, right)) +private fun ne(context: Context, left: Expr<*>, right: Expr<*>): BoolExpr = context.mkNot(context.mkEq(left, right)) private fun fpNe(context: Context, left: FPExpr, right: FPExpr): BoolExpr = context.mkNot(context.mkFPEq(left, right)) internal object Rem : BinOperator(Context::mkBVSRem, Context::mkFPRem) @@ -129,7 +129,7 @@ internal object Cmp : BinOperator(::bvCmp, ::fpCmp) internal object Cmpl : BinOperator(::bvCmp, ::fpCmpl) internal object Cmpg : BinOperator(::bvCmp, ::fpCmpg) -private fun bvCmp(context: Context, left: BitVecExpr, right: BitVecExpr): Expr = +private fun bvCmp(context: Context, left: BitVecExpr, right: BitVecExpr): Expr<*> = context.mkITE( context.mkBVSLE(left, right), context.mkITE(context.mkEq(left, right), context.mkBV(0, 32), context.mkBV(-1, 32)), @@ -146,7 +146,7 @@ private fun fpCmp(context: Context, left: FPExpr, right: FPExpr) = context.mkBV(1, 32) ) -private fun fpCmpl(context: Context, left: FPExpr, right: FPExpr): Expr = +private fun fpCmpl(context: Context, left: FPExpr, right: FPExpr): Expr<*> = context.mkITE( context.mkOr(context.mkFPIsNaN(left), context.mkFPIsNaN(right)), context.mkBV(-1, 32), context.mkITE( @@ -156,7 +156,7 @@ private fun fpCmpl(context: Context, left: FPExpr, right: FPExpr): Expr = ) ) -private fun fpCmpg(context: Context, left: FPExpr, right: FPExpr): Expr = +private fun fpCmpg(context: Context, left: FPExpr, right: FPExpr): Expr<*> = context.mkITE( context.mkOr(context.mkFPIsNaN(left), context.mkFPIsNaN(right)), context.mkBV(1, 32), context.mkITE( @@ -166,7 +166,7 @@ private fun fpCmpg(context: Context, left: FPExpr, right: FPExpr): Expr = ) ) -fun negate(context: Context, variable: Z3Variable): Expr = when (variable.expr) { +fun negate(context: Context, variable: Z3Variable): Expr<*> = when (variable.expr) { is BitVecExpr -> context.mkBVNeg(context.alignVar(variable).expr as BitVecExpr) is FPExpr -> context.mkFPNeg(variable.expr) is BoolExpr -> context.mkNot(variable.expr) @@ -183,7 +183,7 @@ fun negate(context: Context, variable: Z3Variable): Expr = when (variable.expr) * @see * Java Language Specification: Binary Numeric Promotion */ -fun Context.alignVars(left: Z3Variable, right: Z3Variable): Pair { +fun Context.alignVars(left: Z3Variable, right: Z3Variable): Pair, Expr<*>> { val maxSort = maxOf(left.expr.sort, right.expr.sort, mkBitVecSort(Int.SIZE_BITS), compareBy { it.rank() }) return convertVar(left, maxSort) to convertVar(right, maxSort) } @@ -198,7 +198,7 @@ fun Context.alignVars(left: Z3Variable, right: Z3Variable): Pair { * @see * Java Language Specification: Binary Numeric Promotion */ -internal fun Context.alignVarAndConst(left: Z3Variable, right: Number): Pair { +internal fun Context.alignVarAndConst(left: Z3Variable, right: Number): Pair, Expr<*>> { val maxSort = maxOf(left.expr.sort, toSort(right), mkBitVecSort(Int.SIZE_BITS), compareBy { it.rank() }) return convertVar(left, maxSort) to makeConst(right, maxSort) } 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 97677c78cd..09b88308b1 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 @@ -4,10 +4,9 @@ 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 abstract class Z3Initializer : AutoCloseable { - protected val context: Context by lazy { + private val contextDelegate = lazy { Context().also { // Global.setParameter("smt.core.minimize", "true") Global.setParameter("rewriter.hi_fp_unspecified", "true") @@ -15,39 +14,49 @@ abstract class Z3Initializer : AutoCloseable { Global.setParameter("parallel.threads.max", "4") } } + protected val context: Context by contextDelegate - override fun close() = context.close() + override fun close() { + if (contextDelegate.isInitialized()) { + context.close() + } + } companion object { private val libraries = listOf("libz3", "libz3java") private val vcWinLibrariesToLoadBefore = listOf("vcruntime140", "vcruntime140_1") - private val supportedArchs = setOf("amd64", "x86_64") + private val supportedArchs = setOf("amd64", "x86_64", "aarch64") private val initializeCallback by lazy { System.setProperty("z3.skipLibraryLoad", "true") - val arch = System.getProperty("os.arch") - require(arch in supportedArchs) { "Not supported arch: $arch" } + val archProperty = System.getProperty("os.arch") + require(archProperty in supportedArchs) { "Not supported arch: $archProperty" } - 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("mac") -> ".dylib" to libraries + val osProperty = System.getProperty("os.name").lowercase() + val (os, ext, allLibraries) = when { + osProperty.startsWith("windows") -> Triple("windows", ".dll", vcWinLibrariesToLoadBefore + libraries) + osProperty.startsWith("linux") -> Triple("linux", ".so", libraries) + osProperty.startsWith("mac") -> Triple("mac", ".dylib", libraries) else -> error("Unknown OS: $osProperty") } - val libZ3DllUrl = Z3Initializer::class.java + + val arch = if (archProperty == "aarch64") "arm" else "x64" + val z3DistFolder = "lib/$os/$arch/z3" + + val libZ3FilesUrl = Z3Initializer::class.java .classLoader - .getResource("lib/x64/libz3.dll") ?: error("Can't find native library folder") + .getResource("$z3DistFolder/libz3$ext") + ?: error("Can't find native library '$z3DistFolder/libz3$ext' in ${System.getProperty("java.class.path")}") // 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") { + if (libZ3FilesUrl.toURI().scheme == "jar") { val tempDir = FileUtil.createTempDirectory("libs-").toFile() allLibraries.forEach { name -> Z3Initializer::class.java .classLoader - .getResourceAsStream("lib/x64/$name$ext") + .getResourceAsStream("$z3DistFolder/$name$ext") ?.use { input -> File(tempDir, "$name$ext") .outputStream() @@ -57,7 +66,7 @@ abstract class Z3Initializer : AutoCloseable { libFolder = "$tempDir" } else { - libFolder = File(libZ3DllUrl.file).parent + libFolder = File(libZ3FilesUrl.file).parent } allLibraries.forEach { System.load("$libFolder/$it$ext") } diff --git a/utbot-framework/src/main/kotlin/org/utbot/external/api/UnitTestBotLight.kt b/utbot-framework/src/main/kotlin/org/utbot/external/api/UnitTestBotLight.kt new file mode 100644 index 0000000000..4faeef7027 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/external/api/UnitTestBotLight.kt @@ -0,0 +1,92 @@ +package org.utbot.external.api + +import org.utbot.engine.EngineController +import org.utbot.engine.ExecutionStateListener +import org.utbot.engine.MockStrategy +import org.utbot.engine.UtBotSymbolicEngine +import org.utbot.framework.UtSettings +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.context.simple.SimpleApplicationContext +import org.utbot.framework.context.simple.SimpleConcreteExecutionContext +import org.utbot.framework.context.simple.SimpleMockerContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.framework.plugin.services.JdkInfoDefaultProvider +import org.utbot.framework.util.SootUtils.runSoot +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation +import java.io.File +import java.nio.file.Paths + + +@Suppress("unused") +object UnitTestBotLight { + + /** + * Creates an instance of symbolic engine with default values + * w/o specific objects like + * - [ApplicationContext] + * - [UtExecutionInstrumentation] + * - [EngineController] + * - etc. + * + * @return symbolic engine instance + */ + @JvmStatic + fun javaUtBotSymbolicEngine( + methodUnderTest: ExecutableId, + classpath: String, + mockStrategy: MockStrategy, + chosenClassesToMockAlways: Set, + ) = UtBotSymbolicEngine( + controller = EngineController(), + methodUnderTest = methodUnderTest, + classpath = classpath, + dependencyPaths = "", + mockStrategy = mockStrategy, + chosenClassesToMockAlways = chosenClassesToMockAlways, + applicationContext = SimpleApplicationContext(SimpleMockerContext( + mockFrameworkInstalled = true, + staticsMockingIsConfigured = true + )), + concreteExecutionContext = SimpleConcreteExecutionContext(classpath), + solverTimeoutInMillis = UtSettings.checkSolverTimeoutMillis + ) + + @JvmStatic + @JvmOverloads + fun run ( + stateListener: ExecutionStateListener, + methodForAutomaticGeneration: TestMethodInfo, + classpath: String, + dependencyClassPath: String, + mockStrategyApi: MockStrategyApi = MockStrategyApi.OTHER_PACKAGES, + ) { + // init Soot if it is not yet + runSoot(classpath.split(File.pathSeparator).map(Paths::get), classpath, false, JdkInfoDefaultProvider().info) + val classLoader = Thread.currentThread().contextClassLoader + val utContext = UtContext(classLoader) + withUtContext(utContext) { + UtBotSymbolicEngine( + EngineController(), + methodForAutomaticGeneration.methodToBeTestedFromUserInput.executableId, + classpath, + dependencyClassPath, + when (mockStrategyApi) { + MockStrategyApi.NO_MOCKS -> MockStrategy.NO_MOCKS + MockStrategyApi.OTHER_PACKAGES -> MockStrategy.OTHER_PACKAGES + MockStrategyApi.OTHER_CLASSES -> MockStrategy.OTHER_CLASSES + }, + HashSet(), + SimpleApplicationContext(SimpleMockerContext( + mockFrameworkInstalled = true, + staticsMockingIsConfigured = true + )), + SimpleConcreteExecutionContext(classpath) + ).addListener(stateListener).traverseAll() + } + } +} \ No newline at end of file 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 4871fdfc84..ecfb1c1634 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 @@ -3,23 +3,14 @@ package org.utbot.external.api import org.utbot.common.FileUtil import org.utbot.common.nameOfPackage import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.Junit5 -import org.utbot.framework.codegen.NoStaticMocking -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.model.CodeGenerator -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.TestCaseGenerator -import org.utbot.framework.plugin.api.UtMethodTestSet -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.UtSymbolicExecution +import org.utbot.framework.codegen.domain.* +import org.utbot.framework.codegen.generator.CodeGeneratorParams +import org.utbot.framework.codegen.services.language.CgLanguageAssistant +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.context.utils.transformValueProvider +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionData +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.executableId import org.utbot.framework.plugin.api.util.id @@ -32,43 +23,77 @@ import org.utbot.framework.plugin.api.util.withUtContext import org.utbot.framework.plugin.api.util.wrapperByPrimitive import org.utbot.framework.plugin.services.JdkInfoDefaultProvider import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Seed import org.utbot.instrumentation.ConcreteExecutor import org.utbot.instrumentation.execute import kotlin.reflect.jvm.kotlinFunction +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.plugin.api.* +import java.lang.reflect.Method object UtBotJavaApi { + /** + * For running tests it could be reasonable to reuse the same concrete executor + */ @JvmStatic var stopConcreteExecutorOnExit: Boolean = true + /** + * Generates test code + * @param methodsForGeneration specify methods that are supposed to be executed concretely. + * In order to execute method you are supposed to provide some + * values to pass in it this is why we use [TestMethodInfo] here. + * @param generatedTestCases specify [UtMethodTestSet]s that are used for test code + * generation. By comparison with the first parameter, + * {@code UtMethodTestSet} contains more information about + * test, including result of the executions. Note, that + * you can get the object with any sort of analysis, + * for instance, symbolic or fuzz execution. + * @param destinationClassName the name of containing class for the generated tests + * @param classpath classpath that are used to build the class under test + * @param dependencyClassPath class path including dependencies required for the code generation + * @param classUnderTest for this class test should be generated + * @param projectType JVM, Spring, Python, or other type of project + * @param testFramework test framework that is going to be used for running generated tests + * @param mockFramework framework that will be used in the generated tests + * @param codegenLanguage the target language of the test generation. It can be different from the source language. + * @param staticsMocking the approach to the statics mocking + * @param generateWarningsForStaticMocking enable generation of warning about forced static mocking in comments + * of generated tests. + * @param forceStaticMocking enables static mocking + * @param testClassPackageName package name for the generated class with the tests + * @param applicationContext specify application context here + */ @JvmStatic @JvmOverloads - fun generate( + fun generateTestCode( methodsForGeneration: List, generatedTestCases: List = mutableListOf(), destinationClassName: String, classpath: String, dependencyClassPath: String, classUnderTest: Class<*>, + projectType: ProjectType = ProjectType.PureJvm, testFramework: TestFramework = Junit5, mockFramework: MockFramework = MockFramework.MOCKITO, codegenLanguage: CodegenLanguage = CodegenLanguage.JAVA, staticsMocking: StaticsMocking = NoStaticMocking, generateWarningsForStaticMocking: Boolean = false, forceStaticMocking: ForceStaticMocking = ForceStaticMocking.DO_NOT_FORCE, - testClassPackageName: String = classUnderTest.nameOfPackage + testClassPackageName: String = classUnderTest.nameOfPackage, + applicationContext: ApplicationContext ): String { - val utContext = UtContext(classUnderTest.classLoader) - val testSets: MutableList = generatedTestCases.toMutableList() val concreteExecutor = ConcreteExecutor( - UtExecutionInstrumentation, - classpath, - dependencyClassPath + applicationContext.createConcreteExecutionContext( + fullClasspath = dependencyClassPath, + classpathWithoutDependencies = classpath + ).instrumentationFactory, + classpath ) testSets.addAll(generateUnitTests(concreteExecutor, methodsForGeneration, classUnderTest)) @@ -77,19 +102,21 @@ object UtBotJavaApi { concreteExecutor.close() } - return withUtContext(utContext) { - val codeGenerator = CodeGenerator( + return withUtContext(UtContext(classUnderTest.classLoader)) { + applicationContext.createCodeGenerator( + CodeGeneratorParams( classUnderTest = classUnderTest.id, + projectType = projectType, testFramework = testFramework, mockFramework = mockFramework, codegenLanguage = codegenLanguage, + cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(codegenLanguage), staticsMocking = staticsMocking, forceStaticMocking = forceStaticMocking, generateWarningsForStaticMocking = generateWarningsForStaticMocking, - testClassPackageName = testClassPackageName + testClassPackageName = testClassPackageName, ) - - codeGenerator.generateAsString(testSets, destinationClassName) + ).generateAsString(testSets, destinationClassName) } } @@ -100,29 +127,36 @@ object UtBotJavaApi { */ @JvmStatic @JvmOverloads - fun generateTestSets( - methodsForAutomaticGeneration: List, + fun generateTestSetsForMethods( + methodsToAnalyze: List, classUnderTest: Class<*>, classpath: String, dependencyClassPath: String, mockStrategyApi: MockStrategyApi = MockStrategyApi.OTHER_PACKAGES, - generationTimeoutInMillis: Long = UtSettings.utBotGenerationTimeoutInMillis + generationTimeoutInMillis: Long = UtSettings.utBotGenerationTimeoutInMillis, + applicationContext: ApplicationContext ): MutableList { + assert(methodsToAnalyze.all {classUnderTest.declaredMethods.contains(it)}) + { "Some methods are absent in the ${classUnderTest.name} class." } + val utContext = UtContext(classUnderTest.classLoader) val testSets: MutableList = mutableListOf() testSets.addAll(withUtContext(utContext) { val buildPath = FileUtil.isolateClassFiles(classUnderTest).toPath() - TestCaseGenerator(listOf(buildPath), classpath, dependencyClassPath, jdkInfo = JdkInfoDefaultProvider().info) - .generate( - methodsForAutomaticGeneration.map { - it.methodToBeTestedFromUserInput.executableId - }, - mockStrategyApi, - chosenClassesToMockAlways = emptySet(), - generationTimeoutInMillis - ) + TestCaseGenerator( + listOf(buildPath), + classpath, + dependencyClassPath, + jdkInfo = JdkInfoDefaultProvider().info, + applicationContext = applicationContext + ).generate( + methodsToAnalyze.map { it.executableId }, + mockStrategyApi, + chosenClassesToMockAlways = emptySet(), + generationTimeoutInMillis + ) }) return testSets @@ -131,19 +165,24 @@ object UtBotJavaApi { /** * Generates test cases using only fuzzing workflow. * - * @see [generateTestSets] + * @see [generateTestSetsForMethods] */ @JvmStatic @JvmOverloads fun fuzzingTestSets( - methodsForAutomaticGeneration: List, + methodsToAnalyze: List, classUnderTest: Class<*>, classpath: String, dependencyClassPath: String, mockStrategyApi: MockStrategyApi = MockStrategyApi.OTHER_PACKAGES, generationTimeoutInMillis: Long = UtSettings.utBotGenerationTimeoutInMillis, - primitiveValuesSupplier: CustomFuzzerValueSupplier = CustomFuzzerValueSupplier { null } + primitiveValuesSupplier: CustomFuzzerValueSupplier = CustomFuzzerValueSupplier { null }, + applicationContext: ApplicationContext ): MutableList { + + assert(methodsToAnalyze.all {classUnderTest.declaredMethods.contains(it)}) + { "Some methods are absent in the ${classUnderTest.name} class." } + fun createPrimitiveModels(supplier: CustomFuzzerValueSupplier, classId: ClassId): Sequence = supplier .takeIf { classId.isPrimitive || classId.isPrimitiveWrapper || classId == stringClassId } @@ -160,33 +199,31 @@ object UtBotJavaApi { } ?.map { UtPrimitiveModel(it) } ?: emptySequence() - val customModelProvider = ModelProvider { description -> + val customModelProvider = JavaValueProvider { _, type -> sequence { - description.parametersMap.forEach { (classId, indices) -> - createPrimitiveModels(primitiveValuesSupplier, classId).forEach { model -> - indices.forEach { index -> - yieldValue(index, FuzzedValue(model)) - } - } + createPrimitiveModels(primitiveValuesSupplier, type.classId).forEach { model -> + yield(Seed.Simple(FuzzedValue(model))) } } } return withUtContext(UtContext(classUnderTest.classLoader)) { val buildPath = FileUtil.isolateClassFiles(classUnderTest).toPath() - TestCaseGenerator(listOf(buildPath), classpath, dependencyClassPath, jdkInfo = JdkInfoDefaultProvider().info) + TestCaseGenerator( + listOf(buildPath), + classpath, + dependencyClassPath, + jdkInfo = JdkInfoDefaultProvider().info, + applicationContext = applicationContext.transformValueProvider { defaultModelProvider -> + customModelProvider.withFallback(defaultModelProvider) + } + ) .generate( - methodsForAutomaticGeneration.map { - it.methodToBeTestedFromUserInput.executableId - }, + methodsToAnalyze.map{ it.executableId }, mockStrategyApi, chosenClassesToMockAlways = emptySet(), generationTimeoutInMillis, - generate = { symbolicEngine -> - symbolicEngine.fuzzing { defaultModelProvider -> - customModelProvider.withFallback(defaultModelProvider) - } - } + generate = { symbolicEngine -> symbolicEngine.fuzzing() } ) }.toMutableList() } @@ -222,7 +259,9 @@ object UtBotJavaApi { arrayOf(), parameters = UtConcreteExecutionData( testInfo.initialState, - instrumentation = emptyList() + instrumentation = emptyList(), + UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis, + isRerun = false, ) ).result } else { diff --git a/utbot-framework/src/main/kotlin/org/utbot/external/api/UtModelFactory.kt b/utbot-framework/src/main/kotlin/org/utbot/external/api/UtModelFactory.kt index e7c403570c..acb9e55c5f 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/external/api/UtModelFactory.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/external/api/UtModelFactory.kt @@ -69,7 +69,7 @@ class UtModelFactory( fun produceClassRefModel(clazz: Class<*>) = UtClassRefModel( modelIdCounter.incrementAndGet(), classIdForType(clazz), - clazz + clazz.id ) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt index bd454e3e83..39af79204a 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 @@ -1,15 +1,16 @@ package org.utbot.framework.assemble +import mu.KotlinLogging import org.utbot.common.isPrivate import org.utbot.common.isPublic import org.utbot.engine.ResolvedExecution import org.utbot.engine.ResolvedModels import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.model.util.isAccessibleFrom -import org.utbot.framework.modifications.AnalysisMode.SettersAndDirectAccessors -import org.utbot.framework.modifications.ConstructorAnalyzer -import org.utbot.framework.modifications.ConstructorAssembleInfo -import org.utbot.framework.modifications.UtBotFieldsModificatorsSearcher +import org.utbot.framework.codegen.util.isAccessibleFrom +import org.utbot.modifications.AnalysisMode.SettersAndDirectAccessors +import org.utbot.modifications.ExecutableAnalyzer +import org.utbot.modifications.ExecutableAssembleInfo +import org.utbot.modifications.UtBotFieldsModificatorsSearcher import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId import org.utbot.framework.plugin.api.DirectFieldAccessId @@ -20,6 +21,8 @@ import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtClassRefModel import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtCustomModel +import org.utbot.framework.plugin.api.UtDirectGetFieldModel import org.utbot.framework.plugin.api.UtDirectSetFieldModel import org.utbot.framework.plugin.api.UtEnumConstantModel import org.utbot.framework.plugin.api.UtExecutableCallModel @@ -29,6 +32,7 @@ import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation 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.UtStatementCallModel import org.utbot.framework.plugin.api.UtStatementModel import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation import org.utbot.framework.plugin.api.UtVoidModel @@ -36,11 +40,15 @@ import org.utbot.framework.plugin.api.hasDefaultValue import org.utbot.framework.plugin.api.isMockModel import org.utbot.framework.plugin.api.util.defaultValueModel import org.utbot.framework.plugin.api.util.executableId -import org.utbot.framework.plugin.api.util.isSubtypeOf +import org.utbot.framework.plugin.api.util.isFinal +import org.utbot.framework.plugin.api.util.isPrivate +import org.utbot.framework.plugin.api.util.isPublic +import org.utbot.framework.plugin.api.util.isStatic import org.utbot.framework.plugin.api.util.jClass import org.utbot.framework.util.nextModelName import java.lang.reflect.Constructor import java.util.IdentityHashMap +import org.utbot.modifications.FieldInvolvementMode /** * Creates [UtAssembleModel] from any [UtModel] or it's inner models if possible @@ -52,6 +60,10 @@ import java.util.IdentityHashMap */ class AssembleModelGenerator(private val basePackageName: String) { + companion object { + private val logger = KotlinLogging.logger {} + } + //Instantiated models are stored to avoid cyclic references during reference graph analysis private val instantiatedModels: IdentityHashMap = IdentityHashMap() @@ -61,8 +73,11 @@ class AssembleModelGenerator(private val basePackageName: String) { //Call chain of statements to create assemble model private var callChain = mutableListOf() - private val modificatorsSearcher = UtBotFieldsModificatorsSearcher() - private val constructorAnalyzer = ConstructorAnalyzer() + private val modificatorsSearcher = + UtBotFieldsModificatorsSearcher( + fieldInvolvementMode = FieldInvolvementMode.WriteOnly + ) + private val executableAnalyzer = ExecutableAnalyzer() /** * Clears state before and after block execution. @@ -172,8 +187,13 @@ class AssembleModelGenerator(private val basePackageName: String) { private fun assembleModel(utModel: UtModel): UtModel { val collectedCallChain = callChain.toMutableList() - // We cannot create an assemble model for an anonymous class instance - if (utModel.classId.isAnonymous) { + try { + // We cannot create an assemble model for an anonymous class instance + if (utModel.classId.isAnonymous) { + return utModel + } + } catch (e: ClassNotFoundException) { + // happens, for example, when `utModel.classId.name` is something like "jdk.proxy3.$Proxy144" return utModel } @@ -185,10 +205,13 @@ class AssembleModelGenerator(private val basePackageName: String) { is UtClassRefModel, is UtVoidModel, is UtEnumConstantModel, - is UtLambdaModel -> utModel + is UtCustomModel -> utModel // for example, UtSpringContextModel + is UtLambdaModel -> assembleLambdaModel(utModel) is UtArrayModel -> assembleArrayModel(utModel) is UtCompositeModel -> assembleCompositeModel(utModel) is UtAssembleModel -> assembleAssembleModel(utModel) + // Python, JavaScript are supposed to be here as well + else -> utModel } } catch (e: AssembleException) { utModel @@ -199,6 +222,18 @@ class AssembleModelGenerator(private val basePackageName: String) { return assembledModel } + private fun assembleLambdaModel(lambdaModel: UtLambdaModel): UtModel { + instantiatedModels[lambdaModel]?.let { return it } + + return UtLambdaModel( + lambdaModel.id, + lambdaModel.samType, + lambdaModel.declaringClass, + lambdaModel.lambdaName, + capturedValues = lambdaModel.capturedValues.map { assembleModel(it) }.toMutableList(), + ) + } + /** * Assembles internal structure of [UtArrayModel]. */ @@ -213,14 +248,19 @@ class AssembleModelGenerator(private val basePackageName: String) { instantiatedModels[this] = assembleModel - assembleModel.constModel = assembleModel(constModel) assembleModel.stores += stores .mapValues { assembleModel(it.value) } .toMutableMap() + if (arrayModel.stores.count() < arrayModel.length) { + assembleModel.constModel = assembleModel(constModel) + } + assembleModel } + private val modelsInAnalysis = mutableListOf() + /** * Assembles internal structure of [UtCompositeModel] if possible and handles assembling exceptions. */ @@ -236,9 +276,16 @@ class AssembleModelGenerator(private val basePackageName: String) { val modelName = nextModelName(compositeModel.classId.jClass.simpleName.decapitalize()) val constructorId = findBestConstructorOrNull(compositeModel) - ?: throw AssembleException("No default constructor to instantiate an object of the class ${compositeModel.id}") + ?: throw AssembleException("No default constructor to instantiate an object of the class ${compositeModel.classId}") + + // we do not analyze a constructor which is currently in the analysis + // thus, we do not encounter an infinite loop in self or cross-reference situations + val shouldAnalyzeConstructor = compositeModel !in modelsInAnalysis + modelsInAnalysis.add(compositeModel) - val constructorInfo = constructorAnalyzer.analyze(constructorId) + val constructorInfo = + if (shouldAnalyzeConstructor) executableAnalyzer.analyze(constructorId) + else ExecutableAssembleInfo(constructorId) val instantiationCall = constructorCall(compositeModel, constructorInfo) return UtAssembleModel( @@ -251,25 +298,27 @@ class AssembleModelGenerator(private val basePackageName: String) { instantiatedModels[compositeModel] = this compositeModel.fields.forEach { (fieldId, fieldModel) -> + //if field value has been filled by constructor or it is default, we suppose that it is already properly initialized + if ((fieldId in constructorInfo.setFields || fieldModel.hasDefaultValue()) && fieldId !in constructorInfo.affectedFields) + return@forEach + if (fieldId.isStatic) { throw AssembleException("Static field $fieldId can't be set in an object of the class $classId") } - if (fieldId.isFinal) { - throw AssembleException("Final field $fieldId can't be set in an object of the class $classId") - } + if (!fieldId.type.isAccessibleFrom(basePackageName)) { throw AssembleException( "Field $fieldId can't be set in an object of the class $classId because its type is inaccessible" ) } - //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()) - ) { - val assembledModel = assembleModel(fieldModel) - val modifierCall = modifierCall(this, fieldId, assembledModel) - callChain.add(modifierCall) + + if (fieldId.isFinal) { + throw AssembleException("Final field $fieldId can't be set in an object of the class $classId") } + + val assembledModel = assembleModel(fieldModel) + val modifierCall = modifierCall(this, fieldId, assembledModel) + callChain.add(modifierCall) } callChain.toList() @@ -277,6 +326,8 @@ class AssembleModelGenerator(private val basePackageName: String) { } catch (e: AssembleException) { instantiatedModels.remove(compositeModel) throw e + } finally { + modelsInAnalysis.remove(compositeModel) } } @@ -286,12 +337,11 @@ class AssembleModelGenerator(private val basePackageName: String) { private fun assembleAssembleModel(modelBefore: UtAssembleModel): UtModel { instantiatedModels[modelBefore]?.let { return it } - return UtAssembleModel( modelBefore.id, modelBefore.classId, modelBefore.modelName, - assembleExecutableCallModel(modelBefore.instantiationCall), + assembleStatementCallModel(modelBefore.instantiationCall), modelBefore.origin ) { instantiatedModels[modelBefore] = this @@ -303,7 +353,7 @@ class AssembleModelGenerator(private val basePackageName: String) { * Assembles internal structure of [UtStatementModel]. */ private fun assembleStatementModel(statementModel: UtStatementModel): UtStatementModel = when (statementModel) { - is UtExecutableCallModel -> assembleExecutableCallModel(statementModel) + is UtStatementCallModel -> assembleStatementCallModel(statementModel) is UtDirectSetFieldModel -> assembleDirectSetFieldModel(statementModel) } @@ -313,11 +363,16 @@ class AssembleModelGenerator(private val basePackageName: String) { fieldModel = assembleModel(statementModel.fieldModel) ) - private fun assembleExecutableCallModel(statementModel: UtExecutableCallModel) = - statementModel.copy( - instance = statementModel.instance?.let { assembleModel(it) as UtReferenceModel }, - params = statementModel.params.map { assembleModel(it) } - ) + private fun assembleStatementCallModel(statementModel: UtStatementCallModel) = + when (statementModel) { + is UtExecutableCallModel-> statementModel.copy( + instance = statementModel.instance?.let { assembleModel(it) as UtReferenceModel }, + params = statementModel.params.map { assembleModel(it) } + ) + is UtDirectGetFieldModel -> statementModel.copy( + instance = assembleModel(statementModel.instance) as UtReferenceModel + ) + } /** * Assembles internal structure of [UtCompositeModel] if it represents a mock. @@ -325,23 +380,23 @@ class AssembleModelGenerator(private val basePackageName: String) { private fun assembleMockCompositeModel(compositeModel: UtCompositeModel): UtCompositeModel { // We have to create a model before the construction of the fields to avoid // infinite recursion when some mock contains itself as a field. - val assembledModel = UtCompositeModel( + val assembledCompositeModel = UtCompositeModel( compositeModel.id, compositeModel.classId, isMock = true, ) - instantiatedModels[compositeModel] = assembledModel + instantiatedModels[compositeModel] = assembledCompositeModel val fields = compositeModel.fields.mapValues { assembleModel(it.value) }.toMutableMap() val mockBehaviour = compositeModel.mocks .mapValues { models -> models.value.map { assembleModel(it) } } .toMutableMap() - assembledModel.fields += fields - assembledModel.mocks += mockBehaviour + assembledCompositeModel.fields += fields + assembledCompositeModel.mocks += mockBehaviour - return assembledModel + return assembledCompositeModel } /** @@ -351,9 +406,9 @@ class AssembleModelGenerator(private val basePackageName: String) { */ private fun constructorCall( compositeModel: UtCompositeModel, - constructorInfo: ConstructorAssembleInfo, + constructorInfo: ExecutableAssembleInfo, ): UtExecutableCallModel { - val constructorParams = constructorInfo.constructorId.parameters.withIndex() + val constructorParams = constructorInfo.executableId.parameters.withIndex() .map { (index, param) -> val modelOrNull = compositeModel.fields .filter { it.key == constructorInfo.params[index] } @@ -363,7 +418,7 @@ class AssembleModelGenerator(private val basePackageName: String) { assembleModel(fieldModel) } - return UtExecutableCallModel(instance = null, constructorInfo.constructorId, constructorParams) + return UtExecutableCallModel(instance = null, constructorInfo.executableId, constructorParams) } /** @@ -390,11 +445,11 @@ class AssembleModelGenerator(private val basePackageName: String) { val fromUtilPackage = classId.packageName.startsWith("java.util") constructorIds .sortedBy { it.parameters.size } - .firstOrNull { it.parameters.isEmpty() && fromUtilPackage || constructorAnalyzer.isAppropriate(it) } + .firstOrNull { it.parameters.isEmpty() && fromUtilPackage || executableAnalyzer.isAppropriate(it) } } else { constructorIds .sortedByDescending { it.parameters.size } - .firstOrNull { constructorAnalyzer.isAppropriate(it) } + .firstOrNull { executableAnalyzer.isAppropriate(it) } } } @@ -402,7 +457,7 @@ class AssembleModelGenerator(private val basePackageName: String) { get() = this.isPublic || !this.isPrivate && this.packageName == basePackageName private val Constructor<*>.isVisible : Boolean - get() = this.isPublic || !this.isPrivate && this.declaringClass.packageName == basePackageName + get() = this.isPublic || !this.isPrivate && this.declaringClass.`package`.name == basePackageName /** * Creates setter or direct setter call to set a field. @@ -441,11 +496,11 @@ class AssembleModelGenerator(private val basePackageName: String) { * Finds setters and direct accessors for fields of particular class. */ private fun findSettersAndDirectAccessors(classId: ClassId): Map { - val allModificatorsOfClass = modificatorsSearcher.findModificators(SettersAndDirectAccessors) + val allModificatorsOfClass = modificatorsSearcher.getFieldToModificators(SettersAndDirectAccessors) return allModificatorsOfClass .mapNotNull { (fieldId, possibleModificators) -> - chooseModificator(fieldId, possibleModificators)?.let { fieldId to it } + chooseModificator(classId, fieldId, possibleModificators)?.let { fieldId to it } } .toMap() } @@ -456,12 +511,13 @@ class AssembleModelGenerator(private val basePackageName: String) { * Note: direct accessor is more preferred than setter. */ private fun chooseModificator( + callerClassId: ClassId, fieldId: FieldId, settersAndDirectAccessors: Set, ): StatementId? { val directAccessors = settersAndDirectAccessors .filterIsInstance() - .filter {it.fieldId.isAccessibleFrom(basePackageName) } + .filter {it.fieldId.isAccessibleFrom(basePackageName, callerClassId) } if (directAccessors.any()) { return directAccessors.singleOrNull() diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt deleted file mode 100644 index b4f248ddef..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.utbot.framework.codegen - -import org.utbot.framework.plugin.api.CodegenLanguage - -private val javaKeywords = setOf( - "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", - "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", - "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", - "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", - "throw", "throws", "transient", "try", "void", "volatile", "while", "null", "false", "true" -) - -private val kotlinHardKeywords = setOf( - "as", "as?", "break", "class", "continue", "do", "else", "false", "for", "fun", "if", "in", "!in", "interface", - "is", "!is", "null", "object", "package", "return", "super", "this", "throw", "true", "try", "typealias", "typeof", - "val", "var", "when", "while" -) - -@Suppress("unused") -private val kotlinSoftKeywords = setOf( - "by", "catch", "constructor", "delegate", "dynamic", "field", "file", "finally", "get", "import", "init", - "param", "property", "receiver", "set", "setparam", "value", "where" -) - -@Suppress("unused") -private val kotlinModifierKeywords = setOf( - "actual", "abstract", "annotation", "companion", "const", "crossinline", "data", "enum", "expect", "external", - "final", "infix", "inline", "inner", "internal", "lateinit", "noinline", "open", "operator", "out", "override", - "private", "protected", "public", "reified", "sealed", "suspend", "tailrec", "vararg" -) - -// For now we check only hard keywords because others can be used as methods and variables identifiers -private val kotlinKeywords = kotlinHardKeywords - -private fun getLanguageKeywords(codegenLanguage: CodegenLanguage): Set = when(codegenLanguage) { - CodegenLanguage.JAVA -> javaKeywords - CodegenLanguage.KOTLIN -> kotlinKeywords -} - -fun isLanguageKeyword(word: String, codegenLanguage: CodegenLanguage): Boolean = - word in getLanguageKeywords(codegenLanguage) 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/Domain.kt similarity index 81% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt index b57d9e0cd9..dfe8e2be57 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt @@ -1,16 +1,17 @@ -package org.utbot.framework.codegen +package org.utbot.framework.codegen.domain -import org.utbot.framework.DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS -import org.utbot.framework.codegen.model.constructor.builtin.mockitoClassId -import org.utbot.framework.codegen.model.constructor.builtin.ongoingStubbingClassId -import org.utbot.framework.codegen.model.constructor.util.argumentsClassId -import org.utbot.framework.codegen.model.tree.CgClassId +import org.utbot.framework.DEFAULT_EXECUTION_TIMEOUT_IN_INSTRUMENTED_PROCESS_MS +import org.utbot.framework.codegen.domain.builtin.mockitoClassId +import org.utbot.framework.codegen.domain.builtin.ongoingStubbingClassId +import org.utbot.framework.codegen.tree.argumentsClassId import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.CgClassId import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodeGenerationSettingBox import org.utbot.framework.plugin.api.CodeGenerationSettingItem import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.TypeParameters +import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.isolateCommandLineArgumentsToArgumentFile import org.utbot.framework.plugin.api.util.booleanArrayClassId import org.utbot.framework.plugin.api.util.booleanClassId @@ -30,12 +31,11 @@ import org.utbot.framework.plugin.api.util.shortArrayClassId import org.utbot.framework.plugin.api.util.voidClassId import java.io.File import org.utbot.framework.plugin.api.util.longClassId -import org.utbot.framework.plugin.api.util.objectArrayClassId import org.utbot.framework.plugin.api.util.voidWrapperClassId data class TestClassFile(val packageName: String, val imports: List, val testClass: String) -sealed class Import(internal val order: Int) : Comparable { +abstract class Import(val order: Int) : Comparable { abstract val qualifiedName: String override fun compareTo(other: Import) = importComparator.compare(this, other) @@ -128,13 +128,11 @@ object NoStaticMocking : StaticsMocking( object MockitoStaticMocking : StaticsMocking(id = "Mockito static mocking", displayName = "Mockito static mocking") { val mockedStaticClassId = BuiltinClassId( - name = "org.mockito.MockedStatic", canonicalName = "org.mockito.MockedStatic", simpleName = "MockedStatic" ) val mockedConstructionClassId = BuiltinClassId( - name = "org.mockito.MockedConstruction", canonicalName = "org.mockito.MockedConstruction", simpleName = "MockedConstruction" ) @@ -171,7 +169,7 @@ object MockitoStaticMocking : StaticsMocking(id = "Mockito static mocking", disp ) } -sealed class TestFramework( +abstract class TestFramework( override val id: String, override val displayName: String, override val description: String = "Use $displayName as test framework", @@ -181,19 +179,20 @@ sealed class TestFramework( abstract val mainPackage: String abstract val assertionsClass: ClassId abstract val arraysAssertionsClass: ClassId - abstract val testAnnotation: String + abstract val kotlinFailureAssertionsClass: ClassId abstract val testAnnotationId: ClassId - abstract val testAnnotationFqn: String - abstract val parameterizedTestAnnotation: String + abstract val beforeMethodId: ClassId + abstract val afterMethodId: ClassId abstract val parameterizedTestAnnotationId: ClassId - abstract val parameterizedTestAnnotationFqn: String - abstract val methodSourceAnnotation: String abstract val methodSourceAnnotationId: ClassId - abstract val methodSourceAnnotationFqn: String abstract val nestedClassesShouldBeStatic: Boolean abstract val argListClassId: ClassId - val assertEquals by lazy { assertionId("assertEquals", objectClassId, objectClassId) } + open val testSuperClass: ClassId? = null + + open val assertSame by lazy { assertionId("assertSame", objectClassId, objectClassId) } + + open val assertEquals by lazy { assertionId("assertEquals", objectClassId, objectClassId) } val assertFloatEquals by lazy { assertionId("assertEquals", floatClassId, floatClassId, floatClassId) } @@ -227,10 +226,18 @@ sealed class TestFramework( val assertNotEquals by lazy { assertionId("assertNotEquals", objectClassId, objectClassId) } - protected fun assertionId(name: String, vararg params: ClassId): MethodId = + val fail by lazy { assertionId("fail", objectClassId) } + + val kotlinFail by lazy { kotlinFailAssertionId("fail", objectClassId) } + + protected open fun assertionId(name: String, vararg params: ClassId): MethodId = builtinStaticMethodId(assertionsClass, name, voidClassId, *params) + private fun arrayAssertionId(name: String, vararg params: ClassId): MethodId = - builtinStaticMethodId(arraysAssertionsClass, name, voidClassId, *params) + builtinStaticMethodId(arraysAssertionsClass, name, voidClassId, *params) + + private fun kotlinFailAssertionId(name: String, vararg params: ClassId): MethodId = + builtinStaticMethodId(kotlinFailureAssertionsClass, name, voidClassId, *params) abstract fun getRunTestsCommand( executionInvoke: String, @@ -251,34 +258,64 @@ sealed class TestFramework( } } +class UnknownTestFramework(id: String) : TestFramework(id = id, displayName = id) { + override val mainPackage: String = id + override val assertionsClass: ClassId = ClassId(id) + override val arraysAssertionsClass: ClassId = ClassId(id) + override val kotlinFailureAssertionsClass: ClassId = ClassId(id) + override val testAnnotationId: ClassId = ClassId(id) + override val beforeMethodId: ClassId = ClassId(id) + override val afterMethodId: ClassId = ClassId(id) + override val parameterizedTestAnnotationId: ClassId = ClassId(id) + override val methodSourceAnnotationId: ClassId = ClassId(id) + override val nestedClassesShouldBeStatic: Boolean = false + override val argListClassId: ClassId = ClassId(id) + + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List = emptyList() + +} + object TestNg : TestFramework(id = "TestNG",displayName = "TestNG") { override val mainPackage: String = TEST_NG_PACKAGE - override val testAnnotation: String = "@$mainPackage.Test" - override val testAnnotationFqn: String = "$mainPackage.Test" - override val parameterizedTestAnnotation: String = "@$mainPackage.Test" - override val parameterizedTestAnnotationFqn: String = "@$mainPackage.Test" - override val methodSourceAnnotation: String = "@$mainPackage.DataProvider" - override val methodSourceAnnotationFqn: String = "@$mainPackage.DataProvider" + override val testAnnotationId: ClassId = BuiltinClassId( + canonicalName = "$mainPackage.annotations.Test", + simpleName = "Test" + ) + + override val beforeMethodId = BuiltinClassId( + canonicalName = "$mainPackage.annotations.BeforeMethod", + simpleName = "BeforeMethod" + ) + + override val afterMethodId = BuiltinClassId( + canonicalName = "$mainPackage.annotations.AfterMethod", + simpleName = "AfterMethod" + ) internal const val testXmlName: String = "testng.xml" override val assertionsClass: ClassId = BuiltinClassId( - name = TEST_NG_ASSERTIONS, canonicalName = TEST_NG_ASSERTIONS, simpleName = "Assert" ) override val arraysAssertionsClass: ClassId = BuiltinClassId( - name = TEST_NG_ARRAYS_ASSERTIONS, canonicalName = TEST_NG_ARRAYS_ASSERTIONS, simpleName = "ArrayAsserts" ) + override val kotlinFailureAssertionsClass = assertionsClass + override val assertBooleanArrayEquals by lazy { assertionId("assertEquals", booleanArrayClassId, booleanArrayClassId) } val throwingRunnableClassId = BuiltinClassId( - name = "${assertionsClass.name}\$ThrowingRunnable", canonicalName = "${assertionsClass.canonicalName}.ThrowingRunnable", simpleName = "ThrowingRunnable" ) @@ -294,20 +331,12 @@ object TestNg : TestFramework(id = "TestNG",displayName = "TestNG") { ) ) - override val testAnnotationId: ClassId = BuiltinClassId( - name = "$mainPackage.annotations.Test", - canonicalName = "$mainPackage.annotations.Test", - simpleName = "Test" - ) - override val parameterizedTestAnnotationId: ClassId = BuiltinClassId( - name = "$mainPackage.annotations.Test", canonicalName = "$mainPackage.annotations.Test", - simpleName = "Test" + simpleName = "Test", ) override val methodSourceAnnotationId: ClassId = BuiltinClassId( - name = "$mainPackage.annotations.DataProvider", canonicalName = "$mainPackage.annotations.DataProvider", simpleName = "DataProvider" ) @@ -315,26 +344,7 @@ object TestNg : TestFramework(id = "TestNG",displayName = "TestNG") { override val nestedClassesShouldBeStatic = true override val argListClassId: ClassId - get() { - val outerArrayId = Array?>::class.id - val innerArrayId = BuiltinClassId( - name = objectArrayClassId.name, - simpleName = objectArrayClassId.simpleName, - canonicalName = objectArrayClassId.canonicalName, - packageName = objectArrayClassId.packageName, - elementClassId = objectClassId, - typeParameters = TypeParameters(listOf(objectClassId)) - ) - - return BuiltinClassId( - name = outerArrayId.name, - simpleName = outerArrayId.simpleName, - canonicalName = outerArrayId.canonicalName, - packageName = outerArrayId.packageName, - elementClassId = innerArrayId, - typeParameters = TypeParameters(listOf(innerArrayId)) - ) - } + get() = Array?>::class.id @OptIn(ExperimentalStdlibApi::class) override fun getRunTestsCommand( @@ -385,54 +395,44 @@ object Junit4 : TestFramework(id = "JUnit4",displayName = "JUnit 4") { get() = error("Parametrized tests are not supported for JUnit 4") override val mainPackage: String = JUNIT4_PACKAGE - override val testAnnotation = "@$mainPackage.Test" - override val testAnnotationFqn: String = "$mainPackage.Test" - - override val parameterizedTestAnnotation - get() = parametrizedTestsNotSupportedError - override val parameterizedTestAnnotationFqn - get() = parametrizedTestsNotSupportedError - override val methodSourceAnnotation - get() = parametrizedTestsNotSupportedError - override val methodSourceAnnotationFqn - get() = parametrizedTestsNotSupportedError override val testAnnotationId = BuiltinClassId( - name = "$JUNIT4_PACKAGE.Test", - canonicalName = "$JUNIT4_PACKAGE.Test", + canonicalName = "$mainPackage.Test", simpleName = "Test" ) + override val beforeMethodId = BuiltinClassId( + canonicalName = "$mainPackage.Before", + simpleName = "Before" + ) + + override val afterMethodId = BuiltinClassId( + canonicalName = "$mainPackage.After", + simpleName = "After" + ) + override val parameterizedTestAnnotationId = voidClassId override val methodSourceAnnotationId = voidClassId val runWithAnnotationClassId = BuiltinClassId( - name = "$JUNIT4_PACKAGE.runner.RunWith", canonicalName = "$JUNIT4_PACKAGE.runner.RunWith", simpleName = "RunWith" ) override val assertionsClass = BuiltinClassId( - name = JUNIT4_ASSERTIONS, canonicalName = JUNIT4_ASSERTIONS, simpleName = "Assert" ) override val arraysAssertionsClass = assertionsClass + override val kotlinFailureAssertionsClass = assertionsClass val ignoreAnnotationClassId = with("$JUNIT4_PACKAGE.Ignore") { BuiltinClassId( - name = this, canonicalName = this, simpleName = "Ignore" ) } - val enclosedClassId = BuiltinClassId( - name = "org.junit.experimental.runners.Enclosed", - canonicalName = "org.junit.experimental.runners.Enclosed", - simpleName = "Enclosed" - ) - override val nestedClassesShouldBeStatic = true override val argListClassId: ClassId @@ -455,33 +455,38 @@ object Junit4 : TestFramework(id = "JUnit4",displayName = "JUnit 4") { object Junit5 : TestFramework(id = "JUnit5", displayName = "JUnit 5") { override val mainPackage: String = JUNIT5_PACKAGE - override val testAnnotation = "@$mainPackage.Test" - override val testAnnotationFqn: String = "$mainPackage.Test" - override val parameterizedTestAnnotation = "$JUNIT5_PARAMETERIZED_PACKAGE.ParameterizedTest" - override val parameterizedTestAnnotationFqn: String = "$JUNIT5_PARAMETERIZED_PACKAGE.ParameterizedTest" - override val methodSourceAnnotation: String = "$JUNIT5_PARAMETERIZED_PACKAGE.provider.MethodSource" - override val methodSourceAnnotationFqn: String = "$JUNIT5_PARAMETERIZED_PACKAGE.provider.MethodSource" + + override val testAnnotationId = BuiltinClassId( + canonicalName = "$JUNIT5_PACKAGE.Test", + simpleName = "Test" + ) + + override val beforeMethodId = BuiltinClassId( + canonicalName = "${mainPackage}.BeforeEach", + simpleName = "BeforeEach" + ) + + override val afterMethodId = BuiltinClassId( + canonicalName = "${mainPackage}.AfterEach", + simpleName = "AfterEach" + ) val executableClassId = BuiltinClassId( - name = "$JUNIT5_PACKAGE.function.Executable", canonicalName = "$JUNIT5_PACKAGE.function.Executable", simpleName = "Executable" ) val timeoutClassId = BuiltinClassId( - name = "$JUNIT5_PACKAGE.Timeout", canonicalName = "$JUNIT5_PACKAGE.Timeout", simpleName = "Timeout" ) val timeunitClassId = BuiltinClassId( - name = "TimeUnit", canonicalName = "java.util.concurrent.TimeUnit", simpleName = "TimeUnit" ) val durationClassId = BuiltinClassId( - name = "Duration", canonicalName = "java.time.Duration", simpleName = "Duration" ) @@ -494,37 +499,32 @@ object Junit5 : TestFramework(id = "JUnit5", displayName = "JUnit 5") { ) val nestedTestClassAnnotationId = BuiltinClassId( - name = "$JUNIT5_PACKAGE.Nested", canonicalName = "$JUNIT5_PACKAGE.Nested", simpleName = "Nested" ) - override val testAnnotationId = BuiltinClassId( - name = "$JUNIT5_PACKAGE.Test", - canonicalName = "$JUNIT5_PACKAGE.Test", - simpleName = "Test" - ) - override val parameterizedTestAnnotationId = BuiltinClassId( - name = "$JUNIT5_PARAMETERIZED_PACKAGE.ParameterizedTest", canonicalName = "$JUNIT5_PARAMETERIZED_PACKAGE.ParameterizedTest", simpleName = "ParameterizedTest" ) override val methodSourceAnnotationId: ClassId = BuiltinClassId( - name = "$JUNIT5_PARAMETERIZED_PACKAGE.provider.MethodSource", canonicalName = "$JUNIT5_PARAMETERIZED_PACKAGE.provider.MethodSource", simpleName = "MethodSource" ) override val assertionsClass = BuiltinClassId( - name = JUNIT5_ASSERTIONS, canonicalName = JUNIT5_ASSERTIONS, simpleName = "Assertions" ) override val arraysAssertionsClass = assertionsClass + override val kotlinFailureAssertionsClass = BuiltinClassId( + canonicalName = "org.junit.jupiter.api", + simpleName = "Assertions" + ) + val assertThrows = builtinStaticMethodId( classId = assertionsClass, name = "assertThrows", @@ -547,14 +547,12 @@ object Junit5 : TestFramework(id = "JUnit5", displayName = "JUnit 5") { ) val displayNameClassId = BuiltinClassId( - name = "$JUNIT5_PACKAGE.DisplayName", canonicalName = "$JUNIT5_PACKAGE.DisplayName", simpleName = "DisplayName" ) val disabledAnnotationClassId = with("$JUNIT5_PACKAGE.Disabled") { BuiltinClassId( - name = this, canonicalName = this, simpleName = "Disabled" ) @@ -566,7 +564,6 @@ object Junit5 : TestFramework(id = "JUnit5", displayName = "JUnit 5") { get() { val arrayListId = java.util.ArrayList::class.id return BuiltinClassId( - name = arrayListId.name, simpleName = arrayListId.simpleName, canonicalName = arrayListId.canonicalName, packageName = arrayListId.packageName, @@ -624,7 +621,7 @@ data class HangingTestsTimeout(val timeoutMs: Long) { constructor() : this(DEFAULT_TIMEOUT_MS) companion object { - const val DEFAULT_TIMEOUT_MS = DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS + const val DEFAULT_TIMEOUT_MS = DEFAULT_EXECUTION_TIMEOUT_IN_INSTRUMENTED_PROCESS_MS const val MIN_TIMEOUT_MS = 100L const val MAX_TIMEOUT_MS = 1_000_000L } @@ -693,3 +690,37 @@ enum class ParametrizedTestSource( override val allItems: List = values().toList() } } + +enum class ProjectType { + /** + * Standard JVM project without DI frameworks + */ + PureJvm, + + /** + * Spring or Spring Boot project + */ + Spring, + + /** + * Python project + */ + Python, + + /** + * JavaScript project + */ + JavaScript, +} + +/** + * Extended [UtModel] model with testSet id and execution id. + * + * Used as a key in [valueByUtModelWrapper]. + * Was introduced primarily for shared among all test methods global variables. + */ +data class UtModelWrapper( + val testSetId: Int, + val executionId: Int, + val model: UtModel, +) 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/domain/builtin/MockitoBuiltins.kt similarity index 82% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/MockitoBuiltins.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/MockitoBuiltins.kt index 1a2805b615..d78bd10b3d 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/domain/builtin/MockitoBuiltins.kt @@ -1,7 +1,6 @@ -package org.utbot.framework.codegen.model.constructor.builtin +package org.utbot.framework.codegen.domain.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 @@ -16,42 +15,44 @@ 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", simpleName = "Mockito", ) internal val ongoingStubbingClassId = BuiltinClassId( - name = "org.mockito.stubbing.OngoingStubbing", canonicalName = "org.mockito.stubbing.OngoingStubbing", simpleName = "OngoingStubbing", ) +internal val stubberClassId = BuiltinClassId( + canonicalName = "org.mockito.stubbing.Stubber", + simpleName = "Stubber" +) + +// We wrap an Object classId in BuiltinClassId +// in order to deal with [CgExpression.canBeReceiverOf] +// when we want to call ANY method on an object. +internal val genericObjectClassId = BuiltinClassId( + canonicalName = "java.lang.Object", + simpleName = "Object" +) + internal val answerClassId = BuiltinClassId( - name = "org.mockito.stubbing.Answer", canonicalName = "org.mockito.stubbing.Answer", simpleName = "Answer", ) internal val argumentMatchersClassId = BuiltinClassId( - name = "org.mockito.ArgumentMatchers", canonicalName = "org.mockito.ArgumentMatchers", simpleName = "ArgumentMatchers", ) internal val mockedConstructionContextClassId = BuiltinClassId( - name = "org.mockito.MockedConstruction.Context", - canonicalName = "org.mockito.MockedConstruction.Context", // TODO use $ as a delimiter of outer and nested classes? + canonicalName = "org.mockito.MockedConstruction.Context", simpleName = "Context", - isNested = true + name = "org.mockito.MockedConstruction\$Context", + isNested = true, ) internal val mockMethodId = builtinStaticMethodId( @@ -85,6 +86,19 @@ internal val thenReturnMethodId = builtinMethodId( arguments = arrayOf(objectClassId) ) +internal val doNothingMethodId = builtinStaticMethodId( + classId = mockitoClassId, + name = "doNothing", + returnType = stubberClassId, +) + +internal val whenStubberMethodId = builtinMethodId( + classId = stubberClassId, + name = "when", + returnType = genericObjectClassId, + arguments = arrayOf(objectClassId) +) + // TODO: for this method and other static methods implement some utils that allow calling // TODO: these methods without explicit declaring class id specification, because right now such calls are too verbose internal val any = builtinStaticMethodId( 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/domain/builtin/ReflectionBuiltins.kt similarity index 90% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/ReflectionBuiltins.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/ReflectionBuiltins.kt index 0120e8d0fc..522049321e 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/domain/builtin/ReflectionBuiltins.kt @@ -1,4 +1,4 @@ -package org.utbot.framework.codegen.model.constructor.builtin +package org.utbot.framework.codegen.domain.builtin import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.MethodId @@ -19,15 +19,6 @@ import java.lang.reflect.InvocationTargetException //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, getMethodId, forName, - getDeclaredMethod, getDeclaredConstructor, allocateInstance, - getClass, getDeclaredField, isEnumConstant, getFieldName, - equals, getSuperclass, setMethodId, 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/domain/builtin/UtilMethodBuiltins.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/UtilMethodBuiltins.kt new file mode 100644 index 0000000000..e87b580b18 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/UtilMethodBuiltins.kt @@ -0,0 +1,128 @@ +package org.utbot.framework.codegen.domain.builtin + +import org.mockito.MockitoAnnotations +import org.utbot.framework.codegen.domain.MockitoStaticMocking +import org.utbot.framework.codegen.renderer.utilMethodTextById +import org.utbot.framework.codegen.tree.ututils.UtilClassKind.Companion.PACKAGE_DELIMITER +import org.utbot.framework.codegen.tree.ututils.UtilClassKind.Companion.UT_UTILS_BASE_PACKAGE_NAME +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.framework.utils.UtilMethodProvider + + +/** + * This provider represents an util class file that is generated and put into the user's test module. + * The generated class is UtUtils (its id is defined at [utJavaUtilsClassId] or [utKotlinUtilsClassId]). + * + * Content of this util class may be different (due to mocks in deepEquals), but the methods (and their ids) are the same. + */ +internal class UtilClassFileMethodProvider(language: CodegenLanguage) + : UtilMethodProvider(selectUtilClassId(language)) { + /** + * This property contains the current version of util class. + * This version will be written to the util class file inside a comment. + * + * Whenever we want to create an util class, we first check if there is an already existing one. + * If there is, then we decide whether we need to overwrite it or not. One of the factors here + * is the version of this existing class. If the version of existing class is older than the one + * that is currently stored in [UtilClassFileMethodProvider.UTIL_CLASS_VERSION], then we need to + * overwrite an util class, because it might have been changed in the new version. + * + * **IMPORTANT** if you make any changes to util methods (see [utilMethodTextById]), do not forget to update this version. + */ + val UTIL_CLASS_VERSION = "2.1" +} + +class TestClassUtilMethodProvider(testClassId: ClassId) : UtilMethodProvider(testClassId) + +internal fun selectUtilClassId(codegenLanguage: CodegenLanguage): ClassId = + when (codegenLanguage) { + CodegenLanguage.JAVA -> utJavaUtilsClassId + CodegenLanguage.KOTLIN -> utKotlinUtilsClassId + } + +internal val utJavaUtilsClassId: ClassId + get() = BuiltinClassId( + canonicalName = UT_UTILS_BASE_PACKAGE_NAME + PACKAGE_DELIMITER + "java" + PACKAGE_DELIMITER + "UtUtils", + simpleName = "UtUtils", + isFinal = true, + ) + +internal val utKotlinUtilsClassId: ClassId + get() = BuiltinClassId( + canonicalName = UT_UTILS_BASE_PACKAGE_NAME + PACKAGE_DELIMITER + "kotlin" + PACKAGE_DELIMITER + "UtUtils", + simpleName = "UtUtils", + isFinal = true, + isKotlinObject = true + ) + +val openMocksMethodId = BuiltinMethodId( + classId = MockitoAnnotations::class.id, + name = "openMocks", + returnType = AutoCloseable::class.java.id, + parameters = listOf(objectClassId), + isStatic = true, +) + +/** + * [MethodId] for [AutoCloseable.close]. + */ +val closeMethodId = MethodId( + classId = AutoCloseable::class.java.id, + name = "close", + returnType = voidClassId, + parameters = emptyList(), +) + +private val clearCollectionMethodId = MethodId( + classId = Collection::class.java.id, + name = "clear", + returnType = voidClassId, + parameters = emptyList() +) + +private val clearMapMethodId = MethodId( + classId = Map::class.java.id, + name = "clear", + returnType = voidClassId, + parameters = emptyList() +) + +fun clearMethodId(javaClass: Class<*>): MethodId = when { + Collection::class.java.isAssignableFrom(javaClass) -> clearCollectionMethodId + Map::class.java.isAssignableFrom(javaClass) -> clearMapMethodId + else -> error("Clear method is not implemented for $javaClass") +} + +val mocksAutoCloseable: Set = setOf( + MockitoStaticMocking.mockedStaticClassId, + MockitoStaticMocking.mockedConstructionClassId +) + +val predefinedAutoCloseable: Set = mocksAutoCloseable + +/** + * Checks if this class is marked as auto closeable + * (useful for classes that could not be loaded by class loader like mocks for mocking statics from Mockito Inline). + */ +internal val ClassId.isPredefinedAutoCloseable: Boolean + get() = this in predefinedAutoCloseable + +/** + * Returns [AutoCloseable.close] method id for all auto closeable. + * and predefined as auto closeable via [isPredefinedAutoCloseable], and null otherwise. + * Null always for [BuiltinClassId]. + */ +internal val ClassId.closeMethodIdOrNull: MethodId? + get() = when { + isPredefinedAutoCloseable -> closeMethodId + this is BuiltinClassId -> null + else -> (jClass as? AutoCloseable)?.let { closeMethodId } + } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/context/CgContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/context/CgContext.kt new file mode 100644 index 0000000000..7051b84ad4 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/context/CgContext.kt @@ -0,0 +1,699 @@ +package org.utbot.framework.codegen.domain.context + +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.framework.codegen.domain.Import +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.PersistentSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.persistentSetOf +import org.utbot.common.DynamicProperty +import org.utbot.common.MutableDynamicProperties +import org.utbot.common.mutableDynamicPropertiesOf +import org.utbot.framework.codegen.domain.UtModelWrapper +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.builtin.TestClassUtilMethodProvider +import org.utbot.framework.codegen.domain.builtin.UtilClassFileMethodProvider +import org.utbot.framework.codegen.domain.models.* +import org.utbot.framework.codegen.services.access.Block +import org.utbot.framework.codegen.tree.EnvironmentFieldStateCache +import org.utbot.framework.codegen.tree.importIfNeeded +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.codegen.services.language.CgLanguageAssistant +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.isCheckedException +import org.utbot.framework.plugin.api.util.isSubtypeOf +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.utils.UtilMethodProvider + +typealias CgContextProperties = MutableDynamicProperties +typealias CgContextProperty = DynamicProperty + +/** + * Interface for all code generation context aware entities + * + * Most of the properties are 'val'. + * Although, some of the properties are declared as 'var' so that + * they can be reassigned as well as modified + * + * For example, [outerMostTestClass] and [currentExecution] can be reassigned + * when we start generating another method or test class + * + * [existingVariableNames] is a 'var' property + * that can be reverted to its previous value on exit from a name scope + * + * @see [CgContextOwner.withNameScope] + */ +interface CgContextOwner { + // current class under test + val classUnderTest: ClassId + + // If project under test is configured with Spring, we should do some extra analysis + val projectType: ProjectType + + // test class currently being generated (if series of nested classes is generated, it is the outermost one) + val outerMostTestClass: ClassId + + // test class currently being generated (if series of nested classes is generated, it is the innermost one) + var currentTestClass: ClassId + + // provider of util methods used for the test class currently being generated + val utilMethodProvider: UtilMethodProvider + + // current executable under test + // NOTE: may differ from `executableToCall` + var currentExecutableUnderTest: ExecutableId? + + // executable that is called in the current test method body and whose result is used in asserts as `actual` + // NOTE: may differ from `executableUnderTest` + val currentExecutableToCall: ExecutableId? get() = + currentExecution?.stateBefore?.executableToCall ?: currentExecutableUnderTest + + // ClassInfo for the outermost class currently being generated + val outerMostTestClassContext: TestClassContext + + // If generating series of nested classes, it is ClassInfo for the innermost one, + // otherwise it should be equal to outerMostTestClassInfo + val currentTestClassContext: TestClassContext + + // exceptions that can be thrown inside of current method being built + val collectedExceptions: MutableSet + + // annotations required by the current method being built + val collectedMethodAnnotations: MutableSet + + // imports required by the test class being built + val collectedImports: MutableSet + + val importedStaticMethods: MutableSet + val importedClasses: MutableSet + + // util methods required by the test class being built + val requiredUtilMethods: MutableSet + + val utilMethodsUsed: Boolean + + // test methods being generated + val testMethods: MutableList + + // names of methods that already exist in the test class + val existingMethodNames: MutableSet + + // At the start of a test method we save the initial state of some static fields in variables. + // This map is used to restore the initial values of these variables at the end of the test method. + val prevStaticFieldValues: MutableMap + + // names of parameters of methods under test + val paramNames: Map> + + // UtExecution we currently generate a test method for. + // It is null when no test method is being generated at the moment. + var currentExecution: UtExecution? + + val testFramework: TestFramework + + val mockFramework: MockFramework + + val staticsMocking: StaticsMocking + + val forceStaticMocking: ForceStaticMocking + + val generateWarningsForStaticMocking: Boolean + + val codegenLanguage: CodegenLanguage + + val cgLanguageAssistant: CgLanguageAssistant + + val parametrizedTestSource: ParametrizedTestSource + + /** + * Flag indicating whether a mock framework is used in the generated code + * NOTE! This flag is not about whether a mock framework is present + * in the user's project dependencies or not. + * This flag is true if the generated test class contains at least one mock object, + * and false otherwise. See method [withMockFramework]. + */ + var mockFrameworkUsed: Boolean + + // object that represents a set of information about JUnit of selected version + + // Persistent collections are used to conveniently restore their previous state. + // For example, when we exit a block of code we return to the previous name scope. + // At that moment we revert some collections (e.g. variable names) to the previous state. + + // current block of code being built + var currentBlock: PersistentList + + // variable names being used in the current name scope + var existingVariableNames: PersistentSet + + // variables of java.lang.Class type declared in the current name scope + var declaredClassRefs: PersistentMap + + // Variables of either java.lang.reflect.Constructor or java.lang.reflect.Method types + // declared in the current name scope. + // java.lang.reflect.Executable is a superclass of both of these types. + var declaredExecutableRefs: PersistentMap + + // Variables of java.lang.reflect.Field type declared in the current name scope + var declaredFieldRefs: PersistentMap + + // generated this instance for method under test + var thisInstance: CgValue? + + // generated arguments for method under test + val methodArguments: MutableList + + // a variable representing an actual result of the method under test call + var actual: CgVariable + + // a variable representing if test method contains reflective call or not + // and should we catch exceptions like InvocationTargetException or not so on + var containsReflectiveCall: Boolean + + // map from a set of tests for a method to another map + // which connects code generation error message + // with the number of times it occurred + val codeGenerationErrors: MutableMap> + + // package for generated test class + val testClassPackageName: String + + val shouldOptimizeImports: Boolean + + /** + * Used for differentiating models in codegen + * because comparisons by [UtModel] are not enough, + * especially for Spring related variable processing. + * + * Default value is -1, meaning that we are not processing any test set. + * + * @see [CgContext.withTestSetIdScope] + */ + var currentTestSetId: Int + + /** + * Used for differentiating models in codegen + * because comparisons by [UtModel] are not enough, + * especially for Spring related variable processing. + * + * Default value is -1, meaning that we are not processing any execution. + * + * @see [CgContext.withExecutionIdScope] + */ + var currentExecutionId: Int + + // used for comparing stateBefore and result variables -- in case of equality do not create new variable + var valueByUtModelWrapper: MutableMap + + // parameters of the method currently being generated + val currentMethodParameters: MutableMap + + val testClassCustomName: String? + + /** + * Determines whether tests that throw Runtime exceptions should fail or pass. + */ + val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour + + /** + * Timeout for possibly hanging tests (uses info from concrete executor). + */ + val hangingTestsTimeout: HangingTestsTimeout + + /** + * Determines whether tests with timeout should fail (with added Timeout annotation) or be disabled (for our pipeline). + */ + val enableTestsTimeout: Boolean + + var statesCache: EnvironmentFieldStateCache + + /** + * Result models required to create generic execution in parametrized tests. + */ + var successfulExecutionsModels: List + + /** + * Many of [CgContext] properties are only needed in some specific scenarios + * (e.g. Spring-related properties and parametrized test specific properties). + * + * To avoid overly inflating [CgContext] interface such properties should + * be added as dynamic properties. + * + * @see DynamicProperty + */ + // TODO make parameterized test specific properties dynamic + val properties: CgContextProperties + + fun block(init: () -> Unit): Block { + val prevBlock = currentBlock + return try { + val block = persistentListOf() + currentBlock = block + withNameScope { + init() + currentBlock + } + } finally { + currentBlock = prevBlock + } + } + + operator fun CgStatement.unaryPlus() { + currentBlock = currentBlock.add(this) + } + + operator fun CgExecutableCall.unaryPlus(): CgStatementExecutableCall = + CgStatementExecutableCall(this).also { + currentBlock = currentBlock.add(it) + } + + fun updateExecutableUnderTest(executableId: ExecutableId) { + currentExecutableUnderTest = executableId + } + + fun withTestSetIdScope(testSetId: Int, block: () -> R): R { + currentTestSetId = testSetId + return block() + } + + fun withExecutionIdScope(executionId: Int, block: () -> R): R { + currentExecutionId = executionId + return block() + } + + fun addExceptionIfNeeded(exception: ClassId) + fun runWithoutCollectingExceptions(block: () -> T): T + + fun createGetClassExpression(id: ClassId, codegenLanguage: CodegenLanguage): CgGetClass = + when (codegenLanguage) { + CodegenLanguage.JAVA -> CgGetJavaClass(id) + CodegenLanguage.KOTLIN -> CgGetKotlinClass(id) + }.also { + importIfNeeded(id) + } + + /** + * This method sets up context for a new test class file generation and executes the given [block]. + * Afterwards, context is set back to the initial state. + */ + fun withTestClassFileScope(block: () -> R): R + + /** + * This method sets up context for a new test class generation and executes the given [block]. + * Afterwards, context is set back to the initial state. + */ + fun withTestClassScope(block: () -> R): R + + /** + * This method does almost all the same as [withTestClassScope], but for nested test classes. + * The difference is that instead of working with [outerMostTestClassContext] it works with [currentTestClassContext]. + */ + fun withNestedClassScope(testClassModel: SimpleTestClassModel, block: () -> R): R + + /** + * Set [mockFrameworkUsed] flag to true if the block is successfully executed + */ + fun withMockFramework(block: () -> R): R { + val result = block() + mockFrameworkUsed = true + return result + } + + fun rememberVariableForModel(variable: CgVariable, model: UtModel? = null) { + model?.let { + valueByUtModelWrapper[it.wrap()] = variable + } + } + + fun withNameScope(block: () -> R): R { + val prevVariableNames = existingVariableNames + val prevDeclaredClassRefs = declaredClassRefs + val prevDeclaredExecutableRefs = declaredExecutableRefs + val prevDeclaredFieldRefs = declaredFieldRefs + val prevValueByCgModel = valueByUtModelWrapper.toMutableMap() + return try { + block() + } finally { + existingVariableNames = prevVariableNames + declaredClassRefs = prevDeclaredClassRefs + declaredExecutableRefs = prevDeclaredExecutableRefs + declaredFieldRefs = prevDeclaredFieldRefs + valueByUtModelWrapper = prevValueByCgModel + } + } + + /** + * [ClassId] of a class that contains util methods. + * For example, it can be the current test class, or it can be a generated separate `UtUtils` class. + */ + val utilsClassId: ClassId + get() = utilMethodProvider.utilClassId + + /** + * Checks is it our util reflection field getter method. + * When this method is used with type cast in Kotlin, this type cast have to be safety + */ + val MethodId.isGetFieldUtilMethod: Boolean + get() = this == utilMethodProvider.getFieldValueMethodId + || this == utilMethodProvider.getStaticFieldValueMethodId + + val testClassThisInstance: CgThisInstance + + // util methods and auxiliary classes of current test class + + val capturedArgumentClass: ClassId + get() = utilMethodProvider.capturedArgumentClassId + + val getUnsafeInstance: MethodId + get() = utilMethodProvider.getUnsafeInstanceMethodId + + val createInstance: MethodId + get() = utilMethodProvider.createInstanceMethodId + + val createArray: MethodId + get() = utilMethodProvider.createArrayMethodId + + val setField: MethodId + get() = utilMethodProvider.setFieldMethodId + + val setStaticField: MethodId + get() = utilMethodProvider.setStaticFieldMethodId + + val getFieldValue: MethodId + get() = utilMethodProvider.getFieldValueMethodId + + val getStaticFieldValue: MethodId + get() = utilMethodProvider.getStaticFieldValueMethodId + + val getEnumConstantByName: MethodId + get() = utilMethodProvider.getEnumConstantByNameMethodId + + val deepEquals: MethodId + get() = utilMethodProvider.deepEqualsMethodId + + val arraysDeepEquals: MethodId + get() = utilMethodProvider.arraysDeepEqualsMethodId + + val iterablesDeepEquals: MethodId + get() = utilMethodProvider.iterablesDeepEqualsMethodId + + val streamsDeepEquals: MethodId + get() = utilMethodProvider.streamsDeepEqualsMethodId + + val mapsDeepEquals: MethodId + get() = utilMethodProvider.mapsDeepEqualsMethodId + + val hasCustomEquals: MethodId + get() = utilMethodProvider.hasCustomEqualsMethodId + + val getArrayLength: MethodId + get() = utilMethodProvider.getArrayLengthMethodId + + val consumeBaseStream: MethodId + get() = utilMethodProvider.consumeBaseStreamMethodId + + val buildStaticLambda: MethodId + get() = utilMethodProvider.buildStaticLambdaMethodId + + val buildLambda: MethodId + get() = utilMethodProvider.buildLambdaMethodId + + val getLookupIn: MethodId + get() = utilMethodProvider.getLookupInMethodId + + val getSingleAbstractMethod: MethodId + get() = utilMethodProvider.getSingleAbstractMethodMethodId + + val getLambdaCapturedArgumentTypes: MethodId + get() = utilMethodProvider.getLambdaCapturedArgumentTypesMethodId + + val getLambdaCapturedArgumentValues: MethodId + get() = utilMethodProvider.getLambdaCapturedArgumentValuesMethodId + + val getInstantiatedMethodType: MethodId + get() = utilMethodProvider.getInstantiatedMethodTypeMethodId + + val getLambdaMethod: MethodId + get() = utilMethodProvider.getLambdaMethodMethodId + + fun UtModel.wrap(): UtModelWrapper = + UtModelWrapper( + testSetId = currentTestSetId, + executionId = currentExecutionId, + model = this, + ) +} + +/** + * Context with current code generation info + */ +class CgContext( + override val classUnderTest: ClassId, + override val projectType: ProjectType, + val generateUtilClassFile: Boolean = false, + override var currentExecutableUnderTest: ExecutableId? = null, + override val collectedExceptions: MutableSet = mutableSetOf(), + override val collectedMethodAnnotations: MutableSet = mutableSetOf(), + override val collectedImports: MutableSet = mutableSetOf(), + override val importedStaticMethods: MutableSet = mutableSetOf(), + override val importedClasses: MutableSet = mutableSetOf(), + override val requiredUtilMethods: MutableSet = mutableSetOf(), + override val testMethods: MutableList = mutableListOf(), + override val existingMethodNames: MutableSet = mutableSetOf(), + override val prevStaticFieldValues: MutableMap = mutableMapOf(), + override val paramNames: Map>, + override var currentExecution: UtExecution? = null, + override val testFramework: TestFramework, + override val mockFramework: MockFramework, + override val staticsMocking: StaticsMocking, + override val forceStaticMocking: ForceStaticMocking, + override val generateWarningsForStaticMocking: Boolean, + override val codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem, + override val cgLanguageAssistant: CgLanguageAssistant, + override val parametrizedTestSource: ParametrizedTestSource = ParametrizedTestSource.DO_NOT_PARAMETRIZE, + override var mockFrameworkUsed: Boolean = false, + override var currentBlock: PersistentList = persistentListOf(), + override var existingVariableNames: PersistentSet = persistentSetOf(), + override var declaredClassRefs: PersistentMap = persistentMapOf(), + override var declaredExecutableRefs: PersistentMap = persistentMapOf(), + override var declaredFieldRefs: PersistentMap = persistentMapOf(), + override var thisInstance: CgValue? = null, + override val methodArguments: MutableList = mutableListOf(), + override val codeGenerationErrors: MutableMap> = mutableMapOf(), + override val testClassPackageName: String = classUnderTest.packageName, + override var shouldOptimizeImports: Boolean = false, + override var testClassCustomName: String? = null, + override val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = + RuntimeExceptionTestsBehaviour.defaultItem, + override val hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), + override val enableTestsTimeout: Boolean = true, + override var containsReflectiveCall: Boolean = false, + override val properties: CgContextProperties = mutableDynamicPropertiesOf(), +) : CgContextOwner { + override lateinit var statesCache: EnvironmentFieldStateCache + override lateinit var actual: CgVariable + override lateinit var successfulExecutionsModels: List + + /** + * This property cannot be accessed outside of test class file scope + * (i.e. outside of [CgContextOwner.withTestClassFileScope]). + */ + override val outerMostTestClassContext: TestClassContext + get() = _outerMostTestClassContext ?: error("Accessing outerMostTestClassInfo out of class file scope") + + private var _outerMostTestClassContext: TestClassContext? = cgLanguageAssistant.outerMostTestClassContent + + /** + * This property cannot be accessed outside of test class scope + * (i.e. outside of [CgContextOwner.withTestClassScope]). + */ + override val currentTestClassContext: TestClassContext + get() = _currentTestClassContext ?: error("Accessing currentTestClassInfo out of class scope") + + private var _currentTestClassContext: TestClassContext? = null + + override val outerMostTestClass: ClassId by lazy { + val (name, simpleName) = cgLanguageAssistant.testClassName( + testClassCustomName, testClassPackageName, classUnderTest + ) + BuiltinClassId( + canonicalName = name, + simpleName = simpleName, + isFinal = true, + ) + } + + /** + * Determine where the util methods will come from. + * If we don't want to use a separately generated util class, + * util methods will be generated directly in the test class (see [TestClassUtilMethodProvider]). + * Otherwise, an util class will be generated separately and we will use util methods from it (see [UtilClassFileMethodProvider]). + */ + override val utilMethodProvider: UtilMethodProvider + get() = if (generateUtilClassFile) { + UtilClassFileMethodProvider(codegenLanguage) + } else { + TestClassUtilMethodProvider(outerMostTestClass) + } + + override lateinit var currentTestClass: ClassId + + override fun withTestClassFileScope(block: () -> R): R { + clearClassScope() + _outerMostTestClassContext = TestClassContext() + return try { + block() + } finally { + clearClassScope() + } + } + + override fun withTestClassScope(block: () -> R): R { + _currentTestClassContext = outerMostTestClassContext + currentTestClass = outerMostTestClass + return try { + block() + } finally { + _currentTestClassContext = null + } + } + + override fun withNestedClassScope(testClassModel: SimpleTestClassModel, block: () -> R): R { + val previousCurrentTestClassInfo = currentTestClassContext + val previousCurrentTestClass = currentTestClass + currentTestClass = createClassIdForNestedClass(testClassModel) + _currentTestClassContext = TestClassContext() + return try { + block() + } finally { + _currentTestClassContext = previousCurrentTestClassInfo + currentTestClass = previousCurrentTestClass + } + } + + private fun createClassIdForNestedClass(testClassModel: SimpleTestClassModel): ClassId { + val simpleName = "${testClassModel.classUnderTest.simpleName}Test" + return BuiltinClassId( + canonicalName = currentTestClass.canonicalName + "." + simpleName, + simpleName = simpleName, + isFinal = true, + ) + } + + private fun clearClassScope() { + _outerMostTestClassContext = null + collectedImports.clear() + importedStaticMethods.clear() + importedClasses.clear() + testMethods.clear() + requiredUtilMethods.clear() + valueByUtModelWrapper.clear() + mockFrameworkUsed = false + } + + // number of times collection of exceptions was suspended + private var exceptionCollectionSuspensionDepth = 0 + + override fun runWithoutCollectingExceptions(block: () -> T): T { + exceptionCollectionSuspensionDepth++ + return try { + block() + } finally { + exceptionCollectionSuspensionDepth-- + } + } + + override fun addExceptionIfNeeded(exception: ClassId) { + if (exceptionCollectionSuspensionDepth > 0) return + if (exception !is BuiltinClassId) { + require(exception isSubtypeOf Throwable::class.id) { + "Class $exception which is not a Throwable was passed" + } + + val isUnchecked = !exception.jClass.isCheckedException + val alreadyAdded = + collectedExceptions.any { existingException -> exception isSubtypeOf existingException } + + if (isUnchecked || alreadyAdded) return + + collectedExceptions + .removeIf { existingException -> existingException isSubtypeOf exception } + } + + if (collectedExceptions.add(exception)) { + importIfNeeded(exception) + } + } + + override var currentTestSetId: Int = -1 + + override var currentExecutionId: Int = -1 + + override var valueByUtModelWrapper: MutableMap = mutableMapOf() + + override val currentMethodParameters: MutableMap = mutableMapOf() + + override val testClassThisInstance: CgThisInstance = CgThisInstance(outerMostTestClass) + + override val utilMethodsUsed: Boolean + get() = requiredUtilMethods.isNotEmpty() + + fun customCopy(shouldOptimizeImports: Boolean, testClassCustomName: String?) = CgContext( + shouldOptimizeImports = shouldOptimizeImports, + testClassCustomName = testClassCustomName, + + classUnderTest = this.classUnderTest, + projectType = this.projectType, + generateUtilClassFile = this.generateUtilClassFile, + currentExecutableUnderTest = this.currentExecutableUnderTest, + collectedExceptions =this.collectedExceptions, + collectedMethodAnnotations = this.collectedMethodAnnotations, + collectedImports = this.collectedImports, + importedStaticMethods = this.importedStaticMethods, + importedClasses = this.importedClasses, + requiredUtilMethods = this.requiredUtilMethods, + testMethods = this.testMethods, + existingMethodNames = this.existingMethodNames, + prevStaticFieldValues = this.prevStaticFieldValues, + paramNames = this.paramNames, + currentExecution = this.currentExecution, + testFramework = this.testFramework, + mockFramework = this.mockFramework, + staticsMocking = this.staticsMocking, + forceStaticMocking = this.forceStaticMocking, + generateWarningsForStaticMocking = this.generateWarningsForStaticMocking, + codegenLanguage = this.codegenLanguage, + cgLanguageAssistant = this.cgLanguageAssistant, + parametrizedTestSource = this.parametrizedTestSource, + mockFrameworkUsed = this.mockFrameworkUsed, + currentBlock = this.currentBlock, + existingVariableNames = this.existingVariableNames, + declaredClassRefs = this.declaredClassRefs, + declaredExecutableRefs = this.declaredExecutableRefs, + declaredFieldRefs = this.declaredFieldRefs, + thisInstance = this.thisInstance, + methodArguments = this.methodArguments, + codeGenerationErrors = this.codeGenerationErrors, + testClassPackageName = this.testClassPackageName, + runtimeExceptionTestsBehaviour = this.runtimeExceptionTestsBehaviour, + hangingTestsTimeout = this.hangingTestsTimeout, + enableTestsTimeout = this.enableTestsTimeout, + containsReflectiveCall = this.containsReflectiveCall, + properties = this.properties, + ) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/context/TestClassContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/context/TestClassContext.kt new file mode 100644 index 0000000000..00f223d102 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/context/TestClassContext.kt @@ -0,0 +1,39 @@ +package org.utbot.framework.codegen.domain.context + +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgAnnotation +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.codegen.domain.models.CgClass + +/** + * This class stores context information needed to build [CgClass]. + * Should only be used in [CgContextOwner]. + */ +data class TestClassContext( + // set of interfaces that the test class must inherit + val collectedTestClassInterfaces: MutableSet = mutableSetOf(), + + // set of annotations of the test class + val collectedTestClassAnnotations: MutableSet = mutableSetOf(), + + // list of data provider methods that test class must implement + val cgDataProviderMethods: MutableList = mutableListOf(), +) { + // test class superclass (if needed) + var testClassSuperclass: ClassId? = null + set(value) { + // Assigning a value to the testClassSuperclass when it is already non-null + // means that we need the test class to have more than one superclass + // which is impossible in Java and Kotlin. + require(value == null || field == null) { "It is impossible for the test class to have more than one superclass" } + field = value + } + + fun clear() { + collectedTestClassAnnotations.clear() + collectedTestClassInterfaces.clear() + cgDataProviderMethods.clear() + testClassSuperclass = null + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgElement.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgElement.kt new file mode 100644 index 0000000000..4d3ee7c675 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgElement.kt @@ -0,0 +1,1038 @@ +package org.utbot.framework.codegen.domain.models + +import org.utbot.common.WorkaroundReason +import org.utbot.common.workaround +import org.utbot.framework.codegen.domain.Import +import org.utbot.framework.codegen.renderer.CgRendererContext +import org.utbot.framework.codegen.renderer.CgVisitor +import org.utbot.framework.codegen.renderer.auxiliaryClassTextById +import org.utbot.framework.codegen.renderer.utilMethodTextById +import org.utbot.framework.codegen.tree.VisibilityModifier +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.CgClassId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.DocClassLinkStmt +import org.utbot.framework.plugin.api.DocCodeStmt +import org.utbot.framework.plugin.api.DocCustomTagStatement +import org.utbot.framework.plugin.api.DocMethodLinkStmt +import org.utbot.framework.plugin.api.DocPreTagStatement +import org.utbot.framework.plugin.api.DocRegularLineStmt +import org.utbot.framework.plugin.api.DocRegularStmt +import org.utbot.framework.plugin.api.DocStatement +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.UtModel +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.objectArrayClassId +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.voidClassId + +interface CgElement { + // TODO: order of cases is important here due to inheritance between some of the element types + fun accept(visitor: CgVisitor): R = visitor.run { + when (val element = this@CgElement) { + is CgClassFile -> visit(element) + is CgClass -> visit(element) + is CgClassBody -> visit(element) + is CgStaticsRegion -> visit(element) + is CgNestedClassesRegion<*> -> visit(element) + is CgSimpleRegion<*> -> visit(element) + is CgTestMethodCluster -> visit(element) + is CgMethodsCluster -> visit(element) + is CgAuxiliaryClass -> visit(element) + is CgUtilMethod -> visit(element) + is CgFrameworkUtilMethod -> visit(element) + is CgTestMethod -> visit(element) + is CgErrorTestMethod -> visit(element) + is CgParameterizedTestDataProviderMethod -> visit(element) + is CgCommentedAnnotation -> visit(element) + is CgSingleArgAnnotation -> visit(element) + is CgMultipleArgsAnnotation -> visit(element) + is CgArrayAnnotationArgument -> visit(element) + is CgNamedAnnotationArgument -> visit(element) + is CgSingleLineComment -> visit(element) + is CgTripleSlashMultilineComment -> visit(element) + is CgMultilineComment -> visit(element) + is CgDocumentationComment -> visit(element) + is CgDocPreTagStatement -> visit(element) + is CgCustomTagStatement -> visit(element) + is CgDocCodeStmt -> visit(element) + is CgDocRegularStmt -> visit(element) + is CgDocRegularLineStmt -> visit(element) + is CgDocClassLinkStmt -> visit(element) + is CgDocMethodLinkStmt -> visit(element) + is CgAnonymousFunction -> visit(element) + is CgReturnStatement -> visit(element) + is CgArrayElementAccess -> visit(element) + is CgSpread -> visit(element) + is CgLessThan -> visit(element) + is CgGreaterThan -> visit(element) + is CgEqualTo -> visit(element) + is CgIncrement -> visit(element) + is CgDecrement -> visit(element) + is CgTryCatch -> visit(element) + is CgInnerBlock -> visit(element) + is CgForLoop -> visit(element) + is CgForEachLoop -> visit(element) + is CgWhileLoop -> visit(element) + is CgDoWhileLoop -> visit(element) + is CgBreakStatement -> visit(element) + is CgContinueStatement -> visit(element) + is CgDeclaration -> visit(element) + is CgFieldDeclaration -> visit(element) + is CgAssignment -> visit(element) + is CgTypeCast -> visit(element) + is CgIsInstance -> visit(element) + is CgThisInstance -> visit(element) + is CgNotNullAssertion -> visit(element) + is CgVariable -> visit(element) + is CgParameterDeclaration -> visit(element) + is CgFormattedString -> visit(element) + is CgLiteral -> visit(element) + is CgNonStaticRunnable -> visit(element) + is CgStaticRunnable -> visit(element) + is CgAllocateInitializedArray -> visit(element) + is CgArrayInitializer -> visit(element) + is CgAllocateArray -> visit(element) + is CgEnumConstantAccess -> visit(element) + is CgFieldAccess -> visit(element) + is CgStaticFieldAccess -> visit(element) + is CgIfStatement -> visit(element) + is CgSwitchCaseLabel -> visit(element) + is CgSwitchCase -> visit(element) + is CgLogicalAnd -> visit(element) + is CgLogicalOr -> visit(element) + is CgGetLength -> visit(element) + is CgGetJavaClass -> visit(element) + is CgGetKotlinClass -> visit(element) + is CgStatementExecutableCall -> visit(element) + is CgConstructorCall -> visit(element) + is CgMethodCall -> visit(element) + is CgThrowStatement -> visit(element) + is CgErrorWrapper -> visit(element) + is CgEmptyLine -> visit(element) + else -> throw IllegalArgumentException("Can not visit element of type ${element::class}") + } + } +} + +// Code entities + +open class CgClassFile( + open val imports: List, + open val declaredClass: CgClass, +): CgElement + +class CgClass( + val id: ClassId, + val documentation: CgDocumentationComment?, + val annotations: List, + val superclass: ClassId?, + val interfaces: List, + val body: CgClassBody, + val isStatic: Boolean, + val isNested: Boolean, + val visibility: VisibilityModifier = VisibilityModifier.PUBLIC, +): CgElement { + val packageName + get() = id.packageName + + val simpleName + get() = id.simpleName +} + +/** + * Body of a class. + * @property methodRegions regions containing methods + * @property staticDeclarationRegions regions containing static declarations. + * This is usually util methods and data providers. + * In Kotlin all static declarations must be grouped together in a companion object. + * In Java there is no such restriction, but for uniformity we are grouping + * Java static declarations together as well. It can also improve code readability. + */ +class CgClassBody( + val classId: ClassId, + val methodRegions: List, + val staticDeclarationRegions: List, + val nestedClassRegions: List>, + val fields: Set = emptySet(), +) : CgElement + +/** + * Field of a class. + * @property ownerClassId [ClassId] of the field owner class. + * @property declaration declaration itself. + * @property annotation optional annotation. + * @property visibility field visibility. + */ +class CgFieldDeclaration( + val ownerClassId: ClassId, + val declaration: CgDeclaration, + val annotation: CgAnnotation? = null, + val visibility: VisibilityModifier = VisibilityModifier.PRIVATE, +) : CgElement + +/** + * A class representing the IntelliJ IDEA's regions. + * A region is a part of code between the special starting and ending comments. + * + * @property header The header of the region, + * no ///region and ///endregion comments are generated if [header] is `null` + */ +sealed class CgRegion : CgElement { + abstract val header: String? + abstract val content: List +} + +open class CgSimpleRegion( + override val header: String?, + override val content: List +) : CgRegion() + +/** + * A region that stores some static declarations, e.g. data providers or util methods. + * There may be more than one static region in a class and they all are stored + * in a [CgClassBody.staticDeclarationRegions]. + * In case of Kotlin, they all will be rendered inside of a companion object. + */ +class CgStaticsRegion( + override val header: String?, + override val content: List +) : CgSimpleRegion(header, content) + +/** + * A region that stores all nested classes + */ +abstract class CgNestedClassesRegion( + override val header: String?, + override val content: List, +): CgRegion() + +class CgRealNestedClassesRegion(header: String? = null, nestedClasses: List): + CgNestedClassesRegion(header, nestedClasses) + +/** + * Regions with nested classes that are represented with [CgAuxiliaryClass], not [CgClass]. + */ +class CgAuxiliaryNestedClassesRegion(header: String? = null, nestedClasses: List): + CgNestedClassesRegion(header, nestedClasses) + +data class CgTestMethodCluster( + override val header: String?, + val description: CgTripleSlashMultilineComment?, + override val content: List +) : CgRegion() + +/** + * Stores all methods as one cluster + * (for e.g. in test class we can store all tests for one executable in such cluster) + */ +data class CgMethodsCluster( + override val header: String?, + override val content: List> +) : CgRegion>() { + companion object { + fun withoutDocs(methodsList: List) = CgMethodsCluster( + header = null, + content = listOf( + CgSimpleRegion( + header = null, + content = methodsList + ) + ) + ) + } +} + +/** + * Util entity is either an instance of [CgAuxiliaryClass] or [CgUtilMethod]. + * Util methods are the helper methods that we use in our generated tests, + * and auxiliary classes are the classes that util methods use. + */ +sealed class CgUtilEntity : CgElement { + internal abstract fun getText(rendererContext: CgRendererContext): String +} + +/** + * This class represents classes that are required by our util methods. + * For example, class `CapturedArgument` that is used by util methods that construct lambda values. + * + * **Note** that we call such classes **auxiliary** instead of **util** in order to avoid confusion + * with class `org.utbot.runtime.utils.UtUtils`, which is generally called an **util class**. + * `UtUtils` is a class that contains all our util methods and **auxiliary classes**. + */ +data class CgAuxiliaryClass(val id: ClassId) : CgUtilEntity() { + override fun getText(rendererContext: CgRendererContext): String { + return rendererContext.utilMethodProvider + .auxiliaryClassTextById(id, rendererContext.codegenLanguage) + } +} + +/** + * This class does not inherit from [CgMethod], because it only needs an [id], + * and it does not need to have info about all the other properties of [CgMethod]. + * This is because util methods are hardcoded. On the rendering stage their text + * is retrieved by their [MethodId]. + * + * @property id identifier of the util method. + */ +data class CgUtilMethod(val id: MethodId) : CgUtilEntity() { + override fun getText(rendererContext: CgRendererContext): String { + return with(rendererContext) { + rendererContext.utilMethodProvider + .utilMethodTextById(id, mockFrameworkUsed, mockFramework, codegenLanguage) + } + } +} + +// Methods + +sealed class CgMethod(open val isStatic: Boolean) : CgElement { + abstract val name: String + abstract val returnType: ClassId + abstract val parameters: List + abstract val statements: List + abstract val exceptions: Set + abstract val annotations: List + abstract val documentation: CgDocumentationComment + abstract val requiredFields: List + abstract val visibility: VisibilityModifier +} + +class CgTestMethod( + override val name: String, + override val returnType: ClassId = voidClassId, + override val parameters: List = emptyList(), + override val statements: List, + override val exceptions: Set = emptySet(), + override val annotations: List, + override val visibility: VisibilityModifier = VisibilityModifier.PUBLIC, + val type: CgTestMethodType, + override val documentation: CgDocumentationComment = CgDocumentationComment(emptyList()), + override val requiredFields: List = emptyList(), +) : CgMethod(isStatic = false) + +class CgFrameworkUtilMethod( + override val name: String, + override val statements: List, + override val exceptions: Set, + override val annotations: List, + override val visibility: VisibilityModifier = VisibilityModifier.PUBLIC, +) : CgMethod(isStatic = false) { + override val returnType: ClassId = voidClassId + override val parameters: List = emptyList() + override val documentation: CgDocumentationComment = CgDocumentationComment(emptyList()) + override val requiredFields: List = emptyList() +} + +class CgErrorTestMethod( + override val name: String, + override val statements: List, + override val documentation: CgDocumentationComment = CgDocumentationComment(emptyList()), + override val visibility: VisibilityModifier = VisibilityModifier.PUBLIC, +) : CgMethod(isStatic = false) { + override val exceptions: Set = emptySet() + override val returnType: ClassId = voidClassId + override val parameters: List = emptyList() + override val annotations: List = emptyList() + override val requiredFields: List = emptyList() +} + +class CgParameterizedTestDataProviderMethod( + override val name: String, + override val statements: List, + override val returnType: ClassId, + override val annotations: List, + override val exceptions: Set, + override val visibility: VisibilityModifier = VisibilityModifier.PUBLIC, +) : CgMethod(isStatic = true) { + override val parameters: List = emptyList() + override val documentation: CgDocumentationComment = CgDocumentationComment(emptyList()) + override val requiredFields: List = emptyList() +} + +enum class CgTestMethodType(val displayName: String, val isThrowing: Boolean) { + SUCCESSFUL(displayName = "Successful tests without exceptions", isThrowing = false), + PASSED_EXCEPTION(displayName = "Thrown exceptions marked as passed", isThrowing = true), + FAILING(displayName = "Failing tests (with exceptions)", isThrowing = true), + TIMEOUT(displayName = "Failing tests (with timeout)", isThrowing = true), + ARTIFICIAL(displayName = "Failing tests (with custom exception)", isThrowing = true), + CRASH(displayName = "Possibly crashing tests", isThrowing = true), + PARAMETRIZED(displayName = "Parametrized tests", isThrowing = false); + + override fun toString(): String = displayName +} + +// Annotations + +enum class AnnotationTarget { + Class, + + Method, + + Field, +} + +abstract class CgAnnotation : CgElement { + abstract val classId: ClassId + abstract val target: AnnotationTarget +} + +/** + * NOTE: use `StatementConstructor.addAnnotation` + * instead of explicit constructor call. + */ +class CgCommentedAnnotation(val annotation: CgAnnotation) : CgAnnotation() { + override val classId: ClassId = annotation.classId + override val target: AnnotationTarget = annotation.target +} + +/** + * NOTE: use `StatementConstructor.addAnnotation` + * instead of explicit constructor call. + */ +class CgSingleArgAnnotation( + override val classId: ClassId, + val argument: CgExpression, + override val target: AnnotationTarget, +) : CgAnnotation() + +/** + * NOTE: use `StatementConstructor.addAnnotation` + * instead of explicit constructor call. + */ +data class CgMultipleArgsAnnotation( + override val classId: ClassId, + val arguments: MutableList, + override val target: AnnotationTarget, +) : CgAnnotation() + +data class CgArrayAnnotationArgument( + val values: List +) : CgExpression { + override val type: ClassId = objectArrayClassId // TODO: is this type correct? +} + +class CgNamedAnnotationArgument( + val name: String, + val value: CgExpression +) : CgElement + +// Statements + +interface CgStatement : CgElement + +// Comments + +sealed class CgComment : CgStatement + +data class CgSingleLineComment(val comment: String) : CgComment() + +/** + * A comment that consists of multiple lines. + * The appearance of such comment may vary depending + * on the [CgAbstractMultilineComment] inheritor being used. + * Each inheritor is rendered differently. + */ +sealed class CgAbstractMultilineComment : CgComment() { + abstract val lines: List +} + +/** + * Multiline comment where each line starts with /// + */ +data class CgTripleSlashMultilineComment(override val lines: List) : CgAbstractMultilineComment() + +/** + * Classic Java multiline comment starting with /* and ending with */ + */ +data class CgMultilineComment(override val lines: List) : CgAbstractMultilineComment() { + constructor(line: String) : this(listOf(line)) +} + +//class CgDocumentationComment(val lines: List) : CgComment { +// constructor(text: String?) : this(text?.split("\n") ?: listOf()) +//} +data class CgDocumentationComment(val lines: List) : CgComment() { + constructor(text: String?) : this(text?.split("\n")?.map { CgDocRegularStmt(it) }?.toList() ?: listOf()) + + override fun equals(other: Any?): Boolean = + if (other is CgDocumentationComment) this.hashCode() == other.hashCode() else false + + override fun hashCode(): Int = lines.hashCode() +} + +sealed class CgDocStatement : CgStatement { //todo is it really CgStatement or maybe something else? + abstract fun isEmpty(): Boolean +} + +sealed class CgDocTagStatement : CgDocStatement() { + abstract val content: List + + override fun isEmpty(): Boolean = content.all { it.isEmpty() } +} + +data class CgDocPreTagStatement(override val content: List) : CgDocTagStatement() + +/** + * Represents a type for statements containing custom JavaDoc tags. + */ +data class CgCustomTagStatement(override val content: List) : CgDocTagStatement() + +data class CgDocCodeStmt(val stmt: String) : CgDocStatement() { + override fun isEmpty(): Boolean = stmt.isEmpty() +} + +data class CgDocRegularStmt(val stmt: String) : CgDocStatement() { + override fun isEmpty(): Boolean = stmt.isEmpty() +} + +/** + * Represents an element of a whole line of a multiline comment. + */ +data class CgDocRegularLineStmt(val stmt: String) : CgDocStatement() { + override fun isEmpty(): Boolean = stmt.isEmpty() +} + +open class CgDocClassLinkStmt(val className: String) : CgDocStatement() { + override fun isEmpty(): Boolean = className.isEmpty() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CgDocClassLinkStmt + + if (className != other.className) return false + + return true + } + + override fun hashCode(): Int { + return className.hashCode() + } +} + +data class CgDocMethodLinkStmt(val methodName: String, val stmt: String) : CgDocClassLinkStmt(stmt) + +fun convertDocToCg(stmt: DocStatement): CgDocStatement { + return when (stmt) { + is DocPreTagStatement -> { + val stmts = stmt.content.map { convertDocToCg(it) } + CgDocPreTagStatement(content = stmts) + } + is DocCustomTagStatement -> { + val stmts = stmt.content.map { convertDocToCg(it) } + CgCustomTagStatement(content = stmts) + } + is DocRegularStmt -> CgDocRegularStmt(stmt = stmt.stmt) + is DocRegularLineStmt -> CgDocRegularLineStmt(stmt = stmt.stmt) + is DocClassLinkStmt -> CgDocClassLinkStmt(className = stmt.className) + is DocMethodLinkStmt -> CgDocMethodLinkStmt(methodName = stmt.methodName, stmt = stmt.className) + is DocCodeStmt -> CgDocCodeStmt(stmt = stmt.stmt) + } +} + +// Anonymous function (lambda) + +data class CgAnonymousFunction( + override val type: ClassId, + val parameters: List, + val body: List +) : CgExpression + +// Return statement + +data class CgReturnStatement(val expression: CgExpression) : CgStatement + +// Array element access + +// TODO: check nested array element access expressions e.g. a[0][1][2] +// TODO in general it is not CgReferenceExpression because array element can have a primitive type +data class CgArrayElementAccess(val array: CgExpression, val index: CgExpression) : CgReferenceExpression { + override val type: ClassId = array.type.elementClassId ?: objectClassId +} + +// Loop conditions +sealed class CgComparison : CgExpression { + abstract val left: CgExpression + abstract val right: CgExpression + + override val type: ClassId = booleanClassId +} + +data class CgLessThan( + override val left: CgExpression, + override val right: CgExpression +) : CgComparison() + +data class CgGreaterThan( + override val left: CgExpression, + override val right: CgExpression +) : CgComparison() + +data class CgEqualTo( + override val left: CgExpression, + override val right: CgExpression +) : CgComparison() + +// Increment and decrement + +data class CgIncrement(val variable: CgVariable) : CgStatement + +data class CgDecrement(val variable: CgVariable) : CgStatement + +// Inner block in method (keeps parent method fields visible) + +data class CgInnerBlock(val statements: List) : CgStatement + +// Try-catch + +// for now finally clause is not supported +data class CgTryCatch( + val statements: List, + val handlers: List, + val finally: List?, + val resources: List? = null +) : CgStatement + +// ?: error("") + +data class CgErrorWrapper( + val message: String, + val expression: CgExpression, +) : CgExpression { + override val type: ClassId + get() = expression.type +} + +// Loops + +sealed class CgLoop : CgStatement { + abstract val condition: CgExpression // TODO: how to represent conditions + abstract val statements: List +} + +data class CgForLoop( + val initialization: CgDeclaration, + override val condition: CgExpression, + val update: CgStatement, + override val statements: List +) : CgLoop() + +data class CgWhileLoop( + override val condition: CgExpression, + override val statements: List +) : CgLoop() + +data class CgDoWhileLoop( + override val condition: CgExpression, + override val statements: List +) : CgLoop() + +/** + * @property condition represents variable in foreach loop + * @property iterable represents iterable in foreach loop + * @property statements represents statements in foreach loop + */ +data class CgForEachLoop( + override val condition: CgExpression, + override val statements: List, + val iterable: CgReferenceExpression, +) : CgLoop() + +// Control statements + +object CgBreakStatement : CgStatement +object CgContinueStatement : CgStatement + +// Variable declaration + +class CgDeclaration( + val variableType: ClassId, + val variableName: String, + val initializer: CgExpression?, + val isMutable: Boolean = false, +) : CgStatement { + val variable: CgVariable + get() = CgVariable(variableName, variableType) +} + +// Variable assignment + +data class CgAssignment( + val lValue: CgExpression, + val rValue: CgExpression +) : CgStatement + +// Expressions + +interface CgExpression : CgStatement { + val type: ClassId +} + +// marker interface representing expressions returning reference +// TODO: it seems that not all [CgValue] implementations are reference expressions +interface CgReferenceExpression : CgExpression + +/** + * Type cast model + * + * @property isSafetyCast shows if we should use "as?" instead of "as" in Kotlin code + */ +data class CgTypeCast( + val targetType: ClassId, + val expression: CgExpression, + val isSafetyCast: Boolean = false, +) : CgExpression { + override val type: ClassId = targetType +} + +/** + * Represents [java.lang.Class.isInstance] method. + */ +data class CgIsInstance( + val classExpression: CgExpression, + val value: CgExpression, +): CgExpression { + override val type: ClassId = booleanClassId +} + +// Value + +// TODO in general CgLiteral is not CgReferenceExpression because it can hold primitive values +interface CgValue : CgReferenceExpression + +// This instance + +data class CgThisInstance(override val type: ClassId) : CgValue + +// Variables + +open class CgVariable( + val name: String, + override val type: ClassId, +) : CgValue { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CgVariable + + if (name != other.name) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + type.hashCode() + return result + } + + override fun toString(): String { + return "${type.simpleName} $name" + } +} + +/** + * If expression is a variable, then this is a variable + * with explicit not null annotation if this is required in language. + * + * In Kotlin the difference is in addition of "!!" to the expression + */ +data class CgNotNullAssertion(val expression: CgExpression) : CgValue { + override val type: ClassId + get() = when (val expressionType = expression.type) { + is BuiltinClassId -> BuiltinClassId( + canonicalName = expressionType.canonicalName, + simpleName = expressionType.simpleName, + isNullable = false, + ) + else -> ClassId( + expressionType.name, + expressionType.elementClassId, + isNullable = false, + ) + } +} + +/** + * Method parameters declaration + * + * @property isReferenceType is used for rendering nullable types in Kotlin codegen. + */ +data class CgParameterDeclaration( + val parameter: CgVariable, + val isVararg: Boolean = false, + val isReferenceType: Boolean = false +) : CgElement { + constructor(name: String, type: ClassId, isReferenceType: Boolean = false) : this( + CgVariable(name, type), + isReferenceType = isReferenceType + ) + + val name: String + get() = parameter.name + + val type: ClassId + get() = parameter.type +} + +/** + * Test method parameter can be one of the following types: + * - argument of MUT with a certain index + * - result expected from MUT with the given arguments + * - exception expected from MUT with the given arguments + */ +sealed class CgParameterKind { + object ThisInstance : CgParameterKind() + data class Argument(val index: Int) : CgParameterKind() + data class Statics(val model: UtModel) : CgParameterKind() + object ExpectedResult : CgParameterKind() + object ExpectedException : CgParameterKind() +} + + +// Primitive and String literals + +data class CgLiteral(override val type: ClassId, val value: Any?) : CgValue { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CgLiteral + + if (type != other.type) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + (value?.hashCode() ?: 0) + return result + } +} + +// Runnable like this::toString or (new Object())::toString (non-static) or Random::nextRandomInt (static) etc +abstract class CgRunnable : CgValue { + abstract val methodId: MethodId +} + +/** + * [referenceExpression] is "this" in this::toString or (new Object()) in (new Object())::toString (non-static) + */ +data class CgNonStaticRunnable( + override val type: ClassId, + val referenceExpression: CgReferenceExpression, + override val methodId: MethodId +) : CgRunnable() + +/** + * [classId] is Random is Random::nextRandomInt (static) etc + */ +data class CgStaticRunnable( + override val type: ClassId, + val classId: ClassId, + override val methodId: MethodId, +) : CgRunnable() + +// Array allocation + +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 { + workaround(WorkaroundReason.ARRAY_ELEMENT_TYPES_ALWAYS_NULLABLE) { + // for now all array element types are nullable + updateElementType(elementType) + } + } + + private fun updateElementType(type: ClassId): ClassId = + if (type.elementClassId != null) { + CgClassId(type.name, updateElementType(type.elementClassId!!), isNullable = true) + } else { + CgClassId(type, isNullable = true) + } +} + +/** + * Allocation of an array with initialization. For example: `new String[] {"a", "b", null}`. + */ +data class CgAllocateInitializedArray(val initializer: CgArrayInitializer) : + CgAllocateArray(initializer.arrayType, initializer.elementType, initializer.size) + +data class CgArrayInitializer(val arrayType: ClassId, val elementType: ClassId, val values: List) : + CgExpression { + val size: Int + get() = values.size + + override val type: ClassId + get() = arrayType +} + + +// Spread operator (for Kotlin, empty for Java) + +data class CgSpread(override val type: ClassId, val array: CgExpression) : CgExpression + +// Interpolated string +// e.g. String.format() for Java, "${}" for Kotlin + +class CgFormattedString(val array: List) : CgElement + +// Enum constant + +data class CgEnumConstantAccess( + val enumClass: ClassId, + val name: String +) : CgReferenceExpression { + override val type: ClassId = enumClass +} + +// Property access + +// TODO in general it is not CgReferenceExpression because field can have a primitive type +abstract class CgAbstractFieldAccess : CgReferenceExpression { + abstract val fieldId: FieldId + + override val type: ClassId + get() = fieldId.type +} + +data class CgFieldAccess( + val caller: CgExpression, + override val fieldId: FieldId +) : CgAbstractFieldAccess() + +data class CgStaticFieldAccess( + override val fieldId: FieldId +) : CgAbstractFieldAccess() { + val declaringClass: ClassId = fieldId.declaringClass + val fieldName: String = fieldId.name +} + +// Conditional statements + +data class CgIfStatement( + val condition: CgExpression, + val trueBranch: List, + val falseBranch: List? = null // false branch may be absent +) : CgStatement + +data class CgSwitchCaseLabel( + val label: CgLiteral? = null, // have to be compile time constant (null for default label) + val statements: List, + val addBreakStatementToEnd: Boolean = true // do not set this field to "true" value if you manually added "break" to statements +) : CgStatement + +data class CgSwitchCase( + val value: CgExpression, // TODO required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum' + val labels: List, + val defaultLabel: CgSwitchCaseLabel? = null +) : CgStatement + +// Binary logical operators + +data class CgLogicalAnd( + val left: CgExpression, + val right: CgExpression +) : CgExpression { + override val type: ClassId = booleanClassId +} + +data class CgLogicalOr( + val left: CgExpression, + val right: CgExpression +) : CgExpression { + override val type: ClassId = booleanClassId +} + +// Acquisition of array length, e.g. args.length + +/** + * @param variable represents an array variable + */ +data class CgGetLength(val variable: CgVariable) : CgExpression { + override val type: ClassId = intClassId +} + +// Acquisition of java or kotlin class, e.g. MyClass.class in Java, MyClass::class.java in Kotlin or MyClass::class for Kotlin classes + +sealed class CgGetClass : CgReferenceExpression { + abstract val classId: ClassId + + override val type: ClassId = Class::class.id +} + +/** + * NOTE: use `CgContext.createGetClassExpression` + * instead of the explicit constructor call. + */ +data class CgGetJavaClass(override val classId: ClassId) : CgGetClass() + +/** + * NOTE: use `CgContext.createGetClassExpression` + * instead of the explicit constructor call. + */ +data class CgGetKotlinClass(override val classId: ClassId) : CgGetClass() + +// Executable calls + +data class CgStatementExecutableCall(val call: CgExecutableCall) : CgStatement + +// TODO in general it is not CgReferenceExpression because returned value can have a primitive type +// (or no value can be returned) +abstract class CgExecutableCall : CgReferenceExpression { + abstract val executableId: ExecutableId + abstract val arguments: List + abstract val typeParameters: TypeParameters +} + +data class CgConstructorCall( + override val executableId: ConstructorId, + override val arguments: List, + override val typeParameters: TypeParameters = TypeParameters() +) : CgExecutableCall() { + override val type: ClassId = executableId.classId +} + +data class CgMethodCall( + val caller: CgExpression?, + override val executableId: MethodId, + override val arguments: List, + override val typeParameters: TypeParameters = TypeParameters() +) : CgExecutableCall() { + override val type: ClassId = executableId.returnType +} + +fun CgExecutableCall.toStatement(): CgStatementExecutableCall = CgStatementExecutableCall(this) + +// Throw statement + +data class CgThrowStatement( + val exception: CgExpression +) : CgStatement + +// Empty line + +object CgEmptyLine : CgStatement + +data class CgExceptionHandler( + val exception: CgVariable, + val statements: List +) \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgMethodTestSet.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgMethodTestSet.kt new file mode 100644 index 0000000000..05bbbf1b04 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgMethodTestSet.kt @@ -0,0 +1,140 @@ +package org.utbot.framework.codegen.domain.models + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtClusterInfo +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.UtMethodTestSet +import org.utbot.framework.plugin.api.UtSymbolicExecution +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.fuzzer.UtFuzzedExecution + +data class CgMethodTestSet( + val executableUnderTest: ExecutableId, + val errors: Map = emptyMap(), + val clustersInfo: List>, +) { + val executablesToCall get() = + executions.map { it.executableToCall ?: executableUnderTest } + .distinctBy { it.classId to it.signature } + + var executions: List = emptyList() + private set + + constructor(from: UtMethodTestSet) : this(from.method, from.errors, from.clustersInfo) { + executions = from.executions + } + + /** + * Constructor for JavaScript and Python purposes. + */ + constructor( + executableId: ExecutableId, + errors: Map = emptyMap(), + executions: List = emptyList(), + clustersInfo: List> = listOf(null to executions.indices), + ) : this(executableId, errors, clustersInfo) { + this.executions = executions + } + + fun prepareTestSetsForParameterizedTestGeneration(): List { + val testSetList = mutableListOf() + + // Mocks are not supported in parametrized tests, so we exclude them + val testSetWithoutMocking = this.excludeExecutionsWithMocking() + for (splitByExecutionTestSet in testSetWithoutMocking.splitExecutionsByResult()) { + for (splitByChangedStaticsTestSet in splitByExecutionTestSet.splitExecutionsByChangedStatics()) { + testSetList += splitByChangedStaticsTestSet + } + } + + return testSetList + } + + /** + * Finds a [ClassId] of all result models in executions. + * + * Tries to find a unique result type in testSets or + * gets executable return type. + * + * Returns `null` if there are multiple distinct [executablesToCall]. + */ + fun getCommonResultTypeOrNull(): ClassId? { + val executableId = executablesToCall.singleOrNull() ?: return null + return when (executableId.returnType) { + voidClassId -> executableId.returnType + else -> { + val successfulExecutions = executions.filter { it.result is UtExecutionSuccess } + if (successfulExecutions.isNotEmpty()) { + successfulExecutions + .map { (it.result as UtExecutionSuccess).model.classId } + .distinct() + .singleOrNull() + ?: executableId.returnType + } else { + executableId.returnType + } + } + } + } + + /** + * Splits [CgMethodTestSet] into separate test sets having + * unique result model [ClassId] in each subset. + */ + private fun splitExecutionsByResult() : List { + val successfulExecutions = executions.filter { it.result is UtExecutionSuccess } + val failureExecutions = executions.filter { it.result is UtExecutionFailure } + + val executionsByResult: MutableMap> = + successfulExecutions + .groupBy { (it.result as UtExecutionSuccess).model.classId }.toMutableMap() + + // if we have failure executions, we add them to the first successful executions group + val groupClassId = executionsByResult.keys.firstOrNull() + if (groupClassId != null) { + executionsByResult[groupClassId] = executionsByResult[groupClassId]!! + failureExecutions + } else { + executionsByResult[objectClassId] = failureExecutions + } + + return executionsByResult.map{ (_, executions) -> substituteExecutions(executions) } + } + + /** + * Splits [CgMethodTestSet] test sets by affected static fields statics. + * + * A separate test set is created for each combination of modified statics. + */ + private fun splitExecutionsByChangedStatics(): List { + val executionsByStaticsUsage = executions.groupBy { it.stateBefore.statics.keys } + + val executionsByStaticsUsageAndTheirTypes = executionsByStaticsUsage + .flatMap { (_, executions) -> + executions.groupBy { it.stateBefore.statics.values.map { model -> model.classId } }.values + } + + return executionsByStaticsUsageAndTheirTypes.map { executions -> substituteExecutions(executions) } + } + + /** + * Excludes [UtFuzzedExecution] and [UtSymbolicExecution] with mocking from [CgMethodTestSet]. + * + * It is used in parameterized test generation. + * We exclude them because we cannot track force mocking occurrences in fuzzing process + * and cannot deal with mocking in parameterized mode properly. + */ + private fun excludeExecutionsWithMocking(): CgMethodTestSet { + val symbolicExecutionsWithoutMocking = executions + .filterIsInstance() + .filter { !it.containsMocking } + + return substituteExecutions(symbolicExecutionsWithoutMocking) + } + + fun substituteExecutions(newExecutions: List): CgMethodTestSet = + copy().apply { executions = newExecutions } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/TestClassModel.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/TestClassModel.kt new file mode 100644 index 0000000000..d218e87451 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/TestClassModel.kt @@ -0,0 +1,33 @@ +package org.utbot.framework.codegen.domain.models + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.mapper.UtModelDeepMapper +import org.utbot.framework.plugin.api.mapper.mapStateBeforeModels + +/** + * Stores method test sets in a structure that replicates structure of their methods in [classUnderTest]. + * I.e., if some method is declared in nested class of [classUnderTest], its testset will be put + * in [TestClassModel] in one of [nestedClasses] + */ +abstract class TestClassModel( + val classUnderTest: ClassId, + val methodTestSets: List, + val nestedClasses: List, +) + +class SimpleTestClassModel( + classUnderTest: ClassId, + methodTestSets: List, + nestedClasses: List = listOf(), +): TestClassModel(classUnderTest, methodTestSets, nestedClasses) + +fun SimpleTestClassModel.mapStateBeforeModels(mapperProvider: () -> UtModelDeepMapper) = + SimpleTestClassModel( + classUnderTest = classUnderTest, + nestedClasses = nestedClasses, + methodTestSets = methodTestSets.map { testSet -> + testSet.substituteExecutions( + testSet.executions.map { execution -> execution.mapStateBeforeModels(mapperProvider()) } + ) + } + ) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/builders/SimpleTestClassModelBuilder.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/builders/SimpleTestClassModelBuilder.kt new file mode 100644 index 0000000000..41f1b6824b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/builders/SimpleTestClassModelBuilder.kt @@ -0,0 +1,55 @@ +package org.utbot.framework.codegen.domain.models.builders + +import org.utbot.framework.codegen.domain.UtModelWrapper +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.SimpleTestClassModel +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.enclosingClass + +typealias TypedModelWrappers = Map> + +open class SimpleTestClassModelBuilder: TestClassModelBuilder() { + override fun createTestClassModel( + classUnderTest: ClassId, + testSets: List, + ): SimpleTestClassModel { + // For each class stores list of methods declared in this class (methods from nested classes are excluded) + val class2methodTestSets = testSets.groupBy { it.executableUnderTest.classId } + + val classesWithMethodsUnderTest = testSets + .map { it.executableUnderTest.classId } + .distinct() + + // For each class stores list of its "direct" nested classes + val class2nestedClasses = mutableMapOf>() + + for (classId in classesWithMethodsUnderTest) { + var currentClass = classId + var enclosingClass = currentClass.enclosingClass + // while we haven't reached the top of nested class hierarchy or the main class under test + while (enclosingClass != null && currentClass != classUnderTest) { + class2nestedClasses.getOrPut(enclosingClass) { mutableSetOf() } += currentClass + currentClass = enclosingClass + enclosingClass = enclosingClass.enclosingClass + } + } + + return constructRecursively(classUnderTest, class2methodTestSets, class2nestedClasses) + } + + private fun constructRecursively( + clazz: ClassId, + class2methodTestSets: Map>, + class2nestedClasses: Map> + ): SimpleTestClassModel { + val currentNestedClasses = class2nestedClasses.getOrDefault(clazz, listOf()) + val currentMethodTestSets = class2methodTestSets.getOrDefault(clazz, listOf()) + return SimpleTestClassModel( + clazz, + currentMethodTestSets, + currentNestedClasses.map { + constructRecursively(it, class2methodTestSets, class2nestedClasses) + } + ) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/builders/TestClassModelBuilder.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/builders/TestClassModelBuilder.kt new file mode 100644 index 0000000000..a1f47e873e --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/builders/TestClassModelBuilder.kt @@ -0,0 +1,9 @@ +package org.utbot.framework.codegen.domain.models.builders + +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.TestClassModel +import org.utbot.framework.plugin.api.ClassId + +abstract class TestClassModelBuilder { + abstract fun createTestClassModel(classUnderTest: ClassId, testSets: List): TestClassModel +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/AbstractCodeGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/AbstractCodeGenerator.kt new file mode 100644 index 0000000000..a7a05831f5 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/AbstractCodeGenerator.kt @@ -0,0 +1,80 @@ +package org.utbot.framework.codegen.generator + +import mu.KotlinLogging +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.plugin.api.UtMethodTestSet +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +abstract class AbstractCodeGenerator(params: CodeGeneratorParams) { + protected val logger = KotlinLogging.logger {} + + open var context: CgContext = with(params) { + CgContext( + classUnderTest = classUnderTest, + projectType = projectType, + generateUtilClassFile = generateUtilClassFile, + paramNames = paramNames, + testFramework = testFramework, + mockFramework = mockFramework, + codegenLanguage = codegenLanguage, + cgLanguageAssistant = cgLanguageAssistant, + parametrizedTestSource = parameterizedTestSource, + staticsMocking = staticsMocking, + forceStaticMocking = forceStaticMocking, + generateWarningsForStaticMocking = generateWarningsForStaticMocking, + runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, + hangingTestsTimeout = hangingTestsTimeout, + enableTestsTimeout = enableTestsTimeout, + testClassPackageName = testClassPackageName + ) + } + + //TODO: we support custom test class name only in utbot-online, probably support them in plugin as well + fun generateAsString(testSets: Collection, testClassCustomName: String? = null): String = + generateAsStringWithTestReport(testSets, testClassCustomName).generatedCode + + //TODO: we support custom test class name only in utbot-online, probably support them in plugin as well + fun generateAsStringWithTestReport( + testSets: Collection, + testClassCustomName: String? = null, + ): CodeGeneratorResult { + val cgTestSets = testSets.map { CgMethodTestSet(it) }.toList() + return withCustomContext(testClassCustomName) { + context.withTestClassFileScope { + generate(cgTestSets) + } + } + } + + protected abstract fun generate(testSets: List): CodeGeneratorResult + + protected fun renderToString(testClassFile: CgClassFile): String { + logger.info { "Rendering phase started at ${now()}" } + val renderer = CgAbstractRenderer.makeRenderer(context) + testClassFile.accept(renderer) + logger.info { "Rendering phase finished at ${now()}" } + + return renderer.toString() + } + + protected fun now(): String = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")) + + /** + * Wrapper function that configures context as needed for utbot-online: + * - turns on imports optimization in code generator + * - passes a custom test class name if there is one + */ + fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { + val prevContext = context + return try { + context = prevContext.customCopy(shouldOptimizeImports = true, testClassCustomName = testClassCustomName) + block() + } finally { + context = prevContext + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/CodeGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/CodeGenerator.kt new file mode 100644 index 0000000000..d43eb7079b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/CodeGenerator.kt @@ -0,0 +1,29 @@ +package org.utbot.framework.codegen.generator + +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.builders.SimpleTestClassModelBuilder +import org.utbot.framework.codegen.tree.CgSimpleTestClassConstructor +import org.utbot.framework.codegen.tree.ututils.UtilClassKind +import org.utbot.framework.plugin.api.ClassId + +open class CodeGenerator(params: CodeGeneratorParams): AbstractCodeGenerator(params) { + protected val classUnderTest: ClassId = params.classUnderTest + + override fun generate(testSets: List): CodeGeneratorResult { + val testClassModel = SimpleTestClassModelBuilder().createTestClassModel(classUnderTest, testSets) + + logger.info { "Code generation phase started at ${now()}" } + val astConstructor = CgSimpleTestClassConstructor(context) + val testClassFile = astConstructor.construct(testClassModel) + logger.info { "Code generation phase finished at ${now()}" } + + val generatedCode = renderToString(testClassFile) + + return CodeGeneratorResult( + generatedCode = generatedCode, + utilClassKind = UtilClassKind.fromCgContextOrNull(context), + testsGenerationReport = astConstructor.testsGenerationReport, + ) + } +} + diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/CodeGeneratorParams.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/CodeGeneratorParams.kt new file mode 100644 index 0000000000..5bbc205307 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/CodeGeneratorParams.kt @@ -0,0 +1,33 @@ +package org.utbot.framework.codegen.generator + +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.services.language.CgLanguageAssistant +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MockFramework + +data class CodeGeneratorParams( + val classUnderTest: ClassId, + val projectType: ProjectType, + val paramNames: MutableMap> = mutableMapOf(), + val generateUtilClassFile: Boolean = false, + val testFramework: TestFramework = TestFramework.defaultItem, + val mockFramework: MockFramework = MockFramework.defaultItem, + val staticsMocking: StaticsMocking = StaticsMocking.defaultItem, + val forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem, + val generateWarningsForStaticMocking: Boolean = true, + val codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem, + val cgLanguageAssistant: CgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(codegenLanguage), + val parameterizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem, + val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, + val hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), + val enableTestsTimeout: Boolean = true, + val testClassPackageName: String = classUnderTest.packageName, +) \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/CodeGeneratorResult.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/CodeGeneratorResult.kt new file mode 100644 index 0000000000..67db5a0bee --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/CodeGeneratorResult.kt @@ -0,0 +1,16 @@ +package org.utbot.framework.codegen.generator + +import org.utbot.framework.codegen.reports.TestsGenerationReport +import org.utbot.framework.codegen.tree.ututils.UtilClassKind + +/** + * @property generatedCode the source code of the test class + * @property testsGenerationReport some info about test generation process + * @property utilClassKind the kind of util class if it is required, otherwise - null + */ +data class CodeGeneratorResult( + val generatedCode: String, + val testsGenerationReport: TestsGenerationReport, + // null if no util class needed, e.g. when we are generating utils directly into test class + val utilClassKind: UtilClassKind? = null, +) \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt deleted file mode 100644 index a8e632976c..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt +++ /dev/null @@ -1,257 +0,0 @@ -package org.utbot.framework.codegen.model - -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.HangingTestsTimeout -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider -import org.utbot.framework.codegen.model.constructor.builtin.UtilClassFileMethodProvider -import org.utbot.framework.codegen.model.constructor.CgMethodTestSet -import org.utbot.framework.codegen.model.constructor.context.CgContext -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor -import org.utbot.framework.codegen.model.constructor.tree.CgUtilClassConstructor -import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport -import org.utbot.framework.codegen.model.tree.AbstractCgClassFile -import org.utbot.framework.codegen.model.tree.CgRegularClassFile -import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.MockFramework -import org.utbot.framework.plugin.api.UtMethodTestSet -import org.utbot.framework.codegen.model.constructor.TestClassModel -import org.utbot.framework.codegen.model.tree.CgComment -import org.utbot.framework.codegen.model.tree.CgSingleLineComment - -class CodeGenerator( - private val classUnderTest: ClassId, - paramNames: MutableMap> = mutableMapOf(), - generateUtilClassFile: Boolean = false, - testFramework: TestFramework = TestFramework.defaultItem, - mockFramework: MockFramework = MockFramework.defaultItem, - staticsMocking: StaticsMocking = StaticsMocking.defaultItem, - forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem, - generateWarningsForStaticMocking: Boolean = true, - codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem, - parameterizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem, - runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, - hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), - enableTestsTimeout: Boolean = true, - testClassPackageName: String = classUnderTest.packageName, -) { - private var context: CgContext = CgContext( - classUnderTest = classUnderTest, - generateUtilClassFile = generateUtilClassFile, - paramNames = paramNames, - testFramework = testFramework, - mockFramework = mockFramework, - codegenLanguage = codegenLanguage, - parametrizedTestSource = parameterizedTestSource, - staticsMocking = staticsMocking, - forceStaticMocking = forceStaticMocking, - generateWarningsForStaticMocking = generateWarningsForStaticMocking, - runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, - hangingTestsTimeout = hangingTestsTimeout, - enableTestsTimeout = enableTestsTimeout, - testClassPackageName = testClassPackageName - ) - - //TODO: we support custom test class name only in utbot-online, probably support them in plugin as well - fun generateAsString(testSets: Collection, testClassCustomName: String? = null): String = - generateAsStringWithTestReport(testSets, testClassCustomName).generatedCode - - //TODO: we support custom test class name only in utbot-online, probably support them in plugin as well - fun generateAsStringWithTestReport( - testSets: Collection, - testClassCustomName: String? = null, - ): CodeGeneratorResult { - val cgTestSets = testSets.map { CgMethodTestSet(it) }.toList() - return generateAsStringWithTestReport(cgTestSets, testClassCustomName) - } - - private fun generateAsStringWithTestReport( - cgTestSets: List, - testClassCustomName: String? = null, - ): CodeGeneratorResult = withCustomContext(testClassCustomName) { - context.withTestClassFileScope { - val testClassModel = TestClassModel.fromTestSets(classUnderTest, cgTestSets) - val testClassFile = CgTestClassConstructor(context).construct(testClassModel) - CodeGeneratorResult( - generatedCode = renderClassFile(testClassFile), - utilClassKind = UtilClassKind.fromCgContextOrNull(context), - testsGenerationReport = testClassFile.testsGenerationReport - ) - } - } - - /** - * Wrapper function that configures context as needed for utbot-online: - * - turns on imports optimization in code generator - * - passes a custom test class name if there is one - */ - private fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { - val prevContext = context - return try { - context = prevContext.copy( - shouldOptimizeImports = true, - testClassCustomName = testClassCustomName - ) - block() - } finally { - context = prevContext - } - } - - private fun renderClassFile(file: AbstractCgClassFile<*>): String { - val renderer = CgAbstractRenderer.makeRenderer(context) - file.accept(renderer) - return renderer.toString() - } -} - -/** - * @property generatedCode the source code of the test class - * @property utilClassKind the kind of util class if it is required, otherwise - null - * @property testsGenerationReport some info about test generation process - */ -data class CodeGeneratorResult( - val generatedCode: String, - // null if no util class needed, e.g. when we are generating utils directly into test class - val utilClassKind: UtilClassKind?, - val testsGenerationReport: TestsGenerationReport -) - -/** - * A kind of util class. See the description of each kind at their respective classes. - * @property utilMethodProvider a [UtilClassFileMethodProvider] containing information about - * utilities that come from a separately generated UtUtils class - * (as opposed to utils that are declared directly in the test class, for example). - * @property mockFrameworkUsed a flag indicating if a mock framework was used. - * For detailed description see [CgContextOwner.mockFrameworkUsed]. - * @property mockFramework a framework used to create mocks - * @property priority when we generate multiple test classes, they can require different [UtilClassKind]. - * We will generate an util class corresponding to the kind with the greatest priority. - * For example, one test class may not use mocks, but the other one does. - * Then we will generate an util class with mocks, because it has a greater priority (see [UtUtilsWithMockito]). - */ -sealed class UtilClassKind( - internal val utilMethodProvider: UtilClassFileMethodProvider, - internal val mockFrameworkUsed: Boolean, - internal val mockFramework: MockFramework = MockFramework.MOCKITO, - private val priority: Int -) : Comparable { - - /** - * The version of util class being generated. - * For more details see [UtilClassFileMethodProvider.UTIL_CLASS_VERSION]. - */ - val utilClassVersion: String - get() = UtilClassFileMethodProvider.UTIL_CLASS_VERSION - - /** - * The comment specifying the version of util class being generated. - * - * @see UtilClassFileMethodProvider.UTIL_CLASS_VERSION - */ - val utilClassVersionComment: CgComment - get() = CgSingleLineComment("$UTIL_CLASS_VERSION_COMMENT_PREFIX${utilClassVersion}") - - - /** - * The comment specifying the kind of util class being generated. - * - * @see utilClassKindCommentText - */ - val utilClassKindComment: CgComment - get() = CgSingleLineComment(utilClassKindCommentText) - - /** - * The text of comment specifying the kind of util class. - * At the moment, there are two kinds: [RegularUtUtils] (without Mockito) and [UtUtilsWithMockito]. - * - * This comment is needed when the plugin decides whether to overwrite an existing util class or not. - * When making that decision, it is important to determine if the existing class uses mocks or not, - * and this comment will help do that. - */ - abstract val utilClassKindCommentText: String - - /** - * A kind of regular UtUtils class. "Regular" here means that this class does not use a mock framework. - */ - object RegularUtUtils : UtilClassKind(UtilClassFileMethodProvider, mockFrameworkUsed = false, priority = 0) { - override val utilClassKindCommentText: String - get() = "This is a regular UtUtils class (without mock framework usage)" - } - - /** - * A kind of UtUtils class that uses a mock framework. At the moment the framework is Mockito. - */ - object UtUtilsWithMockito : UtilClassKind(UtilClassFileMethodProvider, mockFrameworkUsed = true, priority = 1) { - override val utilClassKindCommentText: String - get() = "This is UtUtils class with Mockito support" - } - - override fun compareTo(other: UtilClassKind): Int { - return priority.compareTo(other.priority) - } - - /** - * Construct an util class file as a [CgRegularClassFile] and render it. - * @return the text of the generated util class file. - */ - fun getUtilClassText(codegenLanguage: CodegenLanguage): String { - val utilClassFile = CgUtilClassConstructor.constructUtilsClassFile(this) - val renderer = CgAbstractRenderer.makeRenderer(this, codegenLanguage) - utilClassFile.accept(renderer) - return renderer.toString() - } - - companion object { - - /** - * Class UtUtils will contain a comment specifying the version of this util class - * (if we ever change util methods, then util class will be different, hence the update of its version). - * This is a prefix that will go before the version in the comment. - */ - const val UTIL_CLASS_VERSION_COMMENT_PREFIX = "UtUtils class version: " - - fun utilClassKindByCommentOrNull(comment: String): UtilClassKind? { - return when (comment) { - RegularUtUtils.utilClassKindCommentText -> RegularUtUtils - UtUtilsWithMockito.utilClassKindCommentText -> UtUtilsWithMockito - else -> null - } - } - - /** - * Check if an util class is required, and if so, what kind. - * @return `null` if [CgContext.utilMethodProvider] is not [UtilClassFileMethodProvider], - * because it means that util methods will be taken from some other provider (e.g. [TestClassUtilMethodProvider]). - */ - internal fun fromCgContextOrNull(context: CgContext): UtilClassKind? { - if (context.requiredUtilMethods.isEmpty()) return null - if (!context.mockFrameworkUsed) { - return RegularUtUtils - } - return when (context.mockFramework) { - MockFramework.MOCKITO -> UtUtilsWithMockito - // in case we will add any other mock frameworks, newer Kotlin compiler versions - // will report a non-exhaustive 'when', so we will not forget to support them here as well - } - } - - const val UT_UTILS_PACKAGE_NAME = "org.utbot.runtime.utils" - const val UT_UTILS_CLASS_NAME = "UtUtils" - const val PACKAGE_DELIMITER = "." - - /** - * List of package names of UtUtils class. - * See whole package name at [UT_UTILS_PACKAGE_NAME]. - */ - val utilsPackages: List - get() = UT_UTILS_PACKAGE_NAME.split(PACKAGE_DELIMITER) - } -} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt deleted file mode 100644 index d29a0e8036..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt +++ /dev/null @@ -1,94 +0,0 @@ -package org.utbot.framework.codegen.model.constructor - -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.UtClusterInfo -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.UtMethodTestSet -import org.utbot.framework.plugin.api.util.objectClassId -import org.utbot.framework.plugin.api.util.voidClassId -import soot.jimple.JimpleBody - -data class CgMethodTestSet private constructor( - val executableId: ExecutableId, - val jimpleBody: JimpleBody? = null, - val errors: Map = emptyMap(), - val clustersInfo: List>, -) { - var executions: List = emptyList() - private set - - constructor(from: UtMethodTestSet) : this( - from.method, - from.jimpleBody, - from.errors, - from.clustersInfo - ) { - executions = from.executions - } - - /** - * Splits [CgMethodTestSet] into separate test sets having - * unique result model [ClassId] in each subset. - */ - fun splitExecutionsByResult() : List { - val successfulExecutions = executions.filter { it.result is UtExecutionSuccess } - val failureExecutions = executions.filter { it.result is UtExecutionFailure } - - val executionsByResult: MutableMap> = - successfulExecutions - .groupBy { (it.result as UtExecutionSuccess).model.classId }.toMutableMap() - - // if we have failure executions, we add them to the first successful executions group - val groupClassId = executionsByResult.keys.firstOrNull() - if (groupClassId != null) { - executionsByResult[groupClassId] = executionsByResult[groupClassId]!! + failureExecutions - } else { - executionsByResult[objectClassId] = failureExecutions - } - - return executionsByResult.map{ (_, executions) -> substituteExecutions(executions) } - } - - /** - * Splits [CgMethodTestSet] test sets by affected static fields statics. - * - * A separate test set is created for each combination of modified statics. - */ - fun splitExecutionsByChangedStatics(): List { - val executionsByStaticsUsage: Map, List> = - executions.groupBy { it.stateBefore.statics.keys } - - return executionsByStaticsUsage.map { (_, executions) -> substituteExecutions(executions) } - } - - /** - * Finds a [ClassId] of all result models in executions. - * - * Tries to find a unique result type in testSets or - * gets executable return type. - */ - fun resultType(): ClassId { - return when (executableId.returnType) { - voidClassId -> executableId.returnType - else -> { - val successfulExecutions = executions.filter { it.result is UtExecutionSuccess } - if (successfulExecutions.isNotEmpty()) { - successfulExecutions - .map { (it.result as UtExecutionSuccess).model.classId } - .distinct() - .singleOrNull() - ?: executableId.returnType - } else { - executableId.returnType - } - } - } - } - - private fun substituteExecutions(newExecutions: List): CgMethodTestSet = - copy().apply { executions = newExecutions } -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt deleted file mode 100644 index 4d6f764483..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.utbot.framework.codegen.model.constructor - -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner -import org.utbot.framework.codegen.model.tree.CgAnnotation -import org.utbot.framework.codegen.model.tree.CgMethod -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.codegen.model.tree.CgTestClass - -/** - * This class stores context information needed to build [CgTestClass]. - * Should only be used in [CgContextOwner]. - */ -internal data class TestClassContext( - // set of interfaces that the test class must inherit - val collectedTestClassInterfaces: MutableSet = mutableSetOf(), - - // set of annotations of the test class - val collectedTestClassAnnotations: MutableSet = mutableSetOf(), - - // list of data provider methods that test class must implement - val cgDataProviderMethods: MutableList = mutableListOf(), -) { - // test class superclass (if needed) - var testClassSuperclass: ClassId? = null - set(value) { - // Assigning a value to the testClassSuperclass when it is already non-null - // means that we need the test class to have more than one superclass - // which is impossible in Java and Kotlin. - require(value == null || field == null) { "It is impossible for the test class to have more than one superclass" } - field = value - } - - fun clear() { - collectedTestClassAnnotations.clear() - collectedTestClassInterfaces.clear() - cgDataProviderMethods.clear() - testClassSuperclass = null - } -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassModel.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassModel.kt deleted file mode 100644 index 1e261b917b..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.utbot.framework.codegen.model.constructor - -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.util.enclosingClass - -// TODO: seems like this class needs to be renamed -/** - * Stores method testsets in a structure that replicates structure of their methods in [classUnderTest]. - * I.e., if some method is declared in nested class of [classUnderTest], its testset will be put - * in [TestClassModel] in one of [nestedClasses] - */ -data class TestClassModel( - val classUnderTest: ClassId, - val methodTestSets: List, - val nestedClasses: List = listOf() -) { - companion object { - fun fromTestSets(classUnderTest: ClassId, testSets: List): TestClassModel { - // For each class stores list of methods declared in this class (methods from nested classes are excluded) - val class2methodTestSets = testSets.groupBy { it.executableId.classId } - - val classesWithMethodsUnderTest = testSets - .map { it.executableId.classId } - .distinct() - - // For each class stores list of its "direct" nested classes - val class2nestedClasses = mutableMapOf>() - - for (classId in classesWithMethodsUnderTest) { - var currentClass = classId - var enclosingClass = currentClass.enclosingClass - // while we haven't reached the top of nested class hierarchy or the main class under test - while (enclosingClass != null && currentClass != classUnderTest) { - class2nestedClasses.getOrPut(enclosingClass) { mutableSetOf() } += currentClass - currentClass = enclosingClass - enclosingClass = enclosingClass.enclosingClass - } - } - return constructRecursively(classUnderTest, class2methodTestSets, class2nestedClasses) - } - - private fun constructRecursively( - clazz: ClassId, - class2methodTestSets: Map>, - class2nestedClasses: Map> - ): TestClassModel { - val currentNestedClasses = class2nestedClasses.getOrDefault(clazz, listOf()) - val currentMethodTestSets = class2methodTestSets.getOrDefault(clazz, listOf()) - return TestClassModel( - clazz, - currentMethodTestSets, - currentNestedClasses.map { - constructRecursively(it, class2methodTestSets, class2nestedClasses) - } - ) - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 87ee5c48f5..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt +++ /dev/null @@ -1,312 +0,0 @@ -package org.utbot.framework.codegen.model.constructor.builtin - -import org.utbot.framework.codegen.MockitoStaticMocking -import org.utbot.framework.codegen.model.constructor.util.arrayTypeOf -import org.utbot.framework.codegen.model.constructor.util.utilMethodId -import org.utbot.framework.codegen.model.tree.CgClassId -import org.utbot.framework.codegen.model.visitor.utilMethodTextById -import org.utbot.framework.plugin.api.BuiltinClassId -import org.utbot.framework.plugin.api.BuiltinConstructorId -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.builtinConstructorId -import org.utbot.framework.plugin.api.util.classClassId -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.intClassId -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.objectArrayClassId -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.invoke.MethodHandles -import java.lang.invoke.MethodType -import java.lang.reflect.Method - -/** - * Set of ids of all possible util methods for a given class. - * - * The class may actually not have some of these methods if they - * are not required in the process of code generation (this is the case for [TestClassUtilMethodProvider]). - */ -internal abstract class UtilMethodProvider(val utilClassId: ClassId) { - val utilMethodIds: Set - get() = setOf( - getUnsafeInstanceMethodId, - createInstanceMethodId, - createArrayMethodId, - setFieldMethodId, - setStaticFieldMethodId, - getFieldValueMethodId, - getStaticFieldValueMethodId, - getEnumConstantByNameMethodId, - deepEqualsMethodId, - arraysDeepEqualsMethodId, - iterablesDeepEqualsMethodId, - streamsDeepEqualsMethodId, - mapsDeepEqualsMethodId, - hasCustomEqualsMethodId, - getArrayLengthMethodId, - buildStaticLambdaMethodId, - buildLambdaMethodId, - getLookupInMethodId, - getLambdaCapturedArgumentTypesMethodId, - getLambdaCapturedArgumentValuesMethodId, - getInstantiatedMethodTypeMethodId, - getLambdaMethodMethodId, - getSingleAbstractMethodMethodId - ) - - val getUnsafeInstanceMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getUnsafeInstance", - returnType = Unsafe::class.id, - ) - - /** - * Method that creates instance using Unsafe - */ - val createInstanceMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "createInstance", - returnType = CgClassId(objectClassId, isNullable = true), - arguments = arrayOf(stringClassId) - ) - - val createArrayMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "createArray", - returnType = Array::class.id, - arguments = arrayOf(stringClassId, intClassId, Array::class.id) - ) - - val setFieldMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "setField", - returnType = voidClassId, - arguments = arrayOf(objectClassId, stringClassId, stringClassId, objectClassId) - ) - - val setStaticFieldMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "setStaticField", - returnType = voidClassId, - arguments = arrayOf(Class::class.id, stringClassId, objectClassId) - ) - - val getFieldValueMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getFieldValue", - returnType = objectClassId, - arguments = arrayOf(objectClassId, stringClassId, stringClassId) - ) - - val getStaticFieldValueMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getStaticFieldValue", - returnType = objectClassId, - arguments = arrayOf(Class::class.id, stringClassId) - ) - - val getEnumConstantByNameMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getEnumConstantByName", - returnType = objectClassId, - arguments = arrayOf(Class::class.id, stringClassId) - ) - - val deepEqualsMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "deepEquals", - returnType = booleanClassId, - arguments = arrayOf(objectClassId, objectClassId) - ) - - val arraysDeepEqualsMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "arraysDeepEquals", - returnType = booleanClassId, - arguments = arrayOf(objectClassId, objectClassId) - ) - - val iterablesDeepEqualsMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "iterablesDeepEquals", - returnType = booleanClassId, - arguments = arrayOf(java.lang.Iterable::class.id, java.lang.Iterable::class.id) - ) - - val streamsDeepEqualsMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "streamsDeepEquals", - returnType = booleanClassId, - arguments = arrayOf(java.util.stream.BaseStream::class.id, java.util.stream.BaseStream::class.id) - ) - - val mapsDeepEqualsMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "mapsDeepEquals", - returnType = booleanClassId, - arguments = arrayOf(java.util.Map::class.id, java.util.Map::class.id) - ) - - val hasCustomEqualsMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "hasCustomEquals", - returnType = booleanClassId, - arguments = arrayOf(Class::class.id) - ) - - val getArrayLengthMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getArrayLength", - returnType = intClassId, - arguments = arrayOf(objectClassId) - ) - - val buildStaticLambdaMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "buildStaticLambda", - returnType = objectClassId, - arguments = arrayOf( - classClassId, - classClassId, - stringClassId, - arrayTypeOf(capturedArgumentClassId) - ) - ) - - val buildLambdaMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "buildLambda", - returnType = objectClassId, - arguments = arrayOf( - classClassId, - classClassId, - stringClassId, - objectClassId, - arrayTypeOf(capturedArgumentClassId) - ) - ) - - val getLookupInMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getLookupIn", - returnType = MethodHandles.Lookup::class.id, - arguments = arrayOf(classClassId) - ) - - val getLambdaCapturedArgumentTypesMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getLambdaCapturedArgumentTypes", - returnType = arrayTypeOf(classClassId), - arguments = arrayOf(arrayTypeOf(capturedArgumentClassId)) - ) - - val getLambdaCapturedArgumentValuesMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getLambdaCapturedArgumentValues", - returnType = objectArrayClassId, - arguments = arrayOf(arrayTypeOf(capturedArgumentClassId)) - ) - - val getInstantiatedMethodTypeMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getInstantiatedMethodType", - returnType = MethodType::class.id, - arguments = arrayOf(Method::class.id, arrayTypeOf(classClassId)) - ) - - val getLambdaMethodMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getLambdaMethod", - returnType = Method::class.id, - arguments = arrayOf(classClassId, stringClassId) - ) - - val getSingleAbstractMethodMethodId: MethodId - get() = utilClassId.utilMethodId( - name = "getSingleAbstractMethod", - returnType = java.lang.reflect.Method::class.id, - arguments = arrayOf(classClassId) - ) - - val capturedArgumentClassId: BuiltinClassId - get() = BuiltinClassId( - name = "${utilClassId.name}\$CapturedArgument", - canonicalName = "${utilClassId.name}.CapturedArgument", - simpleName = "CapturedArgument" - ) - - val capturedArgumentConstructorId: BuiltinConstructorId - get() = builtinConstructorId(capturedArgumentClassId, classClassId, objectClassId) -} - -/** - * This provider represents an util class file that is generated and put into the user's test module. - * The generated class is UtUtils (its id is defined at [utUtilsClassId]). - * - * Content of this util class may be different (due to mocks in deepEquals), but the methods (and their ids) are the same. - */ -internal object UtilClassFileMethodProvider : UtilMethodProvider(utUtilsClassId) { - /** - * This property contains the current version of util class. - * This version will be written to the util class file inside a comment. - * - * Whenever we want to create an util class, we first check if there is an already existing one. - * If there is, then we decide whether we need to overwrite it or not. One of the factors here - * is the version of this existing class. If the version of existing class is older than the one - * that is currently stored in [UtilClassFileMethodProvider.UTIL_CLASS_VERSION], then we need to - * overwrite an util class, because it might have been changed in the new version. - * - * **IMPORTANT** if you make any changes to util methods (see [utilMethodTextById]), do not forget to update this version. - */ - const val UTIL_CLASS_VERSION = "1.0" -} - -internal class TestClassUtilMethodProvider(testClassId: ClassId) : UtilMethodProvider(testClassId) - -internal val utUtilsClassId: ClassId - get() = BuiltinClassId( - name = "org.utbot.runtime.utils.UtUtils", - canonicalName = "org.utbot.runtime.utils.UtUtils", - simpleName = "UtUtils", - isFinal = true - ) - -/** - * [MethodId] for [AutoCloseable.close]. - */ -val closeMethodId = MethodId( - classId = AutoCloseable::class.java.id, - name = "close", - returnType = voidClassId, - parameters = emptyList() -) - -val mocksAutoCloseable: Set = setOf( - MockitoStaticMocking.mockedStaticClassId, - MockitoStaticMocking.mockedConstructionClassId -) - -val predefinedAutoCloseable: Set = mocksAutoCloseable - -/** - * Checks if this class is marked as auto closeable - * (useful for classes that could not be loaded by class loader like mocks for mocking statics from Mockito Inline). - */ -internal val ClassId.isPredefinedAutoCloseable: Boolean - get() = this in predefinedAutoCloseable - -/** - * Returns [AutoCloseable.close] method id for all auto closeable. - * and predefined as auto closeable via [isPredefinedAutoCloseable], and null otherwise. - * Null always for [BuiltinClassId]. - */ -internal val ClassId.closeMethodIdOrNull: MethodId? - get() = when { - isPredefinedAutoCloseable -> closeMethodId - this is BuiltinClassId -> null - else -> (jClass as? AutoCloseable)?.let { closeMethodId } - } 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 deleted file mode 100644 index 308bab95bc..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt +++ /dev/null @@ -1,580 +0,0 @@ -package org.utbot.framework.codegen.model.constructor.context - -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.HangingTestsTimeout -import org.utbot.framework.codegen.Import -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.model.constructor.tree.Block -import org.utbot.framework.codegen.model.constructor.util.EnvironmentFieldStateCache -import org.utbot.framework.codegen.model.constructor.util.importIfNeeded -import org.utbot.framework.codegen.model.tree.CgAnnotation -import org.utbot.framework.codegen.model.tree.CgExecutableCall -import org.utbot.framework.codegen.model.tree.CgStatement -import org.utbot.framework.codegen.model.tree.CgStatementExecutableCall -import org.utbot.framework.codegen.model.tree.CgTestMethod -import org.utbot.framework.codegen.model.tree.CgThisInstance -import org.utbot.framework.codegen.model.tree.CgValue -import org.utbot.framework.codegen.model.tree.CgVariable -import java.util.IdentityHashMap -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.PersistentMap -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.CgMethodTestSet -import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider -import org.utbot.framework.codegen.model.constructor.builtin.UtilClassFileMethodProvider -import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider -import org.utbot.framework.codegen.model.constructor.TestClassContext -import org.utbot.framework.codegen.model.constructor.TestClassModel -import org.utbot.framework.codegen.model.tree.CgParameterKind -import org.utbot.framework.plugin.api.BuiltinClassId -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.MethodId -import org.utbot.framework.plugin.api.MockFramework -import org.utbot.framework.plugin.api.UtExecution -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtReferenceModel -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.isCheckedException -import org.utbot.framework.plugin.api.util.isSubtypeOf -import org.utbot.framework.plugin.api.util.jClass - -/** - * Interface for all code generation context aware entities - * - * Most of the properties are 'val'. - * Although, some of the properties are declared as 'var' so that - * they can be reassigned as well as modified - * - * For example, [outerMostTestClass] and [currentExecutable] can be reassigned - * when we start generating another method or test class - * - * [existingVariableNames] is a 'var' property - * that can be reverted to its previous value on exit from a name scope - * - * @see [CgContextOwner.withNameScope] - */ -internal interface CgContextOwner { - // current class under test - val classUnderTest: ClassId - - // test class currently being generated (if series of nested classes is generated, it is the outermost one) - val outerMostTestClass: ClassId - - // test class currently being generated (if series of nested classes is generated, it is the innermost one) - var currentTestClass: ClassId - - // provider of util methods used for the test class currently being generated - val utilMethodProvider: UtilMethodProvider - - // current executable under test - var currentExecutable: ExecutableId? - - // ClassInfo for the outermost class currently being generated - val outerMostTestClassContext: TestClassContext - - // If generating series of nested classes, it is ClassInfo for the innermost one, - // otherwise it should be equal to outerMostTestClassInfo - val currentTestClassContext: TestClassContext - - // exceptions that can be thrown inside of current method being built - val collectedExceptions: MutableSet - - // annotations required by the current method being built - val collectedMethodAnnotations: MutableSet - - // imports required by the test class being built - val collectedImports: MutableSet - - val importedStaticMethods: MutableSet - val importedClasses: MutableSet - - // util methods required by the test class being built - val requiredUtilMethods: MutableSet - - val utilMethodsUsed: Boolean - - // test methods being generated - val testMethods: MutableList - - // names of methods that already exist in the test class - val existingMethodNames: MutableSet - - // At the start of a test method we save the initial state of some static fields in variables. - // This map is used to restore the initial values of these variables at the end of the test method. - val prevStaticFieldValues: MutableMap - - // names of parameters of methods under test - val paramNames: Map> - - // UtExecution we currently generate a test method for. - // It is null when no test method is being generated at the moment. - var currentExecution: UtExecution? - - val testFramework: TestFramework - - val mockFramework: MockFramework - - val staticsMocking: StaticsMocking - - val forceStaticMocking: ForceStaticMocking - - val generateWarningsForStaticMocking: Boolean - - val codegenLanguage: CodegenLanguage - - val parametrizedTestSource: ParametrizedTestSource - - /** - * Flag indicating whether a mock framework is used in the generated code - * NOTE! This flag is not about whether a mock framework is present - * in the user's project dependencies or not. - * This flag is true if the generated test class contains at least one mock object, - * and false otherwise. See method [withMockFramework]. - */ - var mockFrameworkUsed: Boolean - - // object that represents a set of information about JUnit of selected version - - // Persistent collections are used to conveniently restore their previous state. - // For example, when we exit a block of code we return to the previous name scope. - // At that moment we revert some collections (e.g. variable names) to the previous state. - - // current block of code being built - var currentBlock: PersistentList - - // variable names being used in the current name scope - var existingVariableNames: PersistentSet - - // variables of java.lang.Class type declared in the current name scope - var declaredClassRefs: PersistentMap - - // Variables of either java.lang.reflect.Constructor or java.lang.reflect.Method types - // declared in the current name scope. - // java.lang.reflect.Executable is a superclass of both of these types. - var declaredExecutableRefs: PersistentMap - - // Variables of java.lang.reflect.Field type declared in the current name scope - var declaredFieldRefs: PersistentMap - - // generated this instance for method under test - var thisInstance: CgValue? - - // generated arguments for method under test - val methodArguments: MutableList - - // a variable representing an actual result of the method under test call - var actual: CgVariable - - // a variable representing if test method contains reflective call or not - // and should we catch exceptions like InvocationTargetException or not so on - var containsReflectiveCall: Boolean - - // map from a set of tests for a method to another map - // which connects code generation error message - // with the number of times it occurred - val codeGenerationErrors: MutableMap> - - // package for generated test class - val testClassPackageName: String - - val shouldOptimizeImports: Boolean - - var valueByModel: IdentityHashMap - - // use it to compare stateBefore and result variables - in case of equality do not create new variable - var valueByModelId: MutableMap - - // parameters of the method currently being generated - val currentMethodParameters: MutableMap - - val testClassCustomName: String? - - /** - * Determines whether tests that throw Runtime exceptions should fail or pass. - */ - val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour - - /** - * Timeout for possibly hanging tests (uses info from concrete executor). - */ - val hangingTestsTimeout: HangingTestsTimeout - - /** - * Determines whether tests with timeout should fail (with added Timeout annotation) or be disabled (for our pipeline). - */ - val enableTestsTimeout: Boolean - - var statesCache: EnvironmentFieldStateCache - - /** - * Result models required to create generic execution in parametrized tests. - */ - var successfulExecutionsModels: List - - fun block(init: () -> Unit): Block { - val prevBlock = currentBlock - return try { - val block = persistentListOf() - currentBlock = block - withNameScope { - init() - currentBlock - } - } finally { - currentBlock = prevBlock - } - } - - operator fun CgStatement.unaryPlus() { - currentBlock = currentBlock.add(this) - } - - operator fun CgExecutableCall.unaryPlus(): CgStatementExecutableCall = - CgStatementExecutableCall(this).also { - currentBlock = currentBlock.add(it) - } - - fun updateCurrentExecutable(executableId: ExecutableId) { - currentExecutable = executableId - } - - fun addExceptionIfNeeded(exception: ClassId) { - if (exception !is BuiltinClassId) { - require(exception isSubtypeOf Throwable::class.id) { - "Class $exception which is not a Throwable was passed" - } - - val isUnchecked = !exception.jClass.isCheckedException - val alreadyAdded = - collectedExceptions.any { existingException -> exception isSubtypeOf existingException } - - if (isUnchecked || alreadyAdded) return - - collectedExceptions - .removeIf { existingException -> existingException isSubtypeOf exception } - } - - if (collectedExceptions.add(exception)) { - importIfNeeded(exception) - } - } - - fun addAnnotation(annotation: CgAnnotation) { - if (collectedMethodAnnotations.add(annotation)) { - importIfNeeded(annotation.classId) // TODO: check how JUnit annotations are loaded - } - } - - /** - * This method sets up context for a new test class file generation and executes the given [block]. - * Afterwards, context is set back to the initial state. - */ - fun withTestClassFileScope(block: () -> R): R - - /** - * This method sets up context for a new test class generation and executes the given [block]. - * Afterwards, context is set back to the initial state. - */ - fun withTestClassScope(block: () -> R): R - - /** - * This method does almost all the same as [withTestClassScope], but for nested test classes. - * The difference is that instead of working with [outerMostTestClassContext] it works with [currentTestClassContext]. - */ - fun withNestedClassScope(testClassModel: TestClassModel, block: () -> R): R - - /** - * Set [mockFrameworkUsed] flag to true if the block is successfully executed - */ - fun withMockFramework(block: () -> R): R { - val result = block() - mockFrameworkUsed = true - return result - } - - fun updateVariableScope(variable: CgVariable, model: UtModel? = null) { - model?.let { - valueByModel[it] = variable - (model as UtReferenceModel).let { refModel -> - refModel.id.let { id -> valueByModelId[id] = variable } - } - } - } - - fun withNameScope(block: () -> R): R { - val prevVariableNames = existingVariableNames - val prevDeclaredClassRefs = declaredClassRefs - val prevDeclaredExecutableRefs = declaredExecutableRefs - val prevDeclaredFieldRefs = declaredFieldRefs - val prevValueByModel = IdentityHashMap(valueByModel) - val prevValueByModelId = valueByModelId.toMutableMap() - return try { - block() - } finally { - existingVariableNames = prevVariableNames - declaredClassRefs = prevDeclaredClassRefs - declaredExecutableRefs = prevDeclaredExecutableRefs - declaredFieldRefs = prevDeclaredFieldRefs - valueByModel = prevValueByModel - valueByModelId = prevValueByModelId - } - } - - /** - * [ClassId] of a class that contains util methods. - * For example, it can be the current test class, or it can be a generated separate `UtUtils` class. - */ - val utilsClassId: ClassId - get() = utilMethodProvider.utilClassId - - /** - * Checks is it our util reflection field getter method. - * When this method is used with type cast in Kotlin, this type cast have to be safety - */ - val MethodId.isGetFieldUtilMethod: Boolean - get() = this == utilMethodProvider.getFieldValueMethodId - || this == utilMethodProvider.getStaticFieldValueMethodId - - val testClassThisInstance: CgThisInstance - - // util methods and auxiliary classes of current test class - - val capturedArgumentClass: ClassId - get() = utilMethodProvider.capturedArgumentClassId - - val getUnsafeInstance: MethodId - get() = utilMethodProvider.getUnsafeInstanceMethodId - - val createInstance: MethodId - get() = utilMethodProvider.createInstanceMethodId - - val createArray: MethodId - get() = utilMethodProvider.createArrayMethodId - - val setField: MethodId - get() = utilMethodProvider.setFieldMethodId - - val setStaticField: MethodId - get() = utilMethodProvider.setStaticFieldMethodId - - val getFieldValue: MethodId - get() = utilMethodProvider.getFieldValueMethodId - - val getStaticFieldValue: MethodId - get() = utilMethodProvider.getStaticFieldValueMethodId - - val getEnumConstantByName: MethodId - get() = utilMethodProvider.getEnumConstantByNameMethodId - - val deepEquals: MethodId - get() = utilMethodProvider.deepEqualsMethodId - - val arraysDeepEquals: MethodId - get() = utilMethodProvider.arraysDeepEqualsMethodId - - val iterablesDeepEquals: MethodId - get() = utilMethodProvider.iterablesDeepEqualsMethodId - - val streamsDeepEquals: MethodId - get() = utilMethodProvider.streamsDeepEqualsMethodId - - val mapsDeepEquals: MethodId - get() = utilMethodProvider.mapsDeepEqualsMethodId - - val hasCustomEquals: MethodId - get() = utilMethodProvider.hasCustomEqualsMethodId - - val getArrayLength: MethodId - get() = utilMethodProvider.getArrayLengthMethodId - - val buildStaticLambda: MethodId - get() = utilMethodProvider.buildStaticLambdaMethodId - - val buildLambda: MethodId - get() = utilMethodProvider.buildLambdaMethodId - - val getLookupIn: MethodId - get() = utilMethodProvider.getLookupInMethodId - - val getSingleAbstractMethod: MethodId - get() = utilMethodProvider.getSingleAbstractMethodMethodId - - val getLambdaCapturedArgumentTypes: MethodId - get() = utilMethodProvider.getLambdaCapturedArgumentTypesMethodId - - val getLambdaCapturedArgumentValues: MethodId - get() = utilMethodProvider.getLambdaCapturedArgumentValuesMethodId - - val getInstantiatedMethodType: MethodId - get() = utilMethodProvider.getInstantiatedMethodTypeMethodId - - val getLambdaMethod: MethodId - get() = utilMethodProvider.getLambdaMethodMethodId -} - -/** - * Context with current code generation info - */ -internal data class CgContext( - override val classUnderTest: ClassId, - val generateUtilClassFile: Boolean, - override var currentExecutable: ExecutableId? = null, - override val collectedExceptions: MutableSet = mutableSetOf(), - override val collectedMethodAnnotations: MutableSet = mutableSetOf(), - override val collectedImports: MutableSet = mutableSetOf(), - override val importedStaticMethods: MutableSet = mutableSetOf(), - override val importedClasses: MutableSet = mutableSetOf(), - override val requiredUtilMethods: MutableSet = mutableSetOf(), - override val testMethods: MutableList = mutableListOf(), - override val existingMethodNames: MutableSet = mutableSetOf(), - override val prevStaticFieldValues: MutableMap = mutableMapOf(), - override val paramNames: Map>, - override var currentExecution: UtExecution? = null, - override val testFramework: TestFramework, - override val mockFramework: MockFramework, - override val staticsMocking: StaticsMocking, - override val forceStaticMocking: ForceStaticMocking, - override val generateWarningsForStaticMocking: Boolean, - override val codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem, - override val parametrizedTestSource: ParametrizedTestSource = ParametrizedTestSource.DO_NOT_PARAMETRIZE, - override var mockFrameworkUsed: Boolean = false, - override var currentBlock: PersistentList = persistentListOf(), - override var existingVariableNames: PersistentSet = persistentSetOf(), - override var declaredClassRefs: PersistentMap = persistentMapOf(), - override var declaredExecutableRefs: PersistentMap = persistentMapOf(), - override var declaredFieldRefs: PersistentMap = persistentMapOf(), - override var thisInstance: CgValue? = null, - override val methodArguments: MutableList = mutableListOf(), - override val codeGenerationErrors: MutableMap> = mutableMapOf(), - override val testClassPackageName: String = classUnderTest.packageName, - override var shouldOptimizeImports: Boolean = false, - override var testClassCustomName: String? = null, - override val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = - RuntimeExceptionTestsBehaviour.defaultItem, - override val hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), - override val enableTestsTimeout: Boolean = true, - override var containsReflectiveCall: Boolean = false, -) : CgContextOwner { - override lateinit var statesCache: EnvironmentFieldStateCache - override lateinit var actual: CgVariable - override lateinit var successfulExecutionsModels: List - - /** - * This property cannot be accessed outside of test class file scope - * (i.e. outside of [CgContextOwner.withTestClassFileScope]). - */ - override val outerMostTestClassContext: TestClassContext - get() = _outerMostTestClassContext ?: error("Accessing outerMostTestClassInfo out of class file scope") - - private var _outerMostTestClassContext: TestClassContext? = null - - /** - * This property cannot be accessed outside of test class scope - * (i.e. outside of [CgContextOwner.withTestClassScope]). - */ - override val currentTestClassContext: TestClassContext - get() = _currentTestClassContext ?: error("Accessing currentTestClassInfo out of class scope") - - private var _currentTestClassContext: TestClassContext? = null - - override val outerMostTestClass: ClassId by lazy { - val packagePrefix = if (testClassPackageName.isNotEmpty()) "$testClassPackageName." else "" - val simpleName = testClassCustomName ?: "${classUnderTest.simpleName}Test" - val name = "$packagePrefix$simpleName" - BuiltinClassId( - name = name, - canonicalName = name, - simpleName = simpleName - ) - } - - /** - * Determine where the util methods will come from. - * If we don't want to use a separately generated util class, - * util methods will be generated directly in the test class (see [TestClassUtilMethodProvider]). - * Otherwise, an util class will be generated separately and we will use util methods from it (see [UtilClassFileMethodProvider]). - */ - override val utilMethodProvider: UtilMethodProvider - get() = if (generateUtilClassFile) { - UtilClassFileMethodProvider - } else { - TestClassUtilMethodProvider(outerMostTestClass) - } - - override lateinit var currentTestClass: ClassId - - override fun withTestClassFileScope(block: () -> R): R { - clearClassScope() - _outerMostTestClassContext = TestClassContext() - return try { - block() - } finally { - clearClassScope() - } - } - - override fun withTestClassScope(block: () -> R): R { - _currentTestClassContext = outerMostTestClassContext - currentTestClass = outerMostTestClass - return try { - block() - } finally { - _currentTestClassContext = null - } - } - - override fun withNestedClassScope(testClassModel: TestClassModel, block: () -> R): R { - val previousCurrentTestClassInfo = currentTestClassContext - val previousCurrentTestClass = currentTestClass - currentTestClass = createClassIdForNestedClass(testClassModel) - _currentTestClassContext = TestClassContext() - return try { - block() - } finally { - _currentTestClassContext = previousCurrentTestClassInfo - currentTestClass = previousCurrentTestClass - } - } - - private fun createClassIdForNestedClass(testClassModel: TestClassModel): ClassId { - val simpleName = "${testClassModel.classUnderTest.simpleName}Test" - return BuiltinClassId( - name = currentTestClass.name + "$" + simpleName, - canonicalName = currentTestClass.canonicalName + "." + simpleName, - simpleName = simpleName - ) - } - - private fun clearClassScope() { - _outerMostTestClassContext = null - collectedImports.clear() - importedStaticMethods.clear() - importedClasses.clear() - testMethods.clear() - requiredUtilMethods.clear() - valueByModel.clear() - valueByModelId.clear() - mockFrameworkUsed = false - } - - - override var valueByModel: IdentityHashMap = IdentityHashMap() - - override var valueByModelId: MutableMap = mutableMapOf() - - override val currentMethodParameters: MutableMap = mutableMapOf() - - override val testClassThisInstance: CgThisInstance = CgThisInstance(outerMostTestClass) - - override val utilMethodsUsed: Boolean - get() = requiredUtilMethods.isNotEmpty() -} \ No newline at end of file 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 deleted file mode 100644 index 8bd36ad50f..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt +++ /dev/null @@ -1,1813 +0,0 @@ -package org.utbot.framework.codegen.model.constructor.tree - -import org.utbot.common.PathUtil -import org.utbot.common.isStatic -import org.utbot.framework.assemble.assemble -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour.PASS -import org.utbot.framework.codegen.model.constructor.CgMethodTestSet -import org.utbot.framework.codegen.model.constructor.builtin.closeMethodIdOrNull -import org.utbot.framework.codegen.model.constructor.builtin.forName -import org.utbot.framework.codegen.model.constructor.builtin.getClass -import org.utbot.framework.codegen.model.constructor.builtin.getTargetException -import org.utbot.framework.codegen.model.constructor.builtin.invoke -import org.utbot.framework.codegen.model.constructor.builtin.newInstance -import org.utbot.framework.codegen.model.constructor.context.CgContext -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getCallableAccessManagerBy -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getMockFrameworkManagerBy -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getNameGeneratorBy -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getStatementConstructorBy -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getTestFrameworkManagerBy -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getVariableConstructorBy -import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor -import org.utbot.framework.codegen.model.constructor.util.EnvironmentFieldStateCache -import org.utbot.framework.codegen.model.constructor.util.FieldStateCache -import org.utbot.framework.codegen.model.constructor.util.classCgClassId -import org.utbot.framework.codegen.model.constructor.util.needExpectedDeclaration -import org.utbot.framework.codegen.model.constructor.util.overridesEquals -import org.utbot.framework.codegen.model.constructor.util.plus -import org.utbot.framework.codegen.model.constructor.util.setArgumentsArrayElement -import org.utbot.framework.codegen.model.constructor.util.typeCast -import org.utbot.framework.codegen.model.tree.CgAllocateArray -import org.utbot.framework.codegen.model.tree.CgArrayElementAccess -import org.utbot.framework.codegen.model.tree.CgClassId -import org.utbot.framework.codegen.model.tree.CgDeclaration -import org.utbot.framework.codegen.model.tree.CgDocPreTagStatement -import org.utbot.framework.codegen.model.tree.CgDocRegularStmt -import org.utbot.framework.codegen.model.tree.CgDocumentationComment -import org.utbot.framework.codegen.model.tree.CgEqualTo -import org.utbot.framework.codegen.model.tree.CgErrorTestMethod -import org.utbot.framework.codegen.model.tree.CgExecutableCall -import org.utbot.framework.codegen.model.tree.CgExpression -import org.utbot.framework.codegen.model.tree.CgFieldAccess -import org.utbot.framework.codegen.model.tree.CgGetJavaClass -import org.utbot.framework.codegen.model.tree.CgLiteral -import org.utbot.framework.codegen.model.tree.CgMethod -import org.utbot.framework.codegen.model.tree.CgMethodCall -import org.utbot.framework.codegen.model.tree.CgMultilineComment -import org.utbot.framework.codegen.model.tree.CgNotNullAssertion -import org.utbot.framework.codegen.model.tree.CgParameterDeclaration -import org.utbot.framework.codegen.model.tree.CgParameterKind -import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod -import org.utbot.framework.codegen.model.tree.CgRegion -import org.utbot.framework.codegen.model.tree.CgSimpleRegion -import org.utbot.framework.codegen.model.tree.CgSingleLineComment -import org.utbot.framework.codegen.model.tree.CgStatement -import org.utbot.framework.codegen.model.tree.CgStaticFieldAccess -import org.utbot.framework.codegen.model.tree.CgTestMethod -import org.utbot.framework.codegen.model.tree.CgTestMethodType -import org.utbot.framework.codegen.model.tree.CgTestMethodType.CRASH -import org.utbot.framework.codegen.model.tree.CgTestMethodType.FAILING -import org.utbot.framework.codegen.model.tree.CgTestMethodType.PARAMETRIZED -import org.utbot.framework.codegen.model.tree.CgTestMethodType.SUCCESSFUL -import org.utbot.framework.codegen.model.tree.CgTestMethodType.TIMEOUT -import org.utbot.framework.codegen.model.tree.CgTryCatch -import org.utbot.framework.codegen.model.tree.CgTypeCast -import org.utbot.framework.codegen.model.tree.CgValue -import org.utbot.framework.codegen.model.tree.CgVariable -import org.utbot.framework.codegen.model.tree.buildParameterizedTestDataProviderMethod -import org.utbot.framework.codegen.model.tree.buildTestMethod -import org.utbot.framework.codegen.model.tree.convertDocToCg -import org.utbot.framework.codegen.model.tree.toStatement -import org.utbot.framework.codegen.model.util.canBeSetFrom -import org.utbot.framework.codegen.model.util.equalTo -import org.utbot.framework.codegen.model.util.inc -import org.utbot.framework.codegen.model.util.canBeReadFrom -import org.utbot.framework.codegen.model.util.length -import org.utbot.framework.codegen.model.util.lessThan -import org.utbot.framework.codegen.model.util.nullLiteral -import org.utbot.framework.codegen.model.util.resolve -import org.utbot.framework.fields.ExecutionStateAnalyzer -import org.utbot.framework.fields.FieldPath -import org.utbot.framework.plugin.api.BuiltinClassId -import org.utbot.framework.plugin.api.BuiltinMethodId -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.ConcreteExecutionFailureException -import org.utbot.framework.plugin.api.ConstructorId -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.MethodId -import org.utbot.framework.plugin.api.TimeoutException -import org.utbot.framework.plugin.api.TypeParameters -import org.utbot.framework.plugin.api.UtArrayModel -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtClassRefModel -import org.utbot.framework.plugin.api.UtCompositeModel -import org.utbot.framework.plugin.api.UtConcreteExecutionFailure -import org.utbot.framework.plugin.api.UtDirectSetFieldModel -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.UtLambdaModel -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation -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.UtSandboxFailure -import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation -import org.utbot.framework.plugin.api.UtSymbolicExecution -import org.utbot.framework.plugin.api.UtTimeoutException -import org.utbot.framework.plugin.api.UtVoidModel -import org.utbot.framework.plugin.api.isNotNull -import org.utbot.framework.plugin.api.isNull -import org.utbot.framework.plugin.api.onFailure -import org.utbot.framework.plugin.api.onSuccess -import org.utbot.framework.plugin.api.util.doubleArrayClassId -import org.utbot.framework.plugin.api.util.doubleClassId -import org.utbot.framework.plugin.api.util.doubleWrapperClassId -import org.utbot.framework.plugin.api.util.executable -import org.utbot.framework.plugin.api.util.jField -import org.utbot.framework.plugin.api.util.floatArrayClassId -import org.utbot.framework.plugin.api.util.floatClassId -import org.utbot.framework.plugin.api.util.floatWrapperClassId -import org.utbot.framework.plugin.api.util.hasField -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.isInnerClassEnclosingClassReference -import org.utbot.framework.plugin.api.util.isIterableOrMap -import org.utbot.framework.plugin.api.util.isPrimitive -import org.utbot.framework.plugin.api.util.isPrimitiveArray -import org.utbot.framework.plugin.api.util.isPrimitiveWrapper -import org.utbot.framework.plugin.api.util.isRefType -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.kClass -import org.utbot.framework.plugin.api.util.objectArrayClassId -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 org.utbot.framework.plugin.api.util.wrapIfPrimitive -import org.utbot.framework.util.isInaccessibleViaReflection -import org.utbot.framework.util.isUnit -import org.utbot.summary.SummarySentenceConstants.TAB -import java.lang.reflect.InvocationTargetException -import java.security.AccessControlException -import java.lang.reflect.ParameterizedType -import org.utbot.framework.UtSettings - -private const val DEEP_EQUALS_MAX_DEPTH = 5 // TODO move it to plugin settings? - -internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by context, - CgCallableAccessManager by getCallableAccessManagerBy(context), - CgStatementConstructor by getStatementConstructorBy(context) { - - private val nameGenerator = getNameGeneratorBy(context) - private val testFrameworkManager = getTestFrameworkManagerBy(context) - - private val variableConstructor = getVariableConstructorBy(context) - private val mockFrameworkManager = getMockFrameworkManagerBy(context) - - private val floatDelta: Float = 1e-6f - private val doubleDelta = 1e-6 - - // a model for execution result (it is lateinit because execution can fail, - // and we need it only on assertions generation stage - private lateinit var resultModel: UtModel - - private lateinit var methodType: CgTestMethodType - - private val fieldsOfExecutionResults = mutableMapOf, MutableList>() - - private fun setupInstrumentation() { - if (currentExecution is UtSymbolicExecution) { - val execution = currentExecution as UtSymbolicExecution - val instrumentation = execution.instrumentation - if (instrumentation.isEmpty()) return - - if (generateWarningsForStaticMocking && forceStaticMocking == ForceStaticMocking.DO_NOT_FORCE) { - // warn user about possible flaky tests - multilineComment(forceStaticMocking.warningMessage) - return - } - - instrumentation - .filterIsInstance() - .forEach { mockFrameworkManager.mockNewInstance(it) } - instrumentation - .filterIsInstance() - .groupBy { it.methodId.classId } - .forEach { (classId, methodMocks) -> mockFrameworkManager.mockStaticMethodsOfClass(classId, methodMocks) } - - if (generateWarningsForStaticMocking && forceStaticMocking == ForceStaticMocking.FORCE) { - // warn user about forced using static mocks - multilineComment(forceStaticMocking.warningMessage) - } - } - } - - /** - * Create variables for initial values of the static fields and store them in [prevStaticFieldValues] - * in order to use these variables at the end of the test to restore the initial static fields state. - * - * Note: - * Later in the test method we also cache the 'before' and 'after' states of fields (including static fields). - * This cache is stored in [statesCache]. - * - * However, it is _not_ the same cache as [prevStaticFieldValues]. - * The [statesCache] should not be confused with [prevStaticFieldValues] cache. - * - * The difference is that [prevStaticFieldValues] contains the static field states before we made _any_ changes. - * On the other hand, [statesCache] contains 'before' and 'after' states where the 'before' state is - * the state that we _specifically_ set up in order to cover a certain branch. - * - * Thus, this method only caches an actual initial static fields state in order to recover it - * at the end of the test, and it has nothing to do with the 'before' and 'after' caches. - */ - private fun rememberInitialStaticFields(statics: Map) { - val accessibleStaticFields = statics.accessibleFields() - for ((field, _) in accessibleStaticFields) { - val declaringClass = field.declaringClass - val fieldAccessible = field.canBeReadFrom(context) - - // prevValue is nullable if not accessible because of getStaticFieldValue(..) : Any? - val prevValue = newVar( - CgClassId(field.type, isNullable = !fieldAccessible), - "prev${field.name.capitalize()}" - ) { - if (fieldAccessible) { - declaringClass[field] - } else { - val declaringClassVar = newVar(classCgClassId) { - Class::class.id[forName](declaringClass.name) - } - utilsClassId[getStaticFieldValue](declaringClassVar, field.name) - } - } - // remember the previous value of a static field to recover it at the end of the test - prevStaticFieldValues[field] = prevValue - } - } - - private fun substituteStaticFields(statics: Map, isParametrized: Boolean = false) { - val accessibleStaticFields = statics.accessibleFields() - for ((field, model) in accessibleStaticFields) { - val declaringClass = field.declaringClass - val fieldAccessible = field.canBeSetFrom(context) - - val fieldValue = if (isParametrized) { - currentMethodParameters[CgParameterKind.Statics(model)] - } else { - variableConstructor.getOrCreateVariable(model, field.name) - } - - if (fieldAccessible) { - declaringClass[field] `=` fieldValue - } else { - val declaringClassVar = newVar(classCgClassId) { - Class::class.id[forName](declaringClass.name) - } - +utilsClassId[setStaticField](declaringClassVar, field.name, fieldValue) - } - } - } - - private fun recoverStaticFields() { - for ((field, prevValue) in prevStaticFieldValues.accessibleFields()) { - if (field.canBeSetFrom(context)) { - field.declaringClass[field] `=` prevValue - } else { - val declaringClass = getClassOf(field.declaringClass) - +utilsClassId[setStaticField](declaringClass, field.name, prevValue) - } - } - } - - private fun Map.accessibleFields(): Map = filterKeys { !it.isInaccessibleViaReflection } - - /** - * Generates result assertions for unit tests. - */ - private fun generateResultAssertions() { - when (currentExecutable) { - is ConstructorId -> generateConstructorCall(currentExecutable!!, currentExecution!!) - 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 - .onSuccess { result -> - methodType = SUCCESSFUL - - // TODO possible engine bug - void method return type and result not UtVoidModel - if (result.isUnit() || method.returnType == voidClassId) { - +thisInstance[method](*methodArguments.toTypedArray()) - } else { - resultModel = result - val expected = variableConstructor.getOrCreateVariable(result, "expected") - assertEquality(expected, actual) - } - } - .onFailure { exception -> - processExecutionFailure(currentExecution, exception) - } - } - else -> {} // TODO: check this specific case - } - } - - private fun processExecutionFailure(execution: UtExecution, exception: Throwable) { - val methodInvocationBlock = { - with(currentExecutable) { - when (this) { - is MethodId -> thisInstance[this](*methodArguments.toTypedArray()).intercepted() - is ConstructorId -> this(*methodArguments.toTypedArray()).intercepted() - else -> {} // TODO: check this specific case - } - } - } - - if (shouldTestPassWithException(execution, exception)) { - testFrameworkManager.expectException(exception::class.id) { - methodInvocationBlock() - } - methodType = SUCCESSFUL - - return - } - - if (shouldTestPassWithTimeoutException(execution, exception)) { - writeWarningAboutTimeoutExceeding() - testFrameworkManager.expectTimeout(hangingTestsTimeout.timeoutMs) { - methodInvocationBlock() - } - methodType = TIMEOUT - - return - } - - when (exception) { - is ConcreteExecutionFailureException -> { - methodType = CRASH - writeWarningAboutCrash() - } - is AccessControlException -> { - methodType = CRASH - writeWarningAboutFailureTest(exception) - return - } - else -> { - methodType = FAILING - writeWarningAboutFailureTest(exception) - } - } - - methodInvocationBlock() - } - - private fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean { - if (exception is AccessControlException) return false - // tests with timeout or crash should be processed differently - if (exception is TimeoutException || exception is ConcreteExecutionFailureException) return false - if (UtSettings.treatAssertAsErrorSuit && exception is AssertionError) return false - - val exceptionRequiresAssert = exception !is RuntimeException || runtimeExceptionTestsBehaviour == PASS - val exceptionIsExplicit = execution.result is UtExplicitlyThrownException - return exceptionRequiresAssert || exceptionIsExplicit - } - - private fun shouldTestPassWithTimeoutException(execution: UtExecution, exception: Throwable): Boolean { - return execution.result is UtTimeoutException || exception is TimeoutException - } - - private fun writeWarningAboutTimeoutExceeding() { - +CgMultilineComment( - listOf( - "This execution may take longer than the ${hangingTestsTimeout.timeoutMs} ms timeout", - " and therefore fail due to exceeding the timeout." - ) - ) - } - - private fun writeWarningAboutFailureTest(exception: Throwable) { - require(currentExecutable is ExecutableId) - val executableName = "${currentExecutable!!.classId.name}.${currentExecutable!!.name}" - - val warningLine = mutableListOf( - "This test fails because method [$executableName] produces [$exception]".escapeControlChars() - ) - - val neededStackTraceLines = mutableListOf() - var executableCallFound = false - exception.stackTrace.reversed().forEach { stackTraceElement -> - val line = stackTraceElement.toString() - if (line.startsWith(executableName)) { - executableCallFound = true - } - if (executableCallFound) { - neededStackTraceLines += TAB + line - } - } - - +CgMultilineComment(warningLine + neededStackTraceLines.reversed()) - } - - private fun String.escapeControlChars() : String { - return this.replace("\b", "\\b").replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r") - } - - private fun writeWarningAboutCrash() { - +CgSingleLineComment("This invocation possibly crashes JVM") - } - - /** - * Generates result assertions in parameterized tests for successful executions - * and just runs the method if all executions are unsuccessful. - */ - private fun generateAssertionsForParameterizedTest() { - emptyLineIfNeeded() - - when (currentExecutable) { - is ConstructorId -> generateConstructorCall(currentExecutable!!, currentExecution!!) - is MethodId -> { - val method = currentExecutable as MethodId - currentExecution!!.result - .onSuccess { result -> - if (result.isUnit()) { - +thisInstance[method](*methodArguments.toTypedArray()) - } else { - //"generic" expected variable is represented with a wrapper if - //actual result is primitive to support cases with exceptions. - resultModel = if (result is UtPrimitiveModel) assemble(result) else result - - val expectedVariable = currentMethodParameters[CgParameterKind.ExpectedResult]!! - val expectedExpression = CgNotNullAssertion(expectedVariable) - - assertEquality(expectedExpression, actual) - } - } - .onFailure { thisInstance[method](*methodArguments.toTypedArray()).intercepted() } - } - else -> {} // TODO: check this specific case - } - } - - /** - * Generates assertions for field states. - * - * Note: not supported in parameterized tests. - */ - private fun generateFieldStateAssertions() { - val thisInstanceCache = statesCache.thisInstance - for (path in thisInstanceCache.paths) { - assertStatesByPath(thisInstanceCache, path) - } - for (argumentCache in statesCache.arguments) { - for (path in argumentCache.paths) { - assertStatesByPath(argumentCache, path) - } - } - for ((_, staticFieldCache) in statesCache.classesWithStaticFields) { - for (path in staticFieldCache.paths) { - assertStatesByPath(staticFieldCache, path) - } - } - } - - /** - * If the given field is _not_ of reference type, then the variable for its 'before' state - * is not created, because we only need its final state to make an assertion. - * For reference type fields, in turn, we make an assertion assertFalse(before == after). - */ - private fun assertStatesByPath(cache: FieldStateCache, path: FieldPath) { - emptyLineIfNeeded() - val beforeVariable = cache.before[path]?.variable - val (afterVariable, afterModel) = cache.after[path]!! - - if (afterModel !is UtReferenceModel) { - val expectedAfter = - variableConstructor.getOrCreateVariable(afterModel, "expected" + afterVariable.name.capitalize()) - assertEquality(expectedAfter, afterVariable) - } else { - if (beforeVariable != null) - testFrameworkManager.assertBoolean(false, beforeVariable equalTo afterVariable) - // TODO: fail here - } - } - - private fun assertDeepEquals( - expectedModel: UtModel, - expected: CgVariable?, - actual: CgVariable, - depth: Int, - visitedModels: MutableSet, - ) { - if (expectedModel in visitedModels) return - - var expected = expected - if (expected == null) { - require(!needExpectedDeclaration(expectedModel)) - expected = actual - } - - visitedModels += expectedModel - - with(testFrameworkManager) { - if (depth >= DEEP_EQUALS_MAX_DEPTH) { - currentBlock += CgSingleLineComment("Current deep equals depth exceeds max depth $DEEP_EQUALS_MAX_DEPTH") - currentBlock += getDeepEqualsAssertion(expected, actual).toStatement() - return - } - - when (expectedModel) { - is UtPrimitiveModel -> { - currentBlock += when { - (expected.type == floatClassId || expected.type == floatWrapperClassId) -> - assertions[assertFloatEquals]( // cast have to be not safe here because of signature - typeCast(floatClassId, expected, isSafetyCast = false), - typeCast(floatClassId, actual, isSafetyCast = false), - floatDelta - ) - (expected.type == doubleClassId || expected.type == doubleWrapperClassId) -> - assertions[assertDoubleEquals]( // cast have to be not safe here because of signature - typeCast(doubleClassId, expected, isSafetyCast = false), - typeCast(doubleClassId, actual, isSafetyCast = false), - doubleDelta - ) - expectedModel.value is Boolean -> { - when (parametrizedTestSource) { - 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 - else -> { - require(expected.type.isPrimitive || expected.type == String::class.java) { - "Expected primitive or String but got ${expected.type}" - } - assertions[assertEquals](expected, actual) - } - }.toStatement() - } - is UtEnumConstantModel -> { - currentBlock += assertions[assertEquals]( - expected, - actual - ).toStatement() - } - is UtClassRefModel -> { - // TODO this stuff is needed because Kotlin has javaclass property instead of Java getClass method - // probably it is better to change getClass method behaviour in the future - val actualObject: CgVariable = when (codegenLanguage) { - CodegenLanguage.KOTLIN -> newVar( - baseType = objectClassId, - baseName = variableConstructor.constructVarName("actualObject"), - init = { CgTypeCast(objectClassId, actual) } - ) - else -> actual - } - - currentBlock += assertions[assertEquals]( - CgGetJavaClass(expected.type), - actualObject[getClass]() - ).toStatement() - } - is UtNullModel -> { - currentBlock += assertions[assertNull](actual).toStatement() - } - is UtArrayModel -> { - val arrayInfo = expectedModel.collectArrayInfo() - val nestedElementClassId = arrayInfo.nestedElementClassId - ?: error("Expected element class id from array ${arrayInfo.classId} but null found") - - if (!arrayInfo.isPrimitiveArray) { - // array of objects, have to use deep equals - - // We can't use for loop here because array model can contain different models - // and there is no a general approach to process it in loop - // For example, actual can be Object[3] and - // actual[0] instance of Point[][] - // actual[1] instance of int[][][] - // actual[2] instance of Object[] - - addArraysLengthAssertion(expected, actual) - currentBlock += getDeepEqualsAssertion(expected, actual).toStatement() - return - } - - // It does not work for Double and Float because JUnit does not have equals overloading with wrappers - if (nestedElementClassId == floatClassId || nestedElementClassId == doubleClassId) { - floatingPointArraysDeepEquals(arrayInfo, expected, actual) - return - } - - // common primitive array, can use default array equals - addArraysLengthAssertion(expected, actual) - currentBlock += getArrayEqualsAssertion( - expectedModel.classId, - typeCast(expectedModel.classId, expected, isSafetyCast = true), - typeCast(expectedModel.classId, actual, isSafetyCast = true) - ).toStatement() - } - is UtAssembleModel -> { - if (expectedModel.classId.isPrimitiveWrapper) { - currentBlock += assertions[assertEquals](expected, actual).toStatement() - return - } - - // UtCompositeModel deep equals is much more easier and human friendly - expectedModel.origin?.let { - assertDeepEquals(it, expected, actual, depth, visitedModels) - return - } - - // special case for strings as they are constructed from UtAssembleModel but can be compared with equals - if (expectedModel.classId == stringClassId) { - currentBlock += assertions[assertEquals]( - expected, - actual - ).toStatement() - return - } - - // We cannot implement deep equals for not field set model - // because if modification was made by direct field access, we can compare modifications by field access too - // (like in modification expected.value = 5 we can assert equality expected.value and actual.value), - // but in other cases we don't know what fields do we need to compare - // (like if modification was List add() method invocation) - - // We can add some heuristics to process standard assemble models like List, Set and Map. - // So, there is a space for improvements - if (expectedModel.modificationsChain.isEmpty() || expectedModel.modificationsChain.any { it !is UtDirectSetFieldModel }) { - currentBlock += getDeepEqualsAssertion(expected, actual).toStatement() - return - } - - for (modificationStep in expectedModel.modificationsChain) { - modificationStep as UtDirectSetFieldModel - val fieldId = modificationStep.fieldId - val fieldModel = modificationStep.fieldModel - - // we should not process enclosing class - // (actually, we do not do it properly anyway) - if (fieldId.isInnerClassEnclosingClassReference) continue - - traverseFieldRecursively( - fieldId, - fieldModel, - expected, - actual, - depth, - visitedModels - ) - } - } - is UtCompositeModel -> { - // Basically, to compare two iterables or maps, we need to iterate over them and compare each entry. - // But it leads to a lot of trash code in each test method, and it is more clear to use - // outer deep equals here - if (expected.isIterableOrMap()) { - currentBlock += CgSingleLineComment( - "${expected.type.canonicalName} is iterable or Map, use outer deep equals to iterate over" - ) - currentBlock += getDeepEqualsAssertion(expected, actual).toStatement() - - return - } - - if (expected.hasNotParametrizedCustomEquals()) { - // We rely on already existing equals - currentBlock += CgSingleLineComment("${expected.type.canonicalName} has overridden equals method") - currentBlock += assertions[assertEquals](expected, actual).toStatement() - - return - } - - for ((fieldId, fieldModel) in expectedModel.fields) { - // we should not process enclosing class - // (actually, we do not do it properly anyway) - if (fieldId.isInnerClassEnclosingClassReference) continue - - traverseFieldRecursively( - fieldId, - fieldModel, - expected, - actual, - depth, - visitedModels - ) - } - } - is UtLambdaModel -> Unit // we do not check equality of lambdas - is UtVoidModel -> { - // Unit result is considered in generateResultAssertions method - error("Unexpected UtVoidModel in deep equals") - } - } - } - } - - private fun TestFrameworkManager.addArraysLengthAssertion( - expected: CgVariable, - actual: CgVariable, - ): CgDeclaration { - val cgGetLengthDeclaration = CgDeclaration( - intClassId, - variableConstructor.constructVarName("${expected.name}Size"), - expected.length(this@CgMethodConstructor) - ) - currentBlock += cgGetLengthDeclaration - currentBlock += assertions[assertEquals]( - cgGetLengthDeclaration.variable, - actual.length(this@CgMethodConstructor) - ).toStatement() - - return cgGetLengthDeclaration - } - - /** - * Generate deep equals for float and double any-dimensional arrays (DOES NOT includes wrappers) - */ - private fun TestFrameworkManager.floatingPointArraysDeepEquals( - expectedArrayInfo: ClassIdArrayInfo, - expected: CgVariable, - actual: CgVariable, - ) { - val cgGetLengthDeclaration = addArraysLengthAssertion(expected, actual) - - val nestedElementClassId = expectedArrayInfo.nestedElementClassId - ?: error("Expected from floating point array ${expectedArrayInfo.classId} to contain elements but null found") - require(nestedElementClassId == floatClassId || nestedElementClassId == doubleClassId) { - "Expected float or double ClassId but `$nestedElementClassId` found" - } - - if (expectedArrayInfo.isSingleDimensionalArray) { - // we can use array equals for all single dimensional arrays - currentBlock += when (nestedElementClassId) { - floatClassId -> getFloatArrayEqualsAssertion( - typeCast(floatArrayClassId, expected, isSafetyCast = true), - typeCast(floatArrayClassId, actual, isSafetyCast = true), - floatDelta - ) - else -> getDoubleArrayEqualsAssertion( - typeCast(doubleArrayClassId, expected, isSafetyCast = true), - typeCast(doubleArrayClassId, actual, isSafetyCast = true), - doubleDelta - ) - }.toStatement() - } else { - // we can't use array equals for multidimensional double and float arrays - // so we need to go deeper to single-dimensional array - forLoop { - val (i, init) = variableConstructor.loopInitialization(intClassId, "i", initializer = 0) - initialization = init - condition = i lessThan cgGetLengthDeclaration.variable.resolve() - update = i.inc() - - statements = block { - val expectedNestedElement = newVar( - baseType = expected.type.elementClassId!!, - baseName = variableConstructor.constructVarName("${expected.name}NestedElement"), - init = { CgArrayElementAccess(expected, i) } - ) - - val actualNestedElement = newVar( - baseType = actual.type.elementClassId!!, - baseName = variableConstructor.constructVarName("${actual.name}NestedElement"), - init = { CgArrayElementAccess(actual, i) } - ) - - emptyLine() - - ifStatement( - CgEqualTo(expectedNestedElement, nullLiteral()), - trueBranch = { +assertions[assertNull](actualNestedElement).toStatement() }, - falseBranch = { - floatingPointArraysDeepEquals( - expectedArrayInfo.getNested(), - expectedNestedElement, - actualNestedElement, - ) - } - ) - } - } - } - } - - private fun CgVariable.isIterableOrMap(): Boolean = type.isIterableOrMap - - /** - * Some classes have overridden equals method, but it doesn't work properly. - * For example, List has overridden equals method but it relies on T equals. - * So, if T doesn't override equals, assertEquals with List fails. - * Therefore, all standard collections and map can fail. - * We overapproximate this assumption for all parametrized classes because we can't be sure that - * overridden equals doesn't rely on type parameters equals. - */ - private fun CgVariable.hasNotParametrizedCustomEquals(): Boolean { - if (type.jClass.overridesEquals()) { - // type parameters is list of class type parameters - empty if class is not generic - val typeParameters = type.kClass.typeParameters - - return typeParameters.isEmpty() - } - - return false - } - - private fun traverseFieldRecursively( - fieldId: FieldId, - fieldModel: UtModel, - expected: CgVariable, - actual: CgVariable, - 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) { - currentBlock += testFrameworkManager.getDeepEqualsAssertion(expected, actual).toStatement() - return - } - - when (parametrizedTestSource) { - ParametrizedTestSource.DO_NOT_PARAMETRIZE -> { - traverseField(fieldId, fieldModel, expected, actual, depth, visitedModels) - } - - ParametrizedTestSource.PARAMETRIZE -> { - traverseFieldForParametrizedTest(fieldId, fieldModel, expected, actual, depth, visitedModels) - } - } - } - - private fun traverseField( - fieldId: FieldId, - fieldModel: UtModel, - expected: CgVariable, - actual: CgVariable, - depth: Int, - visitedModels: MutableSet - ) { - // fieldModel is not visited and will be marked in assertDeepEquals call - val fieldName = fieldId.name - var expectedVariable: CgVariable? = null - - if (needExpectedDeclaration(fieldModel)) { - val expectedFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, expected, fieldName) - - currentBlock += expectedFieldDeclaration - expectedVariable = expectedFieldDeclaration.variable - } - - val actualFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, actual, fieldName) - currentBlock += actualFieldDeclaration - - assertDeepEquals( - fieldModel, - expectedVariable, - actualFieldDeclaration.variable, - depth + 1, - visitedModels, - ) - emptyLineIfNeeded() - } - - private fun traverseFieldForParametrizedTest( - fieldId: FieldId, - fieldModel: UtModel, - expected: CgVariable, - actual: CgVariable, - depth: Int, - visitedModels: MutableSet - ) { - val fieldResultModels = fieldsOfExecutionResults[fieldId to depth] - val nullResultModelInExecutions = fieldResultModels?.find { it.isNull() } - val notNullResultModelInExecutions = fieldResultModels?.find { it.isNotNull() } - - val hasNullResultModel = nullResultModelInExecutions != null - val hasNotNullResultModel = notNullResultModelInExecutions != null - - val needToSubstituteFieldModel = fieldModel is UtNullModel && hasNotNullResultModel - - val fieldModelForAssert = if (needToSubstituteFieldModel) notNullResultModelInExecutions!! else fieldModel - - // fieldModel is not visited and will be marked in assertDeepEquals call - val fieldName = fieldId.name - var expectedVariable: CgVariable? = null - - val needExpectedDeclaration = needExpectedDeclaration(fieldModelForAssert) - if (needExpectedDeclaration) { - val expectedFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, expected, fieldName) - - currentBlock += expectedFieldDeclaration - expectedVariable = expectedFieldDeclaration.variable - } - - val actualFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, actual, fieldName) - currentBlock += actualFieldDeclaration - - if (needExpectedDeclaration && hasNullResultModel) { - ifStatement( - CgEqualTo(expectedVariable!!, nullLiteral()), - trueBranch = { +testFrameworkManager.assertions[testFramework.assertNull](actualFieldDeclaration.variable).toStatement() }, - falseBranch = { - assertDeepEquals( - fieldModelForAssert, - expectedVariable, - actualFieldDeclaration.variable, - depth + 1, - visitedModels, - ) - } - ) - } else { - assertDeepEquals( - fieldModelForAssert, - expectedVariable, - actualFieldDeclaration.variable, - depth + 1, - visitedModels, - ) - } - emptyLineIfNeeded() - } - - private fun collectExecutionsResultFields() { - for (model in successfulExecutionsModels) { - when (model) { - is UtCompositeModel -> { - for ((fieldId, fieldModel) in model.fields) { - collectExecutionsResultFieldsRecursively(fieldId, fieldModel, 0) - } - } - - is UtAssembleModel -> { - model.origin?.let { - for ((fieldId, fieldModel) in it.fields) { - collectExecutionsResultFieldsRecursively(fieldId, fieldModel, 0) - } - } - } - - // Lambdas do not have fields. They have captured values, but we do not consider them here. - is UtLambdaModel, - is UtNullModel, - is UtPrimitiveModel, - is UtArrayModel, - is UtClassRefModel, - is UtEnumConstantModel, - is UtVoidModel -> { - // only [UtCompositeModel] and [UtAssembleModel] have fields to traverse - } - } - } - } - - private fun collectExecutionsResultFieldsRecursively( - fieldId: FieldId, - fieldModel: UtModel, - depth: Int, - ) { - if (depth >= DEEP_EQUALS_MAX_DEPTH) { - return - } - - val fieldKey = fieldId to depth - fieldsOfExecutionResults.getOrPut(fieldKey) { mutableListOf() } += fieldModel - - when (fieldModel) { - is UtCompositeModel -> { - for ((id, model) in fieldModel.fields) { - collectExecutionsResultFieldsRecursively(id, model, depth + 1) - } - } - - is UtAssembleModel -> { - fieldModel.origin?.let { - for ((id, model) in it.fields) { - collectExecutionsResultFieldsRecursively(id, model, depth + 1) - } - } - } - - // Lambdas do not have fields. They have captured values, but we do not consider them here. - is UtLambdaModel, - is UtNullModel, - is UtPrimitiveModel, - is UtArrayModel, - is UtClassRefModel, - is UtEnumConstantModel, - is UtVoidModel -> { - // only [UtCompositeModel] and [UtAssembleModel] have fields to traverse - } - } - } - - @Suppress("UNUSED_ANONYMOUS_PARAMETER") - private fun createDeclarationForFieldFromVariable( - fieldId: FieldId, - variable: CgVariable, - fieldName: String - ): CgDeclaration { - val expectedFieldDeclaration = createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( - baseType = fieldId.type, - baseName = "${variable.name}${fieldName.capitalize()}", - init = { fieldId.getAccessExpression(variable) } - ).either( - { declaration -> declaration }, - { unexpectedExistingVariable -> - error( - "Unexpected existing variable for field $fieldName with type ${fieldId.type} " + - "from expected variable ${variable.name} with type ${variable.type}" - ) - } - ) - return expectedFieldDeclaration - } - - private fun FieldId.getAccessExpression(variable: CgVariable): CgExpression = - // Can directly access field only if it is declared in variable class (or in its ancestors) - // and is accessible from current package - if (variable.type.hasField(this) && canBeReadFrom(context)) { - if (jField.isStatic) CgStaticFieldAccess(this) else CgFieldAccess(variable, this) - } else { - utilsClassId[getFieldValue](variable, this.declaringClass.name, this.name) - } - - /** - * Stores array information about ClassId. - * @property classId ClassId itself. - * @property nestedElementClassId the most nested element ClassId if it is array and null otherwise. - * @property dimensions 0 for non-arrays and number of dimensions in case of arrays. - */ - private data class ClassIdArrayInfo(val classId: ClassId, val nestedElementClassId: ClassId?, val dimensions: Int) { - val isArray get(): Boolean = dimensions > 0 - - val isPrimitiveArray get(): Boolean = isArray && nestedElementClassId!!.isPrimitive - - val isSingleDimensionalArray get(): Boolean = dimensions == 1 - - fun getNested(): ClassIdArrayInfo { - require(dimensions > 0) { "Trying to get nested array from not array type $classId" } - - return copy(dimensions = dimensions - 1) - } - } - - private val UtArrayModel.isArray: Boolean - get() = this.classId.isArray - - private fun UtArrayModel.collectArrayInfo(): ClassIdArrayInfo { - if (!isArray) return ClassIdArrayInfo( - classId = classId, - nestedElementClassId = null, - dimensions = 0 - ) - - val nestedElementClassIdList = generateSequence(classId.elementClassId) { it.elementClassId }.toList() - val dimensions = nestedElementClassIdList.size - val nestedElementClassId = nestedElementClassIdList.last() - - return ClassIdArrayInfo(classId, nestedElementClassId, dimensions) - } - - private fun assertEquality(expected: CgValue, actual: CgVariable) { - when { - expected.type.isArray -> { - // TODO: How to compare arrays of Float and Double wrappers? - // TODO: For example, JUnit5 does not have an assertEquals() overload for these wrappers. - // TODO: So for now we compare arrays of these wrappers as arrays of Objects, but that is probably wrong. - when (expected.type.elementClassId!!) { - floatClassId -> testFrameworkManager.assertFloatArrayEquals( - typeCast(floatArrayClassId, expected, isSafetyCast = true), - typeCast(floatArrayClassId, actual, isSafetyCast = true), - floatDelta - ) - doubleClassId -> testFrameworkManager.assertDoubleArrayEquals( - typeCast(doubleArrayClassId, expected, isSafetyCast = true), - typeCast(doubleArrayClassId, actual, isSafetyCast = true), - doubleDelta - ) - else -> { - val targetType = when { - expected.type.isPrimitiveArray -> expected.type - actual.type.isPrimitiveArray -> actual.type - else -> objectArrayClassId - } - if (targetType.isPrimitiveArray) { - // we can use simple arrayEquals for primitive arrays - testFrameworkManager.assertArrayEquals( - targetType, - typeCast(targetType, expected, isSafetyCast = true), - typeCast(targetType, actual, isSafetyCast = true) - ) - } else { - // array of objects, have to use deep equals - - when (expected) { - is CgLiteral -> testFrameworkManager.assertEquals(expected, actual) - is CgNotNullAssertion -> generateForNotNullAssertion(expected, actual) - else -> { - require(resultModel is UtArrayModel) { - "Result model have to be UtArrayModel to generate arrays assertion " + - "but `${resultModel::class}` found" - } - generateDeepEqualsOrNullAssertion(expected, actual) - } - } - } - } - } - } - else -> when { - (expected.type == floatClassId || expected.type == floatWrapperClassId) -> { - testFrameworkManager.assertFloatEquals( - typeCast(floatClassId, expected, isSafetyCast = true), - typeCast(floatClassId, actual, isSafetyCast = true), - floatDelta - ) - } - (expected.type == doubleClassId || expected.type == doubleWrapperClassId) -> { - testFrameworkManager.assertDoubleEquals( - typeCast(doubleClassId, expected, isSafetyCast = true), - typeCast(doubleClassId, actual, isSafetyCast = true), - doubleDelta - ) - } - expected == nullLiteral() -> testFrameworkManager.assertNull(actual) - expected is CgLiteral && expected.value is Boolean -> { - when (parametrizedTestSource) { - ParametrizedTestSource.DO_NOT_PARAMETRIZE -> - testFrameworkManager.assertBoolean(expected.value, actual) - ParametrizedTestSource.PARAMETRIZE -> - testFrameworkManager.assertEquals(expected, actual) - } - } - else -> { - when (expected) { - is CgLiteral -> testFrameworkManager.assertEquals(expected, actual) - is CgNotNullAssertion -> generateForNotNullAssertion(expected, actual) - else -> generateDeepEqualsOrNullAssertion(expected, actual) - } - } - } - } - } - - private fun generateForNotNullAssertion(expected: CgNotNullAssertion, actual: CgVariable) { - require(expected.expression is CgVariable) { - "Only CgVariable wrapped in CgNotNullAssertion is supported in deepEquals" - } - generateDeepEqualsOrNullAssertion(expected.expression, actual) - } - - private fun generateConstructorCall(currentExecutableId: ExecutableId, currentExecution: UtExecution) { - // we cannot generate any assertions for constructor testing - // but we need to generate a constructor call - val constructorCall = currentExecutableId as ConstructorId - currentExecution.result - .onSuccess { - methodType = SUCCESSFUL - - require(!constructorCall.classId.isInner) { - "Inner class ${constructorCall.classId} constructor testing is not supported yet" - } - - actual = newVar(constructorCall.classId, "actual") { - constructorCall(*methodArguments.toTypedArray()) - } - } - .onFailure { exception -> - processExecutionFailure(currentExecution, exception) - } - } - - /** - * 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, - ) { - when (parametrizedTestSource) { - ParametrizedTestSource.DO_NOT_PARAMETRIZE -> generateDeepEqualsAssertion(expected, actual) - ParametrizedTestSource.PARAMETRIZE -> { - collectExecutionsResultFields() - - when { - actual.type.isPrimitive -> generateDeepEqualsAssertion(expected, actual) - else -> ifStatement( - CgEqualTo(expected, nullLiteral()), - trueBranch = { +testFrameworkManager.assertions[testFramework.assertNull](actual).toStatement() }, - falseBranch = { - +testFrameworkManager.assertions[testFrameworkManager.assertNotNull](actual).toStatement() - generateDeepEqualsAssertion(expected, actual) - } - ) - } - } - } - } - - private fun generateDeepEqualsAssertion( - expected: CgValue, - actual: CgVariable, - ) { - require(expected is CgVariable) { - "Expected value have to be Literal or Variable but `${expected::class}` found" - } - - assertDeepEquals( - resultModel, - expected, - actual, - depth = 0, - visitedModels = hashSetOf() - ) - } - - private fun recordActualResult() { - currentExecution!!.result.onSuccess { result -> - when (val executable = currentExecutable) { - is ConstructorId -> { - // there is nothing to generate for constructors - return - } - is BuiltinMethodId -> error("Unexpected BuiltinMethodId $currentExecutable while generating actual result") - is MethodId -> { - // TODO possible engine bug - void method return type and result not UtVoidModel - if (result.isUnit() || executable.returnType == voidClassId) return - - emptyLineIfNeeded() - - actual = newVar( - CgClassId(result.classId, isNullable = result is UtNullModel), - "actual" - ) { - thisInstance[executable](*methodArguments.toTypedArray()) - } - } - else -> {} // TODO: check this specific case - } - } - } - - fun createTestMethod(executableId: ExecutableId, execution: UtExecution): CgTestMethod = - withTestMethodScope(execution) { - val testMethodName = nameGenerator.testMethodNameFor(executableId, execution.testMethodName) - // TODO: remove this line when SAT-1273 is completed - execution.displayName = execution.displayName?.let { "${executableId.name}: $it" } - testMethod(testMethodName, execution.displayName) { - val statics = currentExecution!!.stateBefore.statics - rememberInitialStaticFields(statics) - val stateAnalyzer = ExecutionStateAnalyzer(execution) - val modificationInfo = stateAnalyzer.findModifiedFields() - val fieldStateManager = CgFieldStateManagerImpl(context) - // TODO: move such methods to another class and leave only 2 public methods: remember initial and final states - val mainBody = { - substituteStaticFields(statics) - setupInstrumentation() - // build this instance - thisInstance = execution.stateBefore.thisInstance?.let { - variableConstructor.getOrCreateVariable(it) - } - // build arguments - for ((index, param) in execution.stateBefore.parameters.withIndex()) { - val name = paramNames[executableId]?.get(index) - methodArguments += variableConstructor.getOrCreateVariable(param, name) - } - fieldStateManager.rememberInitialEnvironmentState(modificationInfo) - recordActualResult() - generateResultAssertions() - fieldStateManager.rememberFinalEnvironmentState(modificationInfo) - generateFieldStateAssertions() - } - - if (statics.isNotEmpty()) { - +tryBlock { - mainBody() - }.finally { - recoverStaticFields() - } - } else { - mainBody() - } - - mockFrameworkManager.getAndClearMethodResources()?.let { resources -> - val closeFinallyBlock = resources.map { - val variable = it.variable - variable.type.closeMethodIdOrNull?.let { closeMethod -> - CgMethodCall(variable, closeMethod, arguments = emptyList()).toStatement() - } ?: error("Resource $variable was expected to be auto closeable but it is not") - } - - val tryWithMocksFinallyClosing = CgTryCatch(currentBlock, handlers = emptyList(), closeFinallyBlock) - currentBlock = currentBlock.clear() - resources.forEach { - // First argument for mocked resource declaration initializer is a target type. - // Pass this argument as a type parameter for the mocked resource - - // TODO this type parameter (required for Kotlin test) is unused until the proper implementation - // of generics in code generation https://github.com/UnitTestBot/UTBotJava/issues/88 - @Suppress("UNUSED_VARIABLE") - val typeParameter = when (val firstArg = (it.initializer as CgMethodCall).arguments.first()) { - is CgGetJavaClass -> firstArg.classId - is CgVariable -> firstArg.type - else -> error("Unexpected mocked resource declaration argument $firstArg") - } - - +CgDeclaration( - it.variableType, - it.variableName, - initializer = nullLiteral(), - isMutable = true, - ) - } - +tryWithMocksFinallyClosing - } - } - } - - private val expectedResultVarName = "expectedResult" - private val expectedErrorVarName = "expectedError" - - fun createParameterizedTestMethod(testSet: CgMethodTestSet, dataProviderMethodName: String): CgTestMethod { - //TODO: orientation on generic execution may be misleading, but what is the alternative? - //may be a heuristic to select a model with minimal number of internal nulls should be used - val genericExecution = chooseGenericExecution(testSet.executions) - - val statics = genericExecution.stateBefore.statics - - return withTestMethodScope(genericExecution) { - val testName = nameGenerator.parameterizedTestMethodName(dataProviderMethodName) - withNameScope { - val testParameterDeclarations = createParameterDeclarations(testSet, genericExecution) - - methodType = PARAMETRIZED - testMethod( - testName, - displayName = null, - testParameterDeclarations, - parameterized = true, - dataProviderMethodName - ) { - rememberInitialStaticFields(statics) - substituteStaticFields(statics, isParametrized = true) - - // build this instance - thisInstance = genericExecution.stateBefore.thisInstance?.let { - variableConstructor.getOrCreateVariable(it) - } - - // build arguments for method under test and parameterized test - for (index in genericExecution.stateBefore.parameters.indices) { - methodArguments += currentMethodParameters[CgParameterKind.Argument(index)]!! - } - - if (containsFailureExecution(testSet) || statics.isNotEmpty()) { - var currentTryBlock = tryBlock { - recordActualResult() - generateAssertionsForParameterizedTest() - } - - if (containsFailureExecution(testSet)) { - val expectedErrorVariable = currentMethodParameters[CgParameterKind.ExpectedException] - ?: error("Test set $testSet contains failure execution, but test method signature has no error parameter") - currentTryBlock = - if (containsReflectiveCall) { - currentTryBlock.catch(InvocationTargetException::class.java.id) { exception -> - testFrameworkManager.assertBoolean( - expectedErrorVariable.isInstance(exception[getTargetException]()) - ) - } - } else { - currentTryBlock.catch(Throwable::class.java.id) { throwable -> - testFrameworkManager.assertBoolean( - expectedErrorVariable.isInstance(throwable) - ) - } - } - } - - if (statics.isNotEmpty()) { - currentTryBlock = currentTryBlock.finally { - recoverStaticFields() - } - } - +currentTryBlock - } else { - recordActualResult() - generateAssertionsForParameterizedTest() - } - } - } - } - } - - private fun chooseGenericExecution(executions: List): UtExecution { - return executions - .firstOrNull { it.result is UtExecutionSuccess && (it.result as UtExecutionSuccess).model !is UtNullModel } - ?: executions - .firstOrNull { it.result is UtExecutionSuccess } ?: executions.first() - } - - private fun createParameterDeclarations( - testSet: CgMethodTestSet, - genericExecution: UtExecution, - ): List { - val executableUnderTest = testSet.executableId - val executableUnderTestParameters = testSet.executableId.executable.parameters - - return mutableListOf().apply { - // arguments - for (index in genericExecution.stateBefore.parameters.indices) { - val argumentName = paramNames[executableUnderTest]?.get(index) - val paramType = executableUnderTestParameters[index].parameterizedType - - val argumentType = when { - paramType is Class<*> && paramType.isArray -> paramType.id - paramType is ParameterizedType -> paramType.id - else -> ClassId(paramType.typeName) - } - - val argument = CgParameterDeclaration( - parameter = declareParameter( - type = argumentType, - name = nameGenerator.variableName(argumentType, argumentName), - ), - isReferenceType = argumentType.isRefType - ) - this += argument - currentMethodParameters[CgParameterKind.Argument(index)] = argument.parameter - } - - val statics = genericExecution.stateBefore.statics - if (statics.isNotEmpty()) { - for ((fieldId, model) in statics) { - val staticType = wrapTypeIfRequired(model.classId) - val static = CgParameterDeclaration( - parameter = declareParameter( - type = staticType, - name = nameGenerator.variableName(fieldId.name, isStatic = true) - ), - isReferenceType = staticType.isRefType - ) - this += static - currentMethodParameters[CgParameterKind.Statics(model)] = static.parameter - } - } - - val expectedResultClassId = wrapTypeIfRequired(testSet.resultType()) - if (expectedResultClassId != voidClassId) { - val wrappedType = wrapIfPrimitive(expectedResultClassId) - //We are required to wrap the type of expected result if it is primitive - //to support nulls for throwing exceptions executions. - val expectedResult = CgParameterDeclaration( - parameter = declareParameter( - type = wrappedType, - name = nameGenerator.variableName(expectedResultVarName) - ), - isReferenceType = wrappedType.isRefType - ) - this += expectedResult - currentMethodParameters[CgParameterKind.ExpectedResult] = expectedResult.parameter - } - - val containsFailureExecution = containsFailureExecution(testSet) - if (containsFailureExecution) { - val classClassId = Class::class.id - val expectedException = CgParameterDeclaration( - parameter = declareParameter( - type = BuiltinClassId( - name = classClassId.name, - simpleName = classClassId.simpleName, - canonicalName = classClassId.canonicalName, - packageName = classClassId.packageName, - typeParameters = TypeParameters(listOf(Throwable::class.java.id)) - ), - name = nameGenerator.variableName(expectedErrorVarName) - ), - // exceptions are always reference type - isReferenceType = true, - ) - this += expectedException - currentMethodParameters[CgParameterKind.ExpectedException] = expectedException.parameter - } - } - } - - /** - * Constructs data provider method for parameterized tests. - * - * The body of this method is constructed manually, statement by statement. - * Standard logic for generating each test case parameter code is used. - */ - fun createParameterizedTestDataProvider( - testSet: CgMethodTestSet, - dataProviderMethodName: String - ): CgParameterizedTestDataProviderMethod { - return withDataProviderScope { - dataProviderMethod(dataProviderMethodName) { - val argListLength = testSet.executions.size - val argListVariable = testFrameworkManager.createArgList(argListLength) - - emptyLine() - - for ((execIndex, execution) in testSet.executions.withIndex()) { - // create a block for current test case - innerBlock { - val arguments = createExecutionArguments(testSet, execution) - createArgumentsCallRepresentation(execIndex, argListVariable, arguments) - } - } - - emptyLineIfNeeded() - - returnStatement { argListVariable } - } - } - } - - private fun createExecutionArguments(testSet: CgMethodTestSet, execution: UtExecution): List { - val arguments = mutableListOf() - - for ((paramIndex, paramModel) in execution.stateBefore.parameters.withIndex()) { - val argumentName = paramNames[testSet.executableId]?.get(paramIndex) - arguments += variableConstructor.getOrCreateVariable(paramModel, argumentName) - } - - val statics = execution.stateBefore.statics - for ((field, model) in statics) { - arguments += variableConstructor.getOrCreateVariable(model, field.name) - } - - - val method = currentExecutable!! - val needsReturnValue = method.returnType != voidClassId - val containsFailureExecution = containsFailureExecution(testSet) - execution.result - .onSuccess { - if (needsReturnValue) { - arguments += variableConstructor.getOrCreateVariable(it) - } - if (containsFailureExecution) { - arguments += nullLiteral() - } - } - .onFailure { - if (needsReturnValue) { - arguments += nullLiteral() - } - if (containsFailureExecution) { - arguments += CgGetJavaClass(it::class.id) - } - } - - emptyLineIfNeeded() - - return arguments - } - - private fun withTestMethodScope(execution: UtExecution, block: () -> R): R { - clearTestMethodScope() - currentExecution = execution - statesCache = EnvironmentFieldStateCache.emptyCacheFor(execution) - return try { - block() - } finally { - clearTestMethodScope() - } - } - - private fun withDataProviderScope(block: () -> R): R { - clearMethodScope() - return try { - block() - } finally { - clearMethodScope() - } - } - - /** - * This function makes sure some information about the method currently being generated is empty. - * It clears only the information that is relevant to all kinds of methods: - * - test methods - * - data provider methods - * - and any other kinds of methods that may be added in the future - */ - private fun clearMethodScope() { - collectedExceptions.clear() - collectedMethodAnnotations.clear() - } - - /** - * This function makes sure some information about the **test method** currently being generated is empty. - * It is used at the start of test method generation and right after it. - */ - private fun clearTestMethodScope() { - clearMethodScope() - prevStaticFieldValues.clear() - thisInstance = null - methodArguments.clear() - currentExecution = null - containsReflectiveCall = false - mockFrameworkManager.clearExecutionResources() - currentMethodParameters.clear() - } - - /** - * Generates a collection of [CgStatement] to prepare arguments - * for current execution in parameterized tests. - */ - private fun createArgumentsCallRepresentation( - executionIndex: Int, - argsVariable: CgVariable, - arguments: List, - ) { - val argsArray = newVar(objectArrayClassId, "testCaseObjects") { - CgAllocateArray(objectArrayClassId, objectClassId, arguments.size) - } - for ((i, argument) in arguments.withIndex()) { - setArgumentsArrayElement(argsArray, i, argument, this) - } - testFrameworkManager.passArgumentsToArgsVariable(argsVariable, argsArray, executionIndex) - } - - private fun containsFailureExecution(testSet: CgMethodTestSet) = - testSet.executions.any { it.result is UtExecutionFailure } - - - private fun testMethod( - methodName: String, - displayName: String?, - params: List = emptyList(), - parameterized: Boolean = false, - dataProviderMethodName: String? = null, - body: () -> Unit, - ): CgTestMethod { - collectedMethodAnnotations += if (parameterized) { - testFrameworkManager.collectParameterizedTestAnnotations(dataProviderMethodName) - } else { - setOf(annotation(testFramework.testAnnotationId)) - } - - displayName?.let { - testFrameworkManager.addTestDescription(displayName) - } - - val result = currentExecution!!.result - if (result is UtTimeoutException) { - testFrameworkManager.setTestExecutionTimeout(hangingTestsTimeout.timeoutMs) - } - - if (result is UtTimeoutException && !enableTestsTimeout) { - testFrameworkManager.disableTestMethod( - "Disabled due to failing by exceeding the timeout" - ) - } - - if (result is UtConcreteExecutionFailure) { - testFrameworkManager.disableTestMethod( - "Disabled due to possible JVM crash" - ) - } - - if (result is UtSandboxFailure) { - testFrameworkManager.disableTestMethod( - "Disabled due to sandbox" - ) - } - - val testMethod = buildTestMethod { - name = methodName - parameters = params - statements = block(body) - // Exceptions and annotations assignment must run after the statements block is build, - // because we collect info about exceptions and required annotations while building the statements - exceptions += collectedExceptions - annotations += collectedMethodAnnotations - methodType = this@CgMethodConstructor.methodType - val docComment = currentExecution?.summary?.map { convertDocToCg(it) }?.toMutableList() ?: mutableListOf() - - // add JVM crash report path if exists - if (result is UtConcreteExecutionFailure) { - result.extractJvmReportPathOrNull()?.let { - val jvmReportDocumentation = CgDocRegularStmt(getJvmReportDocumentation(it)) - val lastTag = docComment.lastOrNull() - // if the last statement is a

     tag, put the path inside it
    -                    if (lastTag == null || lastTag !is CgDocPreTagStatement) {
    -                        docComment += jvmReportDocumentation
    -                    } else {
    -                        val tagContent = lastTag.content
    -                        docComment.removeLast()
    -                        docComment += CgDocPreTagStatement(tagContent + jvmReportDocumentation)
    -                    }
    -                }
    -            }
    -
    -            documentation = CgDocumentationComment(docComment)
    -            documentation = if (parameterized) {
    -                CgDocumentationComment(text = null)
    -            } else {
    -                CgDocumentationComment(docComment)
    -            }
    -        }
    -        testMethods += testMethod
    -        return testMethod
    -    }
    -
    -    private fun dataProviderMethod(dataProviderMethodName: String, body: () -> Unit): CgParameterizedTestDataProviderMethod {
    -        return buildParameterizedTestDataProviderMethod {
    -            name = dataProviderMethodName
    -            returnType = testFramework.argListClassId
    -            statements = block(body)
    -            // Exceptions and annotations assignment must run after the statements block is build,
    -            // because we collect info about exceptions and required annotations while building the statements
    -            exceptions += collectedExceptions
    -            annotations += testFrameworkManager.createDataProviderAnnotations(dataProviderMethodName)
    -        }
    -    }
    -
    -    fun errorMethod(executable: ExecutableId, errors: Map): CgRegion {
    -        val name = nameGenerator.errorMethodNameFor(executable)
    -        val body = block {
    -            comment("Couldn't generate some tests. List of errors:")
    -            comment()
    -            errors.entries.sortedByDescending { it.value }.forEach { (message, repeated) ->
    -                val multilineMessage = message
    -                    .split("\r") // split stacktrace from concrete if present
    -                    .flatMap { line ->
    -                        line
    -                            .split(" ")
    -                            .windowed(size = 10, step = 10, partialWindows = true) {
    -                                it.joinToString(separator = " ")
    -                            }
    -                    }
    -                comment("$repeated occurrences of:")
    -
    -                if (multilineMessage.size <= 1) {
    -                    // wrap one liner with line comment
    -                    multilineMessage.singleOrNull()?.let { comment(it) }
    -                } else {
    -                    // wrap all lines with multiline comment
    -                    multilineComment(multilineMessage)
    -                }
    -
    -                emptyLine()
    -            }
    -        }
    -        val errorTestMethod = CgErrorTestMethod(name, body)
    -        return CgSimpleRegion("Errors report for ${executable.name}", listOf(errorTestMethod))
    -    }
    -
    -    private fun getJvmReportDocumentation(jvmReportPath: String): String {
    -        val pureJvmReportPath = jvmReportPath.substringAfter("# ")
    -
    -        // \n is here because IntellijIdea cannot process other separators
    -        return PathUtil.toHtmlLinkTag(PathUtil.replaceSeparator(pureJvmReportPath), fileName = "JVM crash report") + "\n"
    -    }
    -
    -    private fun UtConcreteExecutionFailure.extractJvmReportPathOrNull(): String? =
    -        exception.processStdout.singleOrNull {
    -            "hs_err_pid" in it
    -        }
    -
    -    private fun CgExecutableCall.wrapReflectiveCall() {
    -        +tryBlock {
    -            +this@wrapReflectiveCall
    -        }.catch(InvocationTargetException::class.id) { e ->
    -            throwStatement {
    -                e[getTargetException]()
    -            }
    -        }
    -    }
    -
    -    /**
    -     * Intercept calls to [java.lang.reflect.Method.invoke] and to [java.lang.reflect.Constructor.newInstance]
    -     * in order to wrap these calls in a try-catch block that will handle [InvocationTargetException]
    -     * that may be thrown by these calls.
    -     */
    -    private fun CgExecutableCall.intercepted() {
    -        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
    deleted file mode 100644
    index 34b876307e..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt
    +++ /dev/null
    @@ -1,333 +0,0 @@
    -package org.utbot.framework.codegen.model.constructor.tree
    -
    -import org.utbot.framework.codegen.Junit4
    -import org.utbot.framework.codegen.Junit5
    -import org.utbot.framework.codegen.ParametrizedTestSource
    -import org.utbot.framework.codegen.TestNg
    -import org.utbot.framework.codegen.model.constructor.CgMethodTestSet
    -import org.utbot.framework.codegen.model.constructor.TestClassModel
    -import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider
    -import org.utbot.framework.codegen.model.constructor.context.CgContext
    -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner
    -import org.utbot.framework.codegen.model.constructor.name.CgNameGenerator
    -import org.utbot.framework.codegen.model.constructor.name.CgNameGeneratorImpl
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.clearContextRelatedStorage
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getMethodConstructorBy
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getNameGeneratorBy
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getStatementConstructorBy
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getTestFrameworkManagerBy
    -import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor
    -import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructorImpl
    -import org.utbot.framework.codegen.model.tree.CgAuxiliaryClass
    -import org.utbot.framework.codegen.model.tree.CgExecutableUnderTestCluster
    -import org.utbot.framework.codegen.model.tree.CgMethod
    -import org.utbot.framework.codegen.model.tree.CgParameterDeclaration
    -import org.utbot.framework.codegen.model.tree.CgRegion
    -import org.utbot.framework.codegen.model.tree.CgSimpleRegion
    -import org.utbot.framework.codegen.model.tree.CgStaticsRegion
    -import org.utbot.framework.codegen.model.tree.CgTestClass
    -import org.utbot.framework.codegen.model.tree.CgTestClassFile
    -import org.utbot.framework.codegen.model.tree.CgTestMethod
    -import org.utbot.framework.codegen.model.tree.CgTestMethodCluster
    -import org.utbot.framework.codegen.model.tree.CgTripleSlashMultilineComment
    -import org.utbot.framework.codegen.model.tree.CgUtilEntity
    -import org.utbot.framework.codegen.model.tree.CgUtilMethod
    -import org.utbot.framework.codegen.model.tree.buildTestClass
    -import org.utbot.framework.codegen.model.tree.buildTestClassBody
    -import org.utbot.framework.codegen.model.tree.buildTestClassFile
    -import org.utbot.framework.codegen.model.visitor.importUtilMethodDependencies
    -import org.utbot.framework.plugin.api.ClassId
    -import org.utbot.framework.plugin.api.ExecutableId
    -import org.utbot.framework.plugin.api.MethodId
    -import org.utbot.framework.plugin.api.UtExecutionSuccess
    -import org.utbot.framework.plugin.api.UtMethodTestSet
    -import org.utbot.framework.plugin.api.util.description
    -import org.utbot.framework.plugin.api.util.humanReadableName
    -
    -internal class CgTestClassConstructor(val context: CgContext) :
    -    CgContextOwner by context,
    -    CgStatementConstructor by getStatementConstructorBy(context) {
    -
    -    init {
    -        clearContextRelatedStorage()
    -    }
    -
    -    private val methodConstructor = getMethodConstructorBy(context)
    -    private val nameGenerator = getNameGeneratorBy(context)
    -    private val testFrameworkManager = getTestFrameworkManagerBy(context)
    -
    -    private val testsGenerationReport: TestsGenerationReport = TestsGenerationReport()
    -
    -    /**
    -     * Given a testClass model  constructs CgTestClass
    -     */
    -    fun construct(testClassModel: TestClassModel): CgTestClassFile {
    -        return buildTestClassFile {
    -            this.declaredClass = withTestClassScope { constructTestClass(testClassModel) }
    -            imports += context.collectedImports
    -            testsGenerationReport = this@CgTestClassConstructor.testsGenerationReport
    -        }
    -    }
    -
    -    private fun constructTestClass(testClassModel: TestClassModel): CgTestClass {
    -        return buildTestClass {
    -            id = currentTestClass
    -
    -            if (currentTestClass != outerMostTestClass) {
    -                isNested = true
    -                isStatic = testFramework.nestedClassesShouldBeStatic
    -                testFrameworkManager.annotationForNestedClasses?.let {
    -                    currentTestClassContext.collectedTestClassAnnotations += it
    -                }
    -            }
    -            if (testClassModel.nestedClasses.isNotEmpty()) {
    -                testFrameworkManager.annotationForOuterClasses?.let {
    -                    currentTestClassContext.collectedTestClassAnnotations += it
    -                }
    -            }
    -
    -            body = buildTestClassBody {
    -                for (nestedClass in testClassModel.nestedClasses) {
    -                    nestedClassRegions += CgSimpleRegion(
    -                        "Tests for ${nestedClass.classUnderTest.simpleName}",
    -                        listOf(
    -                            withNestedClassScope(nestedClass) { constructTestClass(nestedClass) }
    -                        )
    -                    )
    -                }
    -
    -                for (testSet in testClassModel.methodTestSets) {
    -                    updateCurrentExecutable(testSet.executableId)
    -                    val currentMethodUnderTestRegions = constructTestSet(testSet) ?: continue
    -                    val executableUnderTestCluster = CgExecutableUnderTestCluster(
    -                        "Test suites for executable $currentExecutable",
    -                        currentMethodUnderTestRegions
    -                    )
    -                    testMethodRegions += executableUnderTestCluster
    -                }
    -
    -                val currentTestClassDataProviderMethods = currentTestClassContext.cgDataProviderMethods
    -                if (currentTestClassDataProviderMethods.isNotEmpty()) {
    -                    staticDeclarationRegions += CgStaticsRegion("Data providers", currentTestClassDataProviderMethods)
    -                }
    -
    -                if (currentTestClass == outerMostTestClass) {
    -                    val utilEntities = collectUtilEntities()
    -                    // If utilMethodProvider is TestClassUtilMethodProvider, then util entities should be declared
    -                    // in the test class. Otherwise, util entities will be located elsewhere (e.g. another class).
    -                    if (utilMethodProvider is TestClassUtilMethodProvider && utilEntities.isNotEmpty()) {
    -                        staticDeclarationRegions += CgStaticsRegion("Util methods", utilEntities)
    -                    }
    -                }
    -            }
    -            // It is important that annotations, superclass and interfaces assignment is run after
    -            // all methods are generated so that all necessary info is already present in the context
    -            with (currentTestClassContext) {
    -                annotations += collectedTestClassAnnotations
    -                superclass = testClassSuperclass
    -                interfaces += collectedTestClassInterfaces
    -            }
    -        }
    -    }
    -
    -    private fun constructTestSet(testSet: CgMethodTestSet): List>? {
    -        if (testSet.executions.isEmpty()) {
    -            return null
    -        }
    -
    -        successfulExecutionsModels = testSet
    -            .executions
    -            .filter { it.result is UtExecutionSuccess }
    -            .map { (it.result as UtExecutionSuccess).model }
    -
    -        val (methodUnderTest, _, _, clustersInfo) = testSet
    -        val regions = mutableListOf>()
    -        val requiredFields = mutableListOf()
    -
    -        when (context.parametrizedTestSource) {
    -            ParametrizedTestSource.DO_NOT_PARAMETRIZE -> {
    -                for ((clusterSummary, executionIndices) in clustersInfo) {
    -                    val currentTestCaseTestMethods = mutableListOf()
    -                    emptyLineIfNeeded()
    -                    for (i in executionIndices) {
    -                        runCatching {
    -                            currentTestCaseTestMethods += methodConstructor.createTestMethod(methodUnderTest, testSet.executions[i])
    -                        }.onFailure { e -> processFailure(testSet, e) }
    -                    }
    -                    val clusterHeader = clusterSummary?.header
    -                    val clusterContent = clusterSummary?.content
    -                        ?.split('\n')
    -                        ?.let { CgTripleSlashMultilineComment(it) }
    -                    regions += CgTestMethodCluster(clusterHeader, clusterContent, currentTestCaseTestMethods)
    -
    -                    testsGenerationReport.addTestsByType(testSet, currentTestCaseTestMethods)
    -                }
    -            }
    -            ParametrizedTestSource.PARAMETRIZE -> {
    -                for (splitByExecutionTestSet in testSet.splitExecutionsByResult()) {
    -                    for (splitByChangedStaticsTestSet in splitByExecutionTestSet.splitExecutionsByChangedStatics()) {
    -                        createParametrizedTestAndDataProvider(
    -                            splitByChangedStaticsTestSet,
    -                            requiredFields,
    -                            regions,
    -                            methodUnderTest
    -                        )
    -                    }
    -                }
    -            }
    -        }
    -
    -        val errors = testSet.allErrors
    -        if (errors.isNotEmpty()) {
    -            regions += methodConstructor.errorMethod(testSet.executableId, errors)
    -            testsGenerationReport.addMethodErrors(testSet, errors)
    -        }
    -
    -        return regions
    -    }
    -
    -    private fun processFailure(testSet: CgMethodTestSet, failure: Throwable) {
    -        codeGenerationErrors
    -            .getOrPut(testSet) { mutableMapOf() }
    -            .merge(failure.description, 1, Int::plus)
    -    }
    -
    -    private fun createParametrizedTestAndDataProvider(
    -        testSet: CgMethodTestSet,
    -        requiredFields: MutableList,
    -        regions: MutableList>,
    -        methodUnderTest: ExecutableId,
    -    ) {
    -        runCatching {
    -            val dataProviderMethodName = nameGenerator.dataProviderMethodNameFor(testSet.executableId)
    -
    -            val parameterizedTestMethod =
    -                methodConstructor.createParameterizedTestMethod(testSet, dataProviderMethodName)
    -
    -            requiredFields += parameterizedTestMethod.requiredFields
    -
    -            testFrameworkManager.addDataProvider(
    -                methodConstructor.createParameterizedTestDataProvider(testSet, dataProviderMethodName)
    -            )
    -
    -            regions += CgSimpleRegion(
    -                "Parameterized test for method ${methodUnderTest.humanReadableName}",
    -                listOf(parameterizedTestMethod),
    -            )
    -        }.onFailure { error -> processFailure(testSet, error) }
    -    }
    -
    -    /**
    -     * This method collects a list of util entities (methods and classes) needed by the class.
    -     * By the end of the test method generation [requiredUtilMethods] may not contain all the needed.
    -     * That's because some util methods may not be directly used in tests, but they may be used from other util methods.
    -     * We define such method dependencies in [MethodId.methodDependencies].
    -     *
    -     * Once all dependencies are collected, required methods are added back to [requiredUtilMethods],
    -     * because during the work of this method they are being removed from this list, so we have to put them back in.
    -     *
    -     * Also, some util methods may use some classes that also need to be generated.
    -     * That is why we collect information about required classes using [MethodId.classDependencies].
    -     *
    -     * @return a list of [CgUtilEntity] representing required util methods and classes (including their own dependencies).
    -     */
    -    private fun collectUtilEntities(): List {
    -        val utilMethods = mutableListOf()
    -        // Some util methods depend on other util methods or some auxiliary classes.
    -        // Using this loop we make sure that all the util method dependencies are taken into account.
    -        val requiredClasses = mutableSetOf()
    -        while (requiredUtilMethods.isNotEmpty()) {
    -            val method = requiredUtilMethods.first()
    -            requiredUtilMethods.remove(method)
    -            if (method.name !in existingMethodNames) {
    -                utilMethods += CgUtilMethod(method)
    -                // we only need imports from util methods if these util methods are declared in the test class
    -                if (utilMethodProvider is TestClassUtilMethodProvider) {
    -                    importUtilMethodDependencies(method)
    -                }
    -                existingMethodNames += method.name
    -                requiredUtilMethods += method.methodDependencies()
    -                requiredClasses += method.classDependencies()
    -            }
    -        }
    -        // Collect all util methods back into requiredUtilMethods.
    -        // Now there will also be util methods that weren't present in requiredUtilMethods at first,
    -        // but were needed for the present util methods to work.
    -        requiredUtilMethods += utilMethods.map { method -> method.id }
    -
    -        val auxiliaryClasses = requiredClasses.map { CgAuxiliaryClass(it) }
    -
    -        return utilMethods + auxiliaryClasses
    -    }
    -
    -    /**
    -     * If @receiver is an util method, then returns a list of util method ids that @receiver depends on
    -     * Otherwise, an empty list is returned
    -     */
    -    private fun MethodId.methodDependencies(): List = when (this) {
    -        createInstance -> listOf(getUnsafeInstance)
    -        deepEquals -> listOf(arraysDeepEquals, iterablesDeepEquals, streamsDeepEquals, mapsDeepEquals, hasCustomEquals)
    -        arraysDeepEquals, iterablesDeepEquals, streamsDeepEquals, mapsDeepEquals -> listOf(deepEquals)
    -        buildLambda, buildStaticLambda -> listOf(
    -            getLookupIn, getSingleAbstractMethod, getLambdaMethod,
    -            getLambdaCapturedArgumentTypes, getInstantiatedMethodType, getLambdaCapturedArgumentValues
    -        )
    -        else -> emptyList()
    -    }
    -
    -    /**
    -     * If @receiver is an util method, then returns a list of auxiliary class ids that @receiver depends on.
    -     * Otherwise, an empty list is returned.
    -     */
    -    private fun MethodId.classDependencies(): List = when (this) {
    -        buildLambda, buildStaticLambda -> listOf(capturedArgumentClass)
    -        else -> emptyList()
    -    }
    -
    -    /**
    -     * Engine errors + codegen errors for a given [UtMethodTestSet]
    -     */
    -    private val CgMethodTestSet.allErrors: Map
    -        get() = errors + codeGenerationErrors.getOrDefault(this, mapOf())
    -
    -    internal object CgComponents {
    -        /**
    -         * Clears all stored data for current [CgContext].
    -         * As far as context is created per class under test,
    -         * no related data is required after it's processing.
    -         */
    -        fun clearContextRelatedStorage() {
    -            nameGenerators.clear()
    -            statementConstructors.clear()
    -            callableAccessManagers.clear()
    -            testFrameworkManagers.clear()
    -            mockFrameworkManagers.clear()
    -            variableConstructors.clear()
    -            methodConstructors.clear()
    -        }
    -
    -        private val nameGenerators: MutableMap = mutableMapOf()
    -        private val statementConstructors: MutableMap = mutableMapOf()
    -        private val callableAccessManagers: MutableMap = mutableMapOf()
    -        private val testFrameworkManagers: MutableMap = mutableMapOf()
    -        private val mockFrameworkManagers: MutableMap = mutableMapOf()
    -
    -        private val variableConstructors: MutableMap = mutableMapOf()
    -        private val methodConstructors: MutableMap = mutableMapOf()
    -
    -        fun getNameGeneratorBy(context: CgContext) = nameGenerators.getOrPut(context) { CgNameGeneratorImpl(context) }
    -        fun getCallableAccessManagerBy(context: CgContext) = callableAccessManagers.getOrPut(context) { CgCallableAccessManagerImpl(context) }
    -        fun getStatementConstructorBy(context: CgContext) = statementConstructors.getOrPut(context) { CgStatementConstructorImpl(context) }
    -
    -        fun getTestFrameworkManagerBy(context: CgContext) = when (context.testFramework) {
    -            is Junit4 -> testFrameworkManagers.getOrPut(context) { Junit4Manager(context) }
    -            is Junit5 -> testFrameworkManagers.getOrPut(context) { Junit5Manager(context) }
    -            is TestNg -> testFrameworkManagers.getOrPut(context) { TestNgManager(context) }
    -        }
    -
    -        fun getMockFrameworkManagerBy(context: CgContext) = mockFrameworkManagers.getOrPut(context) { MockFrameworkManager(context) }
    -        fun getVariableConstructorBy(context: CgContext) = variableConstructors.getOrPut(context) { CgVariableConstructor(context) }
    -        fun getMethodConstructorBy(context: CgContext) = methodConstructors.getOrPut(context) { CgMethodConstructor(context) }
    -    }
    -}
    -
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgUtilClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgUtilClassConstructor.kt
    deleted file mode 100644
    index fa0d27fed6..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgUtilClassConstructor.kt
    +++ /dev/null
    @@ -1,34 +0,0 @@
    -package org.utbot.framework.codegen.model.constructor.tree
    -
    -import org.utbot.framework.codegen.model.CodeGenerator
    -import org.utbot.framework.codegen.model.UtilClassKind
    -import org.utbot.framework.codegen.model.constructor.builtin.utUtilsClassId
    -import org.utbot.framework.codegen.model.tree.CgAuxiliaryClass
    -import org.utbot.framework.codegen.model.tree.CgRegularClassFile
    -import org.utbot.framework.codegen.model.tree.CgUtilMethod
    -import org.utbot.framework.codegen.model.tree.buildRegularClass
    -import org.utbot.framework.codegen.model.tree.buildRegularClassBody
    -import org.utbot.framework.codegen.model.tree.buildRegularClassFile
    -
    -/**
    - * This class is used to construct a file containing an util class UtUtils.
    - * The util class is constructed when the argument `generateUtilClassFile` in the [CodeGenerator] is true.
    - */
    -internal object CgUtilClassConstructor {
    -    fun constructUtilsClassFile(utilClassKind: UtilClassKind): CgRegularClassFile {
    -        val utilMethodProvider = utilClassKind.utilMethodProvider
    -        return buildRegularClassFile {
    -            // imports are empty, because we use fully qualified classes and static methods,
    -            // so they will be imported once IDEA reformatting action has worked
    -            declaredClass = buildRegularClass {
    -                id = utUtilsClassId
    -                body = buildRegularClassBody {
    -                    content += utilClassKind.utilClassVersionComment
    -                    content += utilClassKind.utilClassKindComment
    -                    content += utilMethodProvider.utilMethodIds.map { CgUtilMethod(it) }
    -                    content += CgAuxiliaryClass(utilMethodProvider.capturedArgumentClassId)
    -                }
    -            }
    -        }
    -    }
    -}
    \ No newline at end of file
    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
    deleted file mode 100644
    index 6f13d036ba..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt
    +++ /dev/null
    @@ -1,542 +0,0 @@
    -package org.utbot.framework.codegen.model.constructor.tree
    -
    -import org.utbot.common.isStatic
    -import org.utbot.framework.codegen.model.constructor.builtin.forName
    -import org.utbot.framework.codegen.model.constructor.builtin.setArrayElement
    -import org.utbot.framework.codegen.model.constructor.context.CgContext
    -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getCallableAccessManagerBy
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getMockFrameworkManagerBy
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getNameGeneratorBy
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getStatementConstructorBy
    -import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor
    -import org.utbot.framework.codegen.model.constructor.util.MAX_ARRAY_INITIALIZER_SIZE
    -import org.utbot.framework.codegen.model.constructor.util.arrayInitializer
    -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.CgAssignment
    -import org.utbot.framework.codegen.model.tree.CgDeclaration
    -import org.utbot.framework.codegen.model.tree.CgEnumConstantAccess
    -import org.utbot.framework.codegen.model.tree.CgExecutableCall
    -import org.utbot.framework.codegen.model.tree.CgExpression
    -import org.utbot.framework.codegen.model.tree.CgFieldAccess
    -import org.utbot.framework.codegen.model.tree.CgGetJavaClass
    -import org.utbot.framework.codegen.model.tree.CgLiteral
    -import org.utbot.framework.codegen.model.tree.CgMethodCall
    -import org.utbot.framework.codegen.model.tree.CgStatement
    -import org.utbot.framework.codegen.model.tree.CgStaticFieldAccess
    -import org.utbot.framework.codegen.model.tree.CgValue
    -import org.utbot.framework.codegen.model.tree.CgVariable
    -import org.utbot.framework.codegen.model.util.at
    -import org.utbot.framework.codegen.model.util.canBeSetFrom
    -import org.utbot.framework.codegen.model.util.fieldThatIsGotWith
    -import org.utbot.framework.codegen.model.util.fieldThatIsSetWith
    -import org.utbot.framework.codegen.model.util.inc
    -import org.utbot.framework.codegen.model.util.isAccessibleFrom
    -import org.utbot.framework.codegen.model.util.lessThan
    -import org.utbot.framework.codegen.model.util.nullLiteral
    -import org.utbot.framework.codegen.model.util.resolve
    -import org.utbot.framework.plugin.api.BuiltinClassId
    -import org.utbot.framework.plugin.api.ClassId
    -import org.utbot.framework.plugin.api.CodegenLanguage
    -import org.utbot.framework.plugin.api.ConstructorId
    -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.UtClassRefModel
    -import org.utbot.framework.plugin.api.UtCompositeModel
    -import org.utbot.framework.plugin.api.UtDirectSetFieldModel
    -import org.utbot.framework.plugin.api.UtEnumConstantModel
    -import org.utbot.framework.plugin.api.UtExecutableCallModel
    -import org.utbot.framework.plugin.api.UtLambdaModel
    -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.UtReferenceModel
    -import org.utbot.framework.plugin.api.UtVoidModel
    -import org.utbot.framework.plugin.api.util.classClassId
    -import org.utbot.framework.plugin.api.util.defaultValueModel
    -import org.utbot.framework.plugin.api.util.jField
    -import org.utbot.framework.plugin.api.util.findFieldByIdOrNull
    -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.isPrimitiveWrapperOrString
    -import org.utbot.framework.plugin.api.util.isStatic
    -import org.utbot.framework.plugin.api.util.stringClassId
    -import org.utbot.framework.plugin.api.util.supertypeOfAnonymousClass
    -import org.utbot.framework.plugin.api.util.wrapperByPrimitive
    -
    -/**
    - * Constructs CgValue or CgVariable given a UtModel
    - */
    -@Suppress("unused")
    -internal class CgVariableConstructor(val context: CgContext) :
    -    CgContextOwner by context,
    -    CgCallableAccessManager by getCallableAccessManagerBy(context),
    -    CgStatementConstructor by getStatementConstructorBy(context) {
    -
    -    private val nameGenerator = getNameGeneratorBy(context)
    -    private val mockFrameworkManager = getMockFrameworkManagerBy(context)
    -
    -    /**
    -     * Take already created CgValue or construct either a new [CgVariable] or new [CgLiteral] for the given model.
    -     *
    -     * Here we consider reference models and other models separately.
    -     *
    -     * It is important, because variables for reference models are constructed differently from the others.
    -     * The difference is that the reference model variables are put into [valueByModel] cache
    -     * not after the whole object is set up, but right after the variable has been initialized.
    -     * For example, when we do `A a = new A()` we already have the variable, and it is stored in [valueByModel].
    -     * After that, we set all the necessary fields and call all the necessary methods.
    -     *
    -     * On the other hand, non-reference model variables are put into [valueByModel] after they have been fully set up.
    -     *
    -     * The point of this early caching of reference model variables is that classes may be recursive.
    -     * For example, we may want to create a looped object: `a.value = a`.
    -     * If we did not cache the variable `a` on its instantiation, then we would try to create `a` from its model
    -     * from scratch. Since this object is recursively pointing to itself, this process would lead
    -     * to a stack overflow. Specifically to avoid this, we cache reference model variables right after
    -     * their instantiation.
    -     *
    -     * We use [valueByModelId] for [UtReferenceModel] by id to not create new variable in case state before
    -     * was not transformed.
    -     */
    -    fun getOrCreateVariable(model: UtModel, name: String? = null): CgValue {
    -        // name could be taken from existing names, or be specified manually, or be created from generator
    -        val baseName = name ?: nameGenerator.nameFrom(model.classId)
    -        return if (model is UtReferenceModel) valueByModelId.getOrPut(model.id) {
    -            when (model) {
    -                is UtCompositeModel -> constructComposite(model, baseName)
    -                is UtAssembleModel -> constructAssemble(model, baseName)
    -                is UtArrayModel -> constructArray(model, baseName)
    -                is UtEnumConstantModel -> constructEnumConstant(model, baseName)
    -                is UtClassRefModel -> constructClassRef(model, baseName)
    -                is UtLambdaModel -> constructLambda(model, baseName)
    -            }
    -        } else valueByModel.getOrPut(model) {
    -            when (model) {
    -                is UtNullModel -> nullLiteral()
    -                is UtPrimitiveModel -> CgLiteral(model.classId, model.value)
    -                is UtReferenceModel -> error("Unexpected UtReferenceModel: ${model::class}")
    -                is UtVoidModel -> error("Unexpected UtVoidModel: ${model::class}")
    -            }
    -        }
    -    }
    -
    -    private fun constructLambda(model: UtLambdaModel, baseName: String): CgVariable {
    -        val lambdaMethodId = model.lambdaMethodId
    -        val capturedValues = model.capturedValues
    -        return newVar(model.samType, baseName) {
    -            if (lambdaMethodId.isStatic) {
    -                constructStaticLambda(model, capturedValues)
    -            } else {
    -                constructLambda(model, capturedValues)
    -            }
    -        }
    -    }
    -
    -    private fun constructStaticLambda(model: UtLambdaModel, capturedValues: List): CgMethodCall {
    -        val capturedArguments = capturedValues.map {
    -            utilMethodProvider.capturedArgumentConstructorId(getClassOf(it.classId), getOrCreateVariable(it))
    -        }
    -        return utilsClassId[buildStaticLambda](
    -            getClassOf(model.samType),
    -            getClassOf(model.declaringClass),
    -            model.lambdaName,
    -            *capturedArguments.toTypedArray()
    -        )
    -    }
    -
    -    private fun constructLambda(model: UtLambdaModel, capturedValues: List): CgMethodCall {
    -        require(capturedValues.isNotEmpty()) {
    -            "Non-static lambda must capture `this` instance, so there must be at least one captured value"
    -        }
    -        val capturedThisInstance = getOrCreateVariable(capturedValues.first())
    -        val capturedArguments = capturedValues
    -            .subList(1, capturedValues.size)
    -            .map { utilMethodProvider.capturedArgumentConstructorId(getClassOf(it.classId), getOrCreateVariable(it)) }
    -        return utilsClassId[buildLambda](
    -            getClassOf(model.samType),
    -            getClassOf(model.declaringClass),
    -            model.lambdaName,
    -            capturedThisInstance,
    -            *capturedArguments.toTypedArray()
    -        )
    -    }
    -
    -    private fun constructComposite(model: UtCompositeModel, baseName: String): CgVariable {
    -        val obj = if (model.isMock) {
    -            mockFrameworkManager.createMockFor(model, baseName)
    -        } else {
    -            val modelType = model.classId
    -            val variableType = if (modelType.isAnonymous) modelType.supertypeOfAnonymousClass else modelType
    -            newVar(variableType, baseName) { utilsClassId[createInstance](model.classId.name) }
    -        }
    -
    -        valueByModelId[model.id] = obj
    -
    -        require(obj.type !is BuiltinClassId) {
    -            "Unexpected BuiltinClassId ${obj.type} found while constructing from composite model"
    -        }
    -
    -        for ((fieldId, fieldModel) in model.fields) {
    -            val field = fieldId.jField
    -            val variableForField = getOrCreateVariable(fieldModel)
    -            val fieldFromVariableSpecifiedType = obj.type.findFieldByIdOrNull(fieldId)
    -
    -            // we cannot set field directly if variable declared type does not have such field
    -            // or we cannot directly create variable for field with the specified type (it is private, for example)
    -            // Example:
    -            // Object heapByteBuffer = createInstance("java.nio.HeapByteBuffer");
    -            // branchRegisterRequest.byteBuffer = heapByteBuffer;
    -            // byteBuffer is field of type ByteBuffer and upper line is incorrect
    -            val canFieldBeDirectlySetByVariableAndFieldTypeRestrictions =
    -                fieldFromVariableSpecifiedType != null && fieldFromVariableSpecifiedType.type.id == variableForField.type
    -            if (canFieldBeDirectlySetByVariableAndFieldTypeRestrictions && fieldId.canBeSetFrom(context)) {
    -                // TODO: check if it is correct to use declaringClass of a field here
    -                val fieldAccess = if (field.isStatic) CgStaticFieldAccess(fieldId) else CgFieldAccess(obj, fieldId)
    -                fieldAccess `=` variableForField
    -            } else {
    -                // composite models must not have info about static fields, hence only non-static fields are set here
    -                +utilsClassId[setField](obj, fieldId.declaringClass.name, fieldId.name, variableForField)
    -            }
    -        }
    -        return obj
    -    }
    -
    -    private fun constructAssemble(model: UtAssembleModel, baseName: String?): CgValue {
    -        val instantiationCall = model.instantiationCall
    -        processInstantiationStatement(model, instantiationCall, baseName)
    -
    -        for (statementModel in model.modificationsChain) {
    -            when (statementModel) {
    -                is UtDirectSetFieldModel -> {
    -                    val instance = declareOrGet(statementModel.instance)
    -                    // fields here are supposed to be accessible, so we assign them directly without any checks
    -                    instance[statementModel.fieldId] `=` declareOrGet(statementModel.fieldModel)
    -                }
    -                is UtExecutableCallModel -> {
    -                    val call = createCgExecutableCallFromUtExecutableCall(statementModel)
    -                    val equivalentFieldAccess = replaceCgExecutableCallWithFieldAccessIfNeeded(call)
    -                    if (equivalentFieldAccess != null)
    -                        +equivalentFieldAccess
    -                    else
    -                        +call
    -                }
    -            }
    -        }
    -
    -        return valueByModelId.getValue(model.id)
    -    }
    -
    -    private fun processInstantiationStatement(
    -        model: UtAssembleModel,
    -        executableCall: UtExecutableCallModel,
    -        baseName: String?
    -    ) {
    -        val executable = executableCall.executable
    -        val params = executableCall.params
    -
    -        val type = when (executable) {
    -            is MethodId -> executable.returnType
    -            is ConstructorId -> executable.classId
    -        }
    -        // Don't use redundant constructors for primitives and String
    -        val initExpr = if (isPrimitiveWrapperOrString(type)) {
    -            cgLiteralForWrapper(params)
    -        } else {
    -            createCgExecutableCallFromUtExecutableCall(executableCall)
    -        }
    -        newVar(type, model, baseName) {
    -            initExpr
    -        }.also { valueByModelId[model.id] = it }
    -    }
    -
    -
    -    private fun createCgExecutableCallFromUtExecutableCall(statementModel: UtExecutableCallModel): CgExecutableCall {
    -        val executable = statementModel.executable
    -        val params = statementModel.params
    -        val cgCall = when (executable) {
    -            is MethodId -> {
    -                val caller = statementModel.instance?.let { declareOrGet(it) }
    -                val args = params.map { declareOrGet(it) }
    -                caller[executable](*args.toTypedArray())
    -            }
    -            is ConstructorId -> {
    -                val args = params.map { declareOrGet(it) }
    -                executable(*args.toTypedArray())
    -            }
    -        }
    -        return cgCall
    -    }
    -
    -    /**
    -     * If executable is getter/setter that should be syntactically replaced with field access
    -     * (e.g., getter/setter generated by Kotlin in Kotlin code), this method returns [CgStatement]
    -     * with which [call] should be replaced.
    -     *
    -     * Otherwise, returns null.
    -     */
    -    private fun replaceCgExecutableCallWithFieldAccessIfNeeded(call: CgExecutableCall): CgStatement? {
    -        when (context.codegenLanguage) {
    -            CodegenLanguage.JAVA -> return null
    -            CodegenLanguage.KOTLIN -> {
    -                if (call !is CgMethodCall)
    -                    return null
    -
    -                val caller = call.caller ?: return null
    -
    -                caller.type.fieldThatIsSetWith(call.executableId)?.let {
    -                    return CgAssignment(caller[it], call.arguments.single())
    -                }
    -                caller.type.fieldThatIsGotWith(call.executableId)?.let {
    -                    require(call.arguments.isEmpty()) {
    -                        "Method $call was detected as getter for $it, but its arguments list isn't empty"
    -                    }
    -                    return caller[it]
    -                }
    -
    -                return null
    -            }
    -        }
    -    }
    -
    -    /**
    -     * Makes a replacement of constructor call to instantiate a primitive wrapper
    -     * with direct setting of the value. The reason is that in Kotlin constructors
    -     * of primitive wrappers are private.
    -     */
    -    private fun cgLiteralForWrapper(params: List): CgLiteral {
    -        val paramModel = params.singleOrNull()
    -        require(paramModel is UtPrimitiveModel) { "Incorrect param models for primitive wrapper" }
    -
    -        val classId = wrapperByPrimitive[paramModel.classId]
    -            ?: if (paramModel.classId == stringClassId) {
    -                stringClassId
    -            } else {
    -                error("${paramModel.classId} is not a primitive wrapper or a string")
    -            }
    -
    -        return CgLiteral(classId, paramModel.value)
    -    }
    -
    -    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 allPrimitives = elementModels.all { it is UtPrimitiveModel }
    -        val allNulls = elementModels.all { it is UtNullModel }
    -        // we can use array initializer if all elements are primitives or all of them are null,
    -        // and the size of an array is not greater than the fixed maximum size
    -        val canInitWithValues = (allPrimitives || allNulls) && elementModels.size <= MAX_ARRAY_INITIALIZER_SIZE
    -
    -        val initializer = if (canInitWithValues) {
    -            val elements = elementModels.map { model ->
    -                when (model) {
    -                    is UtPrimitiveModel -> model.value.resolve()
    -                    is UtNullModel -> null.resolve()
    -                    else -> error("Non primitive or null model $model is unexpected in array initializer")
    -                }
    -            }
    -            arrayInitializer(arrayModel.classId, elementType, elements)
    -        } 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 = arrayModel[0]
    -            if (elementModel isNotDefaultValueOf elementType) {
    -                array.setArrayElement(0, getOrCreateVariable(elementModel))
    -            }
    -        } else {
    -            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)
    -                        }
    -                    }
    -
    -                    // choose all not default values
    -                    val defaultValue = if (arrayModel.constModel isDefaultValueOf elementType) {
    -                        arrayModel.constModel
    -                    } else {
    -                        elementType.defaultValueModel()
    -                    }
    -                    arrayModel.stores.entries.filter { (_, element) -> element != defaultValue }
    -                }
    -
    -            // set all values from stores manually
    -            indexedValuesFromStores
    -                .sortedBy { it.key }
    -                .forEach { (index, element) -> array.setArrayElement(index, getOrCreateVariable(element)) }
    -        }
    -
    -        return array
    -    }
    -
    -    // TODO: cannot be used now but will be useful in case of storing stores in generated code
    -    /**
    -     * Splits sorted by indices pairs of index and value from stores to continuous by index chunks
    -     * [indexedValuesFromStores] have to be sorted by key
    -     */
    -    private fun splitSettingFromStoresToForLoops(
    -        array: CgVariable,
    -        indexedValuesFromStores: List>
    -    ) {
    -        val ranges = mutableListOf()
    -
    -        var start = 0
    -        for (i in 0 until indexedValuesFromStores.lastIndex) {
    -            if (indexedValuesFromStores[i + 1].key - indexedValuesFromStores[i].key > 1) {
    -                ranges += start..i
    -                start = i + 1
    -            }
    -            if (i == indexedValuesFromStores.lastIndex - 1) {
    -                ranges += start..indexedValuesFromStores.lastIndex
    -            }
    -        }
    -
    -        for (range in ranges) {
    -            // IntRange stores end inclusively but sublist takes it exclusively
    -            setStoresRange(array, indexedValuesFromStores.subList(range.first, range.last + 1))
    -        }
    -    }
    -
    -    /**
    -     * [indexedValuesFromStores] have to be continuous sorted range
    -     */
    -    private fun setStoresRange(
    -        array: CgVariable,
    -        indexedValuesFromStores: List>
    -    ) {
    -        if (indexedValuesFromStores.size < 3) {
    -            // range is too small, better set manually
    -            indexedValuesFromStores.forEach { (index, element) ->
    -                array.setArrayElement(index, getOrCreateVariable(element))
    -            }
    -        } else {
    -            val minIndex = indexedValuesFromStores.first().key
    -            val maxIndex = indexedValuesFromStores.last().key
    -
    -            var indicesIndex = 0
    -            // we use until form of for loop so need to shift upper border
    -            basicForLoop(start = minIndex, until = maxIndex + 1) { i ->
    -                // use already sorted indices
    -                val (_, value) = indexedValuesFromStores[indicesIndex++]
    -                array.setArrayElement(i, getOrCreateVariable(value))
    -            }
    -        }
    -    }
    -
    -    private fun constructEnumConstant(model: UtEnumConstantModel, baseName: String?): CgVariable {
    -        return newVar(model.classId, baseName) {
    -            CgEnumConstantAccess(model.classId, model.value.name)
    -        }
    -    }
    -
    -    private fun constructClassRef(model: UtClassRefModel, baseName: String?): CgVariable {
    -        val classId = model.value.id
    -        val init = if (classId.isAccessibleFrom(testClassPackageName)) {
    -            CgGetJavaClass(classId)
    -        } else {
    -            classClassId[forName](classId.name)
    -        }
    -
    -        return newVar(Class::class.id, baseName) { init }
    -    }
    -
    -    /**
    -     * Either declares a new variable or gets it from context's cache
    -     * Returns the obtained variable
    -     */
    -    private fun declareOrGet(model: UtModel): CgValue = valueByModel[model] ?: getOrCreateVariable(model)
    -
    -    private fun basicForLoop(start: Any, until: Any, body: (i: CgExpression) -> Unit) {
    -        forLoop {
    -            val (i, init) = loopInitialization(intClassId, "i", start.resolve())
    -            initialization = init
    -            condition = i lessThan until.resolve()
    -            update = i.inc()
    -            statements = block { body(i) }
    -        }
    -    }
    -
    -    /**
    -     * A for-loop performing 'n' iterations starting with 0
    -     *
    -     * for (int i = 0; i < n; i++) {
    -     *     ...
    -     * }
    -     */
    -    private fun basicForLoop(until: Any, body: (i: CgExpression) -> Unit) {
    -        basicForLoop(start = 0, until, body)
    -    }
    -
    -    /**
    -     * Create loop initializer expression
    -     */
    -    @Suppress("SameParameterValue")
    -    internal fun loopInitialization(
    -        variableType: ClassId,
    -        baseVariableName: String,
    -        initializer: Any?
    -    ): Pair {
    -        val declaration = CgDeclaration(variableType, baseVariableName.toVarName(), initializer.resolve())
    -        val variable = declaration.variable
    -        updateVariableScope(variable)
    -        return variable to declaration
    -    }
    -
    -    /**
    -     * @receiver must represent a variable containing an array value.
    -     * If an array was created with reflection, then the variable is of [Object] type.
    -     * Otherwise, the variable is of the actual array type.
    -     *
    -     * Both cases are considered here.
    -     * If the variable is [Object], we use reflection method to set an element.
    -     * Otherwise, we set an element directly.
    -     */
    -    private fun CgVariable.setArrayElement(index: Any, value: CgValue) {
    -        val i = index.resolve()
    -        // we have to use reflection if we cannot easily cast array element to array type
    -        // (in case array does not have array type (maybe just object) or element is private class)
    -        if (!type.isArray || (type != value.type && !value.type.isAccessibleFrom(testClassPackageName))) {
    -            +java.lang.reflect.Array::class.id[setArrayElement](this, i, value)
    -        } else {
    -            val arrayElement = if (type == value.type) {
    -                value
    -            } else {
    -                typeCast(type.elementClassId!!, value, isSafetyCast = true)
    -            }
    -
    -            this.at(i) `=` arrayElement
    -        }
    -    }
    -
    -    internal fun constructVarName(baseName: String, isMock: Boolean = false): String =
    -        nameGenerator.variableName(baseName, isMock)
    -
    -    private fun String.toVarName(): String = nameGenerator.variableName(this)
    -
    -}
    \ No newline at end of file
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt
    deleted file mode 100644
    index 2aaa63a50c..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt
    +++ /dev/null
    @@ -1,386 +0,0 @@
    -package org.utbot.framework.codegen.model.constructor.tree
    -
    -import org.utbot.framework.codegen.MockitoStaticMocking
    -import org.utbot.framework.codegen.NoStaticMocking
    -import org.utbot.framework.codegen.model.constructor.builtin.any
    -import org.utbot.framework.codegen.model.constructor.builtin.anyBoolean
    -import org.utbot.framework.codegen.model.constructor.builtin.anyByte
    -import org.utbot.framework.codegen.model.constructor.builtin.anyChar
    -import org.utbot.framework.codegen.model.constructor.builtin.anyDouble
    -import org.utbot.framework.codegen.model.constructor.builtin.anyFloat
    -import org.utbot.framework.codegen.model.constructor.builtin.anyInt
    -import org.utbot.framework.codegen.model.constructor.builtin.anyLong
    -import org.utbot.framework.codegen.model.constructor.builtin.anyOfClass
    -import org.utbot.framework.codegen.model.constructor.builtin.anyShort
    -import org.utbot.framework.codegen.model.constructor.builtin.argumentMatchersClassId
    -import org.utbot.framework.codegen.model.constructor.builtin.mockMethodId
    -import org.utbot.framework.codegen.model.constructor.builtin.mockedConstructionContextClassId
    -import org.utbot.framework.codegen.model.constructor.builtin.mockitoClassId
    -import org.utbot.framework.codegen.model.constructor.builtin.thenReturnMethodId
    -import org.utbot.framework.codegen.model.constructor.builtin.whenMethodId
    -import org.utbot.framework.codegen.model.constructor.context.CgContext
    -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getVariableConstructorBy
    -import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor
    -import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructorImpl
    -import org.utbot.framework.codegen.model.constructor.util.hasAmbiguousOverloadsOf
    -import org.utbot.framework.codegen.model.tree.CgAnonymousFunction
    -import org.utbot.framework.codegen.model.tree.CgAssignment
    -import org.utbot.framework.codegen.model.tree.CgClassId
    -import org.utbot.framework.codegen.model.tree.CgConstructorCall
    -import org.utbot.framework.codegen.model.tree.CgDeclaration
    -import org.utbot.framework.codegen.model.tree.CgExecutableCall
    -import org.utbot.framework.codegen.model.tree.CgExpression
    -import org.utbot.framework.codegen.model.tree.CgLiteral
    -import org.utbot.framework.codegen.model.tree.CgMethodCall
    -import org.utbot.framework.codegen.model.tree.CgParameterDeclaration
    -import org.utbot.framework.codegen.model.tree.CgRunnable
    -import org.utbot.framework.codegen.model.tree.CgStatement
    -import org.utbot.framework.codegen.model.tree.CgStatementExecutableCall
    -import org.utbot.framework.codegen.model.tree.CgStaticRunnable
    -import org.utbot.framework.codegen.model.tree.CgSwitchCase
    -import org.utbot.framework.codegen.model.tree.CgSwitchCaseLabel
    -import org.utbot.framework.codegen.model.tree.CgValue
    -import org.utbot.framework.codegen.model.tree.CgVariable
    -import org.utbot.framework.codegen.model.util.isAccessibleFrom
    -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.TypeParameters
    -import org.utbot.framework.plugin.api.UtCompositeModel
    -import org.utbot.framework.plugin.api.UtModel
    -import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation
    -import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation
    -import org.utbot.framework.plugin.api.util.atomicIntegerClassId
    -import org.utbot.framework.plugin.api.util.atomicIntegerGet
    -import org.utbot.framework.plugin.api.util.atomicIntegerGetAndIncrement
    -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.voidClassId
    -
    -internal abstract class CgVariableConstructorComponent(val context: CgContext) :
    -        CgContextOwner by context,
    -        CgCallableAccessManager by CgCallableAccessManagerImpl(context),
    -        CgStatementConstructor by CgStatementConstructorImpl(context) {
    -
    -    val variableConstructor: CgVariableConstructor by lazy { getVariableConstructorBy(context) }
    -
    -    fun mockitoArgumentMatchersFor(executable: ExecutableId): Array =
    -            executable.parameters.map {
    -                val matcher = it.mockitoAnyMatcher(executable.classId.hasAmbiguousOverloadsOf(executable))
    -                if (matcher != anyOfClass) argumentMatchersClassId[matcher]() else matchByClass(it)
    -            }.toTypedArray()
    -
    -    /**
    -     * Clears all resources required for [currentExecution].
    -     */
    -    open fun clearExecutionResources() {
    -        // do nothing by default
    -    }
    -
    -    // TODO: implement other similar methods like thenThrow, etc.
    -    fun CgMethodCall.thenReturn(returnType: ClassId, vararg args: CgValue) {
    -        val castedArgs = args
    -            // guard args to reuse typecast creation logic
    -            .map { if (it.type == returnType) it else guardExpression(returnType, it).expression }
    -            .toTypedArray()
    -
    -        +this[thenReturnMethodId](*castedArgs)
    -    }
    -
    -    fun ClassId.mockitoAnyMatcher(withExplicitClass: Boolean): MethodId =
    -            when (this) {
    -                byteClassId -> anyByte
    -                charClassId -> anyChar
    -                shortClassId -> anyShort
    -                intClassId -> anyInt
    -                longClassId -> anyLong
    -                floatClassId -> anyFloat
    -                doubleClassId -> anyDouble
    -                booleanClassId -> anyBoolean
    -                // we cannot match by string here
    -                // because anyString accepts only non-nullable strings but argument could be null
    -                else -> if (withExplicitClass) anyOfClass else any
    -            }
    -
    -    private fun matchByClass(id: ClassId): CgMethodCall =
    -            argumentMatchersClassId[anyOfClass](getClassOf(id))
    -}
    -
    -internal class MockFrameworkManager(context: CgContext) : CgVariableConstructorComponent(context) {
    -
    -    private val objectMocker = MockitoMocker(context)
    -    private val staticMocker = when (context.staticsMocking) {
    -        is NoStaticMocking -> null
    -        is MockitoStaticMocking -> MockitoStaticMocker(context, objectMocker)
    -    }
    -
    -    /**
    -     * Precondition: in the given [model] flag [UtCompositeModel.isMock] must be true.
    -     * @return a variable representing a created mock object.
    -     */
    -    fun createMockFor(model: UtCompositeModel, baseName: String): CgVariable = withMockFramework {
    -        require(model.isMock) { "Mock model is expected in MockObjectConstructor" }
    -
    -        objectMocker.createMock(model, baseName)
    -    }
    -
    -    fun mockNewInstance(mock: UtNewInstanceInstrumentation) {
    -        staticMocker?.mockNewInstance(mock)
    -    }
    -
    -    fun mockStaticMethodsOfClass(classId: ClassId, methodMocks: List) {
    -        staticMocker?.mockStaticMethodsOfClass(classId, methodMocks)
    -    }
    -
    -    override fun clearExecutionResources() {
    -        staticMocker?.clearExecutionResources()
    -    }
    -
    -    internal fun getAndClearMethodResources(): List? =
    -        if (staticMocker is MockitoStaticMocker) staticMocker.copyAndClearMockResources() else null
    -}
    -
    -private abstract class ObjectMocker(
    -        context: CgContext
    -) : CgVariableConstructorComponent(context) {
    -    abstract fun createMock(model: UtCompositeModel, baseName: String): CgVariable
    -
    -    abstract fun mock(clazz: CgExpression): CgMethodCall
    -
    -    abstract fun `when`(call: CgExecutableCall): CgMethodCall
    -}
    -
    -private abstract class StaticMocker(
    -        context: CgContext
    -) : CgVariableConstructorComponent(context) {
    -    abstract fun mockNewInstance(mock: UtNewInstanceInstrumentation)
    -    abstract fun mockStaticMethodsOfClass(classId: ClassId, methodMocks: List)
    -}
    -
    -private class MockitoMocker(context: CgContext) : ObjectMocker(context) {
    -    override fun createMock(model: UtCompositeModel, baseName: String): CgVariable {
    -        // create mock object
    -        val modelClass = getClassOf(model.classId)
    -        val mockObject = newVar(model.classId, baseName = baseName, isMock = true) { mock(modelClass) }
    -
    -        for ((executable, values) in model.mocks) {
    -            // void method
    -            if (executable.returnType == voidClassId) {
    -                // void methods on mocks do nothing by default
    -                continue
    -            }
    -
    -            when (executable) {
    -                is MethodId -> {
    -                    if (executable.parameters.any { !it.isAccessibleFrom(testClassPackageName) }) {
    -                        error("Cannot mock method $executable with not accessible parameters" )
    -                    }
    -
    -                    val matchers = mockitoArgumentMatchersFor(executable)
    -                    if (!executable.isAccessibleFrom(testClassPackageName)) {
    -                        error("Cannot mock method $executable as it is not accessible from package $testClassPackageName")
    -                    }
    -
    -                    val results = values.map { variableConstructor.getOrCreateVariable(it) }.toTypedArray()
    -                    `when`(mockObject[executable](*matchers)).thenReturn(executable.returnType, *results)
    -                }
    -                else -> error("ConstructorId was not expected to appear in simple mocker but got $executable")
    -            }
    -        }
    -
    -        return mockObject
    -    }
    -
    -    override fun mock(clazz: CgExpression): CgMethodCall =
    -            mockitoClassId[mockMethodId](clazz)
    -
    -    override fun `when`(call: CgExecutableCall): CgMethodCall =
    -            mockitoClassId[whenMethodId](call)
    -}
    -
    -private class MockitoStaticMocker(context: CgContext, private val mocker: ObjectMocker) : StaticMocker(context) {
    -    private val resources = mutableListOf()
    -    private val mockedStaticForMethods = mutableMapOf()
    -    private val mockedStaticConstructions = mutableSetOf()
    -
    -    override fun mockNewInstance(mock: UtNewInstanceInstrumentation) {
    -        val classId = mock.classId
    -        if (classId in mockedStaticConstructions) return
    -
    -        val mockClassCounter = CgDeclaration(
    -            atomicIntegerClassId,
    -            variableConstructor.constructVarName(MOCK_CLASS_COUNTER_NAME),
    -            CgConstructorCall(ConstructorId(atomicIntegerClassId, emptyList()), emptyList())
    -        )
    -        +mockClassCounter
    -
    -        val mocksExecutablesAnswers = mock
    -            .instances
    -            .filterIsInstance()
    -            .filter { it.isMock }
    -            .map { it.mocks }
    -
    -        val modelClass = getClassOf(classId)
    -
    -        val mockConstructionInitializer = mockConstruction(
    -            modelClass,
    -            classId,
    -            mocksExecutablesAnswers,
    -            mockClassCounter.variable
    -        )
    -        val mockedConstructionDeclaration = CgDeclaration(
    -            MockitoStaticMocking.mockedConstructionClassId,
    -            variableConstructor.constructVarName(MOCKED_CONSTRUCTION_NAME),
    -            mockConstructionInitializer
    -        )
    -        resources += mockedConstructionDeclaration
    -        +CgAssignment(mockedConstructionDeclaration.variable, mockConstructionInitializer)
    -        mockedStaticConstructions += classId
    -    }
    -
    -    override fun mockStaticMethodsOfClass(classId: ClassId, methodMocks: List) {
    -        for ((methodId, values) in methodMocks) {
    -            if (methodId.parameters.any { !it.isAccessibleFrom(testClassPackageName) }) {
    -                error("Cannot mock static method $methodId with not accessible parameters" )
    -            }
    -
    -            val matchers = mockitoArgumentMatchersFor(methodId)
    -            val mockedStaticDeclaration = getOrCreateMockStatic(classId)
    -            val mockedStaticVariable = mockedStaticDeclaration.variable
    -            val methodRunnable = if (matchers.isEmpty()) {
    -                CgStaticRunnable(type = methodId.returnType, classId, methodId)
    -            } else {
    -                CgAnonymousFunction(
    -                    type = methodId.returnType,
    -                    parameters = emptyList(),
    -                    listOf(CgStatementExecutableCall(CgMethodCall(
    -                        caller = null,
    -                        methodId,
    -                        matchers.toList()
    -                    )))
    -                )
    -            }
    -            // void method
    -            if (methodId.returnType == voidClassId) {
    -                // we do not generate additional code for void methods because they do nothing by default
    -                continue
    -            }
    -
    -            if (!methodId.isAccessibleFrom(testClassPackageName)) {
    -                error("Cannot mock static method $methodId as it is not accessible from package $testClassPackageName")
    -            }
    -
    -            val results = values.map { variableConstructor.getOrCreateVariable(it) }.toTypedArray()
    -            `when`(mockedStaticVariable, methodRunnable).thenReturn(methodId.returnType, *results)
    -        }
    -    }
    -
    -    override fun clearExecutionResources() {
    -        resources.clear()
    -        mockedStaticForMethods.clear()
    -        mockedStaticConstructions.clear()
    -    }
    -
    -    private fun getOrCreateMockStatic(classId: ClassId): CgDeclaration =
    -        mockedStaticForMethods.getOrPut(classId) {
    -            val modelClass = getClassOf(classId)
    -            val classMockStaticCall = mockStatic(modelClass)
    -            val mockedStaticVariableName = variableConstructor.constructVarName(MOCKED_STATIC_NAME)
    -            CgDeclaration(
    -                MockitoStaticMocking.mockedStaticClassId,
    -                mockedStaticVariableName,
    -                classMockStaticCall
    -            ).also {
    -                resources += it
    -                +CgAssignment(it.variable, classMockStaticCall)
    -            }
    -        }
    -
    -    private fun mockConstruction(
    -        clazz: CgExpression,
    -        classId: ClassId,
    -        mocksWhenAnswers: List>>,
    -        mockClassCounter: CgVariable
    -    ): CgMethodCall {
    -        val mockParameter = variableConstructor.declareParameter(
    -            classId,
    -            variableConstructor.constructVarName(classId.simpleName, isMock = true)
    -        )
    -        val contextParameter = variableConstructor.declareParameter(
    -            mockedConstructionContextClassId,
    -            variableConstructor.constructVarName("context")
    -        )
    -
    -        val caseLabels = mutableListOf()
    -        for ((index, mockWhenAnswers) in mocksWhenAnswers.withIndex()) {
    -            val statements = mutableListOf()
    -            for ((executable, values) in mockWhenAnswers) {
    -                if (executable.returnType == voidClassId) continue
    -
    -                when (executable) {
    -                    is MethodId -> {
    -                        val matchers = mockitoArgumentMatchersFor(executable)
    -                        val results = values.map { variableConstructor.getOrCreateVariable(it) }.toTypedArray()
    -                        statements += CgStatementExecutableCall(
    -                            mocker.`when`(mockParameter[executable](*matchers))[thenReturnMethodId](*results)
    -                        )
    -                    }
    -                    else -> error("Expected MethodId but got ConstructorId $executable")
    -                }
    -            }
    -
    -            caseLabels += CgSwitchCaseLabel(CgLiteral(intClassId, index), statements)
    -        }
    -
    -        val switchCase = CgSwitchCase(mockClassCounter[atomicIntegerGet](), caseLabels)
    -
    -        val answersBlock = CgAnonymousFunction(
    -            voidClassId,
    -            listOf(mockParameter, contextParameter).map { CgParameterDeclaration(it, isVararg = false) },
    -            listOf(switchCase, CgStatementExecutableCall(mockClassCounter[atomicIntegerGetAndIncrement]()))
    -        )
    -
    -        return mockitoClassId[MockitoStaticMocking.mockConstructionMethodId](clazz, answersBlock)
    -    }
    -
    -    private fun mockStatic(clazz: CgExpression): CgMethodCall =
    -        mockitoClassId[MockitoStaticMocking.mockStaticMethodId](clazz)
    -
    -    private fun `when`(
    -        mockedStatic: CgVariable,
    -        runnable: CgExpression,
    -    ): CgMethodCall {
    -        val typeParams = when (runnable) {
    -            is CgRunnable, is CgAnonymousFunction -> listOf(runnable.type)
    -            else -> error("Unsupported runnable type: $runnable")
    -        }
    -        return CgMethodCall(
    -            mockedStatic,
    -            MockitoStaticMocking.mockedStaticWhen(nullable = mockedStatic.type.isNullable),
    -            listOf(runnable),
    -            TypeParameters(typeParams),
    -        )
    -    }
    -
    -
    -    fun copyAndClearMockResources(): List? {
    -        val copiedResources = resources.toList()
    -        clearExecutionResources()
    -
    -        return copiedResources.ifEmpty { null }
    -    }
    -
    -    companion object {
    -        private const val MOCKED_CONSTRUCTION_NAME = "mockedConstruction"
    -        private const val MOCKED_STATIC_NAME = "mockedStatic"
    -        private const val MOCK_CLASS_COUNTER_NAME = "mockClassCounter"
    -    }
    -}
    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
    deleted file mode 100644
    index fcc8ee0200..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt
    +++ /dev/null
    @@ -1,555 +0,0 @@
    -package org.utbot.framework.codegen.model.constructor.tree
    -
    -import org.utbot.framework.codegen.Junit4
    -import org.utbot.framework.codegen.Junit5
    -import org.utbot.framework.codegen.TestNg
    -import org.utbot.framework.codegen.model.constructor.TestClassContext
    -import org.utbot.framework.codegen.model.constructor.builtin.forName
    -import org.utbot.framework.codegen.model.constructor.context.CgContext
    -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getCallableAccessManagerBy
    -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getStatementConstructorBy
    -import org.utbot.framework.codegen.model.constructor.util.addToListMethodId
    -import org.utbot.framework.codegen.model.constructor.util.argumentsClassId
    -import org.utbot.framework.codegen.model.constructor.util.argumentsMethodId
    -import org.utbot.framework.codegen.model.constructor.util.classCgClassId
    -import org.utbot.framework.codegen.model.constructor.util.importIfNeeded
    -import org.utbot.framework.codegen.model.constructor.util.setArgumentsArrayElement
    -import org.utbot.framework.codegen.model.tree.CgAllocateArray
    -import org.utbot.framework.codegen.model.tree.CgAnnotation
    -import org.utbot.framework.codegen.model.tree.CgEnumConstantAccess
    -import org.utbot.framework.codegen.model.tree.CgExpression
    -import org.utbot.framework.codegen.model.tree.CgGetJavaClass
    -import org.utbot.framework.codegen.model.tree.CgGetKotlinClass
    -import org.utbot.framework.codegen.model.tree.CgLiteral
    -import org.utbot.framework.codegen.model.tree.CgMethod
    -import org.utbot.framework.codegen.model.tree.CgMethodCall
    -import org.utbot.framework.codegen.model.tree.CgMultipleArgsAnnotation
    -import org.utbot.framework.codegen.model.tree.CgNamedAnnotationArgument
    -import org.utbot.framework.codegen.model.tree.CgSingleArgAnnotation
    -import org.utbot.framework.codegen.model.tree.CgValue
    -import org.utbot.framework.codegen.model.tree.CgVariable
    -import org.utbot.framework.codegen.model.util.classLiteralAnnotationArgument
    -import org.utbot.framework.codegen.model.util.isAccessibleFrom
    -import org.utbot.framework.codegen.model.util.resolve
    -import org.utbot.framework.codegen.model.util.stringLiteral
    -import org.utbot.framework.plugin.api.BuiltinMethodId
    -import org.utbot.framework.plugin.api.ClassId
    -import org.utbot.framework.plugin.api.CodegenLanguage
    -import org.utbot.framework.plugin.api.ConstructorId
    -import org.utbot.framework.plugin.api.util.booleanArrayClassId
    -import org.utbot.framework.plugin.api.util.byteArrayClassId
    -import org.utbot.framework.plugin.api.util.charArrayClassId
    -import org.utbot.framework.plugin.api.util.doubleArrayClassId
    -import org.utbot.framework.plugin.api.util.floatArrayClassId
    -import org.utbot.framework.plugin.api.util.id
    -import org.utbot.framework.plugin.api.util.intArrayClassId
    -import org.utbot.framework.plugin.api.util.longArrayClassId
    -import org.utbot.framework.plugin.api.util.shortArrayClassId
    -import org.utbot.framework.plugin.api.util.stringClassId
    -import java.util.concurrent.TimeUnit
    -
    -@Suppress("MemberVisibilityCanBePrivate")
    -internal abstract class TestFrameworkManager(val context: CgContext)
    -    : CgContextOwner by context,
    -        CgCallableAccessManager by getCallableAccessManagerBy(context) {
    -
    -    val assertions = context.testFramework.assertionsClass
    -
    -    val assertEquals = context.testFramework.assertEquals
    -    val assertFloatEquals = context.testFramework.assertFloatEquals
    -    val assertDoubleEquals = context.testFramework.assertDoubleEquals
    -
    -    val assertNull = context.testFramework.assertNull
    -    val assertNotNull = context.testFramework.assertNotNull
    -    val assertTrue = context.testFramework.assertTrue
    -    val assertFalse = context.testFramework.assertFalse
    -
    -    val assertArrayEquals = context.testFramework.assertArrayEquals
    -    val assertBooleanArrayEquals = context.testFramework.assertBooleanArrayEquals
    -    val assertByteArrayEquals = context.testFramework.assertByteArrayEquals
    -    val assertCharArrayEquals = context.testFramework.assertCharArrayEquals
    -    val assertShortArrayEquals = context.testFramework.assertShortArrayEquals
    -    val assertIntArrayEquals = context.testFramework.assertIntArrayEquals
    -    val assertLongArrayEquals = context.testFramework.assertLongArrayEquals
    -    val assertFloatArrayEquals = context.testFramework.assertFloatArrayEquals
    -    val assertDoubleArrayEquals = context.testFramework.assertDoubleArrayEquals
    -
    -    // Points to the class, into which data provider methods in parametrized tests should be put (current or outermost).
    -    // It is needed, because data provider methods are static and thus may not be put into inner classes, e.g. in JUnit5
    -    // all data providers should be placed in the outermost class.
    -    protected abstract val dataProviderMethodsHolder: TestClassContext
    -
    -    protected val statementConstructor = getStatementConstructorBy(context)
    -
    -    abstract val annotationForNestedClasses: CgAnnotation?
    -
    -    abstract val annotationForOuterClasses: CgAnnotation?
    -
    -    protected open val timeoutArgumentName: String = "timeout"
    -
    -    open fun assertEquals(expected: CgValue, actual: CgValue) {
    -        +assertions[assertEquals](expected, actual)
    -    }
    -
    -    open fun assertFloatEquals(expected: CgExpression, actual: CgExpression, delta: Any) {
    -        +assertions[assertFloatEquals](expected, actual, delta)
    -    }
    -
    -    open fun assertDoubleEquals(expected: CgExpression, actual: CgExpression, delta: Any) {
    -        +assertions[assertDoubleEquals](expected, actual, delta)
    -    }
    -
    -    open fun getArrayEqualsAssertion(arrayType: ClassId, expected: CgExpression, actual: CgExpression): CgMethodCall =
    -        when (arrayType) {
    -            booleanArrayClassId -> assertions[assertBooleanArrayEquals](expected, actual)
    -            byteArrayClassId -> assertions[assertByteArrayEquals](expected, actual)
    -            charArrayClassId -> assertions[assertCharArrayEquals](expected, actual)
    -            shortArrayClassId -> assertions[assertShortArrayEquals](expected, actual)
    -            intArrayClassId -> assertions[assertIntArrayEquals](expected, actual)
    -            longArrayClassId -> assertions[assertLongArrayEquals](expected, actual)
    -            floatArrayClassId -> assertions[assertFloatArrayEquals](expected, actual)
    -            doubleArrayClassId -> assertions[assertDoubleArrayEquals](expected, actual)
    -            else -> assertions[assertArrayEquals](expected, actual)
    -        }
    -
    -    fun assertArrayEquals(arrayType: ClassId, expected: CgExpression, actual: CgExpression) {
    -        +getArrayEqualsAssertion(arrayType, expected, actual)
    -    }
    -
    -    open fun getDeepEqualsAssertion(expected: CgExpression, actual: CgExpression): CgMethodCall {
    -        requiredUtilMethods += setOf(
    -            utilMethodProvider.deepEqualsMethodId,
    -            utilMethodProvider.arraysDeepEqualsMethodId,
    -            utilMethodProvider.iterablesDeepEqualsMethodId,
    -            utilMethodProvider.streamsDeepEqualsMethodId,
    -            utilMethodProvider.mapsDeepEqualsMethodId,
    -            utilMethodProvider.hasCustomEqualsMethodId
    -        )
    -        // TODO we cannot use common assertEquals because of using custom deepEquals
    -        //  For this reason we have to use assertTrue here
    -        //  Unfortunately, if test with assertTrue fails, it gives non informative message false != true
    -        //  Thus, we need to provide custom message to assertTrue showing compared objects correctly
    -        //  SAT-1345
    -        return assertions[assertTrue](utilsClassId[deepEquals](expected, actual))
    -    }
    -
    -    @Suppress("unused")
    -    fun assertDeepEquals(expected: CgExpression, actual: CgExpression) {
    -        +getDeepEqualsAssertion(expected, actual)
    -    }
    -
    -    open fun getFloatArrayEqualsAssertion(expected: CgExpression, actual: CgExpression, delta: Any): CgMethodCall =
    -        assertions[assertFloatArrayEquals](expected, actual, delta)
    -
    -    fun assertFloatArrayEquals(expected: CgExpression, actual: CgExpression, delta: Any) {
    -        +getFloatArrayEqualsAssertion(expected, actual, delta)
    -    }
    -
    -    open fun getDoubleArrayEqualsAssertion(expected: CgExpression, actual: CgExpression, delta: Any): CgMethodCall =
    -        assertions[assertDoubleArrayEquals](expected, actual, delta)
    -
    -    fun assertDoubleArrayEquals(expected: CgExpression, actual: CgExpression, delta: Any) {
    -        +getDoubleArrayEqualsAssertion(expected, actual, delta)
    -    }
    -
    -    fun assertNull(actual: CgExpression) {
    -        +assertions[assertNull](actual)
    -    }
    -
    -    fun assertBoolean(expected: Boolean, actual: CgExpression) {
    -        if (expected) {
    -            +assertions[assertTrue](actual)
    -        } else {
    -            +assertions[assertFalse](actual)
    -        }
    -    }
    -
    -    fun assertBoolean(actual: CgExpression) = assertBoolean(expected = true, actual)
    -
    -    // Exception expectation differs between test frameworks
    -    // JUnit4 requires to add a specific argument to the test method annotation
    -    // JUnit5 requires using method assertThrows()
    -    // TestNg allows both approaches, we use similar to JUnit5
    -    abstract fun expectException(exception: ClassId, block: () -> Unit)
    -
    -    /**
    -     * Creates annotations for data provider method in parameterized tests
    -     */
    -    abstract fun createDataProviderAnnotations(dataProviderMethodName: String): MutableList
    -
    -    /**
    -     * Creates declaration of argList collection in parameterized tests.
    -     */
    -    abstract fun createArgList(length: Int): CgVariable
    -
    -    abstract fun collectParameterizedTestAnnotations(dataProviderMethodName: String?): Set
    -
    -    abstract fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int)
    -
    -    open fun expectTimeout(timeoutMs: Long, block: () -> Unit) {}
    -
    -    open fun setTestExecutionTimeout(timeoutMs: Long) {
    -        val timeout = CgNamedAnnotationArgument(
    -            name = timeoutArgumentName,
    -            value = timeoutMs.resolve()
    -        )
    -        val testAnnotation = collectedMethodAnnotations.singleOrNull { it.classId == testFramework.testAnnotationId }
    -
    -        if (testAnnotation is CgMultipleArgsAnnotation) {
    -            testAnnotation.arguments += timeout
    -        } else {
    -            collectedMethodAnnotations += CgMultipleArgsAnnotation(
    -                testFramework.testAnnotationId,
    -                mutableListOf(timeout)
    -            )
    -        }
    -    }
    -
    -
    -    /**
    -     * Add a short test's description depending on the test framework type:
    -     */
    -    abstract fun addTestDescription(description: String)
    -
    -    abstract fun disableTestMethod(reason: String)
    -
    -    /**
    -     * Adds @DisplayName annotation.
    -     *
    -     * Should be used only with JUnit 5.
    -     * @see issue-576 on GitHub
    -     */
    -    open fun addDisplayName(name: String) {
    -        collectedMethodAnnotations += CgSingleArgAnnotation(Junit5.displayNameClassId, stringLiteral(name))
    -    }
    -
    -    protected fun ClassId.toExceptionClass(): CgExpression =
    -            if (isAccessibleFrom(testClassPackageName)) {
    -                CgGetJavaClass(this)
    -            } else {
    -                statementConstructor.newVar(classCgClassId) { Class::class.id[forName](name) }
    -            }
    -
    -    fun addDataProvider(dataProvider: CgMethod) {
    -        dataProviderMethodsHolder.cgDataProviderMethods += dataProvider
    -    }
    -}
    -
    -internal class TestNgManager(context: CgContext) : TestFrameworkManager(context) {
    -    override val dataProviderMethodsHolder: TestClassContext
    -        get() = currentTestClassContext
    -
    -    override val annotationForNestedClasses: CgAnnotation?
    -        get() = null
    -
    -    override val annotationForOuterClasses: CgAnnotation?
    -        get() = null
    -
    -    override val timeoutArgumentName: String = "timeOut"
    -
    -    private val assertThrows: BuiltinMethodId
    -        get() {
    -            require(testFramework is TestNg) { "According to settings, TestNg was expected, but got: $testFramework" }
    -
    -            return testFramework.assertThrows
    -        }
    -
    -    override fun assertEquals(expected: CgValue, actual: CgValue) = super.assertEquals(actual, expected)
    -
    -    override fun assertFloatEquals(expected: CgExpression, actual: CgExpression, delta: Any) =
    -            super.assertFloatEquals(actual, expected, delta)
    -
    -    override fun assertDoubleEquals(expected: CgExpression, actual: CgExpression, delta: Any) =
    -            super.assertDoubleEquals(actual, expected, delta)
    -
    -    override fun getArrayEqualsAssertion(arrayType: ClassId, expected: CgExpression, actual: CgExpression): CgMethodCall =
    -            super.getArrayEqualsAssertion(arrayType, actual, expected)
    -
    -    override fun getDeepEqualsAssertion(expected: CgExpression, actual: CgExpression): CgMethodCall =
    -            super.getDeepEqualsAssertion(actual, expected)
    -
    -    override fun getFloatArrayEqualsAssertion(expected: CgExpression, actual: CgExpression, delta: Any): CgMethodCall =
    -            super.getFloatArrayEqualsAssertion(actual, expected, delta)
    -
    -    override fun getDoubleArrayEqualsAssertion(expected: CgExpression, actual: CgExpression, delta: Any): CgMethodCall =
    -            super.getDoubleArrayEqualsAssertion(actual, expected, delta)
    -
    -    override fun expectException(exception: ClassId, block: () -> Unit) {
    -        require(testFramework is TestNg) { "According to settings, TestNg was expected, but got: $testFramework" }
    -        val lambda = statementConstructor.lambda(testFramework.throwingRunnableClassId) { block() }
    -        +assertions[assertThrows](exception.toExceptionClass(), lambda)
    -    }
    -
    -    override fun createDataProviderAnnotations(dataProviderMethodName: String) =
    -        mutableListOf(
    -            statementConstructor.annotation(
    -                testFramework.methodSourceAnnotationId,
    -                listOf("name" to stringLiteral(dataProviderMethodName))
    -            ),
    -        )
    -
    -    override fun createArgList(length: Int) =
    -        statementConstructor.newVar(testFramework.argListClassId, "argList") {
    -            CgAllocateArray(testFramework.argListClassId, Array::class.java.id, length)
    -        }
    -
    -    override fun collectParameterizedTestAnnotations(dataProviderMethodName: String?) = setOf(
    -        statementConstructor.annotation(
    -            testFramework.parameterizedTestAnnotationId,
    -            listOf("dataProvider" to CgLiteral(stringClassId, dataProviderMethodName))
    -        )
    -    )
    -
    -    override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) =
    -        setArgumentsArrayElement(argsVariable, executionIndex, argsArray, statementConstructor)
    -
    -    /**
    -     * Supplements TestNG @Test annotation with a description.
    -     * It looks like @Test(description="...")
    -     *
    -     * @see issue-576 on GitHub
    -     */
    -    private fun addDescriptionAnnotation(description: String) {
    -        val testAnnotation =
    -            collectedMethodAnnotations.singleOrNull { it.classId == testFramework.testAnnotationId }
    -
    -        val descriptionArgument = CgNamedAnnotationArgument("description", stringLiteral(description))
    -        if (testAnnotation is CgMultipleArgsAnnotation) {
    -            testAnnotation.arguments += descriptionArgument
    -        } else {
    -            collectedMethodAnnotations += CgMultipleArgsAnnotation(
    -                testFramework.testAnnotationId,
    -                mutableListOf(descriptionArgument)
    -            )
    -        }
    -    }
    -
    -    override fun addTestDescription(description: String) = addDescriptionAnnotation(description)
    -
    -    override fun disableTestMethod(reason: String) {
    -        require(testFramework is TestNg) { "According to settings, TestNg was expected, but got: $testFramework" }
    -
    -        val disabledAnnotationArgument = CgNamedAnnotationArgument(
    -            name = "enabled",
    -            value = false.resolve()
    -        )
    -
    -        val descriptionArgumentName = "description"
    -        val descriptionTestAnnotationArgument = CgNamedAnnotationArgument(
    -            name = descriptionArgumentName,
    -            value = reason.resolve()
    -        )
    -
    -        val testAnnotation = collectedMethodAnnotations.singleOrNull { it.classId == testFramework.testAnnotationId }
    -        if (testAnnotation is CgMultipleArgsAnnotation) {
    -            testAnnotation.arguments += disabledAnnotationArgument
    -
    -            val alreadyExistingDescriptionAnnotationArgument = testAnnotation.arguments.singleOrNull {
    -                it.name == descriptionArgumentName
    -            }
    -
    -            // append new description to existing one
    -            if (alreadyExistingDescriptionAnnotationArgument != null) {
    -                val gluedDescription = with(alreadyExistingDescriptionAnnotationArgument) {
    -                    require(value is CgLiteral && value.value is String) {
    -                        "Expected description to be String literal but got ${value.type}"
    -                    }
    -
    -                    listOf(value.value, reason).joinToString("; ").resolve()
    -                }
    -
    -                testAnnotation.arguments.map {
    -                    if (it.name != descriptionArgumentName) return@map it
    -
    -                    CgNamedAnnotationArgument(
    -                        name = descriptionArgumentName,
    -                        value = gluedDescription
    -                    )
    -                }
    -            } else {
    -                testAnnotation.arguments += descriptionTestAnnotationArgument
    -            }
    -        } else {
    -            collectedMethodAnnotations += CgMultipleArgsAnnotation(
    -                testFramework.testAnnotationId,
    -                mutableListOf(disabledAnnotationArgument, descriptionTestAnnotationArgument)
    -            )
    -        }
    -    }
    -}
    -
    -internal class Junit4Manager(context: CgContext) : TestFrameworkManager(context) {
    -    private val parametrizedTestsNotSupportedError: Nothing
    -        get() = error("Parametrized tests are not supported for JUnit4")
    -
    -    override val dataProviderMethodsHolder: TestClassContext
    -        get() = parametrizedTestsNotSupportedError
    -
    -    override val annotationForNestedClasses: CgAnnotation?
    -        get() = null
    -
    -    override val annotationForOuterClasses: CgAnnotation
    -        get() {
    -            require(testFramework is Junit4) { "According to settings, JUnit4 was expected, but got: $testFramework" }
    -            return statementConstructor.annotation(
    -                testFramework.runWithAnnotationClassId,
    -                testFramework.enclosedClassId.let {
    -                    when (codegenLanguage) {
    -                        CodegenLanguage.JAVA   -> CgGetJavaClass(it)
    -                        CodegenLanguage.KOTLIN -> CgGetKotlinClass(it)
    -                    }
    -                }
    -            )
    -        }
    -
    -    override fun expectException(exception: ClassId, block: () -> Unit) {
    -        require(testFramework is Junit4) { "According to settings, JUnit4 was expected, but got: $testFramework" }
    -
    -        require(exception.isAccessibleFrom(testClassPackageName)) {
    -            "Exception $exception is not accessible from package $testClassPackageName"
    -        }
    -
    -        val expected = CgNamedAnnotationArgument(
    -            name = "expected",
    -            value = classLiteralAnnotationArgument(exception, codegenLanguage)
    -        )
    -        val testAnnotation = collectedMethodAnnotations.singleOrNull { it.classId == testFramework.testAnnotationId }
    -        if (testAnnotation is CgMultipleArgsAnnotation) {
    -            testAnnotation.arguments += expected
    -        } else {
    -            collectedMethodAnnotations += CgMultipleArgsAnnotation(testFramework.testAnnotationId, mutableListOf(expected))
    -        }
    -        block()
    -    }
    -
    -    override fun createDataProviderAnnotations(dataProviderMethodName: String) =
    -        parametrizedTestsNotSupportedError
    -
    -    override fun createArgList(length: Int) =
    -        parametrizedTestsNotSupportedError
    -
    -    override fun collectParameterizedTestAnnotations(dataProviderMethodName: String?) =
    -        parametrizedTestsNotSupportedError
    -
    -    override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) =
    -        parametrizedTestsNotSupportedError
    -
    -    override fun addTestDescription(description: String) = Unit
    -
    -    override fun disableTestMethod(reason: String) {
    -        require(testFramework is Junit4) { "According to settings, JUnit4 was expected, but got: $testFramework" }
    -
    -        collectedMethodAnnotations += CgMultipleArgsAnnotation(
    -            testFramework.ignoreAnnotationClassId,
    -            mutableListOf(
    -                CgNamedAnnotationArgument(
    -                    name = "value",
    -                    value = reason.resolve()
    -                )
    -            )
    -        )
    -    }
    -}
    -
    -internal class Junit5Manager(context: CgContext) : TestFrameworkManager(context) {
    -    override val dataProviderMethodsHolder: TestClassContext
    -        get() = outerMostTestClassContext
    -
    -    override val annotationForNestedClasses: CgAnnotation
    -        get() {
    -            require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" }
    -
    -            return statementConstructor.annotation(testFramework.nestedTestClassAnnotationId)
    -        }
    -
    -    override val annotationForOuterClasses: CgAnnotation?
    -        get() = null
    -
    -    private val assertThrows: BuiltinMethodId
    -        get() {
    -            require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" }
    -
    -            return testFramework.assertThrows
    -        }
    -
    -    override fun expectException(exception: ClassId, block: () -> Unit) {
    -        require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" }
    -        val lambda = statementConstructor.lambda(testFramework.executableClassId) { block() }
    -        +assertions[assertThrows](exception.toExceptionClass(), lambda)
    -    }
    -
    -    override fun createDataProviderAnnotations(dataProviderMethodName: String) = mutableListOf()
    -
    -    override fun createArgList(length: Int) =
    -        statementConstructor.newVar(testFramework.argListClassId, "argList") {
    -            val constructor = ConstructorId(testFramework.argListClassId, emptyList())
    -            constructor.invoke()
    -        }
    -
    -    override fun collectParameterizedTestAnnotations(dataProviderMethodName: String?) = setOf(
    -        statementConstructor.annotation(testFramework.parameterizedTestAnnotationId),
    -        statementConstructor.annotation(
    -            testFramework.methodSourceAnnotationId,
    -            "${outerMostTestClass.canonicalName}#$dataProviderMethodName"
    -        )
    -    )
    -
    -    override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) {
    -        +argsVariable[addToListMethodId](
    -            argumentsClassId[argumentsMethodId](argsArray)
    -        )
    -    }
    -
    -
    -    override fun expectTimeout(timeoutMs : Long, block: () -> Unit) {
    -        require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" }
    -        val lambda = statementConstructor.lambda(testFramework.executableClassId) { block() }
    -        importIfNeeded(testFramework.durationClassId)
    -        val duration = CgMethodCall(null, testFramework.ofMillis, listOf(timeoutMs.resolve()))
    -        +assertions[testFramework.assertTimeoutPreemptively](duration, lambda)
    -    }
    -
    -    override fun addDisplayName(name: String) {
    -        require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" }
    -        collectedMethodAnnotations += statementConstructor.annotation(testFramework.displayNameClassId, name)
    -    }
    -
    -    override fun setTestExecutionTimeout(timeoutMs: Long) {
    -        require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" }
    -
    -        val timeoutAnnotationArguments = mutableListOf()
    -        timeoutAnnotationArguments += CgNamedAnnotationArgument(
    -            name = "value",
    -            value = timeoutMs.resolve()
    -        )
    -
    -        val milliseconds = TimeUnit.MILLISECONDS
    -        timeoutAnnotationArguments += CgNamedAnnotationArgument(
    -            name = "unit",
    -            value = CgEnumConstantAccess(testFramework.timeunitClassId, milliseconds.name)
    -        )
    -        importIfNeeded(testFramework.timeunitClassId)
    -
    -        collectedMethodAnnotations += CgMultipleArgsAnnotation(
    -            Junit5.timeoutClassId,
    -            timeoutAnnotationArguments
    -        )
    -    }
    -
    -    override fun addTestDescription(description: String) = addDisplayName(description)
    -
    -    override fun disableTestMethod(reason: String) {
    -        require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" }
    -
    -        collectedMethodAnnotations += CgMultipleArgsAnnotation(
    -            testFramework.disabledAnnotationClassId,
    -            mutableListOf(
    -                CgNamedAnnotationArgument(
    -                    name = "value",
    -                    value = reason.resolve()
    -                )
    -            )
    -        )
    -    }
    -}
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestsGenerationReport.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestsGenerationReport.kt
    deleted file mode 100644
    index 606ef293af..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestsGenerationReport.kt
    +++ /dev/null
    @@ -1,111 +0,0 @@
    -package org.utbot.framework.codegen.model.constructor.tree
    -
    -import org.utbot.common.appendHtmlLine
    -import org.utbot.framework.codegen.model.constructor.CgMethodTestSet
    -import org.utbot.framework.codegen.model.tree.CgTestMethod
    -import org.utbot.framework.codegen.model.tree.CgTestMethodType
    -import org.utbot.framework.plugin.api.ExecutableId
    -import org.utbot.framework.plugin.api.util.kClass
    -import kotlin.reflect.KClass
    -
    -typealias MethodGeneratedTests = MutableMap>
    -typealias ErrorsCount = Map
    -
    -data class TestsGenerationReport(
    -    val executables: MutableSet = mutableSetOf(),
    -    var successfulExecutions: MethodGeneratedTests = mutableMapOf(),
    -    var timeoutExecutions: MethodGeneratedTests = mutableMapOf(),
    -    var failedExecutions: MethodGeneratedTests = mutableMapOf(),
    -    var crashExecutions: MethodGeneratedTests = mutableMapOf(),
    -    var errors: MutableMap = mutableMapOf()
    -) {
    -    val classUnderTest: KClass<*>
    -        get() = executables.firstOrNull()?.classId?.kClass
    -            ?: error("No executables found in test report")
    -
    -    val initialWarnings: MutableList<() -> String> = mutableListOf()
    -    val hasWarnings: Boolean
    -        get() = initialWarnings.isNotEmpty()
    -
    -    val detailedStatistics: String
    -        get() = buildString {
    -            appendHtmlLine("Class: ${classUnderTest.qualifiedName}")
    -            val testMethodsStatistic = executables.map { it.countTestMethods() }
    -            val errors = executables.map { it.countErrors() }
    -            val overallErrors = errors.sum()
    -
    -            appendHtmlLine("Successful test methods: ${testMethodsStatistic.sumBy { it.successful }}")
    -            appendHtmlLine(
    -                "Failing because of unexpected exception test methods: ${testMethodsStatistic.sumBy { it.failing }}"
    -            )
    -            appendHtmlLine(
    -                "Failing because of exceeding timeout test methods: ${testMethodsStatistic.sumBy { it.timeout }}"
    -            )
    -            appendHtmlLine(
    -                "Failing because of possible JVM crash test methods: ${testMethodsStatistic.sumBy { it.crashes }}"
    -            )
    -            appendHtmlLine("Not generated because of internal errors test methods: $overallErrors")
    -        }
    -
    -    fun addMethodErrors(testSet: CgMethodTestSet, errors: Map) {
    -        this.errors[testSet.executableId] = errors
    -    }
    -
    -    fun addTestsByType(testSet: CgMethodTestSet, testMethods: List) {
    -        with(testSet.executableId) {
    -            executables += this
    -
    -            testMethods.forEach {
    -                when (it.type) {
    -                    CgTestMethodType.SUCCESSFUL -> updateExecutions(it, successfulExecutions)
    -                    CgTestMethodType.FAILING -> updateExecutions(it, failedExecutions)
    -                    CgTestMethodType.TIMEOUT -> updateExecutions(it, timeoutExecutions)
    -                    CgTestMethodType.CRASH -> updateExecutions(it, crashExecutions)
    -                    CgTestMethodType.PARAMETRIZED -> {
    -                        // Parametrized tests are not supported in the tests report yet
    -                        // TODO JIRA:1507
    -                    }
    -                }
    -            }
    -        }
    -    }
    -
    -    fun toString(isShort: Boolean): String = buildString {
    -        appendHtmlLine("Target: ${classUnderTest.qualifiedName}")
    -        if (initialWarnings.isNotEmpty()) {
    -            initialWarnings.forEach { appendHtmlLine(it()) }
    -            appendHtmlLine()
    -        }
    -
    -        val testMethodsStatistic = executables.map { it.countTestMethods() }
    -        val overallTestMethods = testMethodsStatistic.sumBy { it.count }
    -
    -        appendHtmlLine("Overall test methods: $overallTestMethods")
    -
    -        if (!isShort) {
    -            appendHtmlLine(detailedStatistics)
    -        }
    -    }
    -
    -    override fun toString(): String = toString(false)
    -
    -    private fun ExecutableId.countTestMethods(): TestMethodStatistic = TestMethodStatistic(
    -        testMethodsNumber(successfulExecutions),
    -        testMethodsNumber(failedExecutions),
    -        testMethodsNumber(timeoutExecutions),
    -        testMethodsNumber(crashExecutions)
    -    )
    -
    -    private fun ExecutableId.countErrors(): Int = errors.getOrDefault(this, emptyMap()).values.sum()
    -
    -    private fun ExecutableId.testMethodsNumber(executables: MethodGeneratedTests): Int =
    -        executables.getOrDefault(this, emptySet()).size
    -
    -    private fun ExecutableId.updateExecutions(it: CgTestMethod, executions: MethodGeneratedTests) {
    -        executions.getOrPut(this) { mutableSetOf() } += it
    -    }
    -
    -    private data class TestMethodStatistic(val successful: Int, val failing: Int, val timeout: Int, val crashes: Int) {
    -        val count: Int = successful + failing + timeout + crashes
    -    }
    -}
    \ No newline at end of file
    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
    deleted file mode 100644
    index d25567d86b..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt
    +++ /dev/null
    @@ -1,425 +0,0 @@
    -package org.utbot.framework.codegen.model.constructor.util
    -
    -import org.utbot.framework.codegen.RegularImport
    -import org.utbot.framework.codegen.StaticImport
    -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner
    -import org.utbot.framework.codegen.model.tree.CgClassId
    -import org.utbot.framework.codegen.model.tree.CgExpression
    -import org.utbot.framework.codegen.model.tree.CgTypeCast
    -import org.utbot.framework.codegen.model.tree.CgValue
    -import org.utbot.framework.codegen.model.tree.CgVariable
    -import org.utbot.framework.codegen.model.util.isAccessibleFrom
    -import org.utbot.framework.fields.ArrayElementAccess
    -import org.utbot.framework.fields.FieldAccess
    -import org.utbot.framework.fields.FieldPath
    -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.enclosingClass
    -import org.utbot.framework.plugin.api.util.executable
    -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.isRefType
    -import org.utbot.framework.plugin.api.util.isSubtypeOf
    -import org.utbot.framework.plugin.api.util.longClassId
    -import org.utbot.framework.plugin.api.util.shortClassId
    -import org.utbot.framework.plugin.api.util.underlyingType
    -import kotlinx.collections.immutable.PersistentList
    -import kotlinx.collections.immutable.PersistentSet
    -import org.utbot.framework.codegen.model.constructor.builtin.setArrayElement
    -import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager
    -import org.utbot.framework.codegen.model.tree.CgAllocateInitializedArray
    -import org.utbot.framework.codegen.model.tree.CgArrayInitializer
    -import org.utbot.framework.codegen.model.util.at
    -import org.utbot.framework.plugin.api.BuiltinClassId
    -import org.utbot.framework.plugin.api.BuiltinMethodId
    -import org.utbot.framework.plugin.api.ClassId
    -import org.utbot.framework.plugin.api.ConstructorId
    -import org.utbot.framework.plugin.api.ExecutableId
    -import org.utbot.framework.plugin.api.MethodId
    -import org.utbot.framework.plugin.api.UtArrayModel
    -import org.utbot.framework.plugin.api.UtExecution
    -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.WildcardTypeParameter
    -import org.utbot.framework.plugin.api.util.isStatic
    -import org.utbot.framework.plugin.api.util.arrayLikeName
    -import org.utbot.framework.plugin.api.util.builtinStaticMethodId
    -import org.utbot.framework.plugin.api.util.denotableType
    -import org.utbot.framework.plugin.api.util.methodId
    -import org.utbot.framework.plugin.api.util.objectArrayClassId
    -import org.utbot.framework.plugin.api.util.objectClassId
    -
    -internal data class EnvironmentFieldStateCache(
    -    val thisInstance: FieldStateCache,
    -    val arguments: Array,
    -    val classesWithStaticFields: MutableMap
    -) {
    -    companion object {
    -        fun emptyCacheFor(execution: UtExecution): EnvironmentFieldStateCache {
    -            val argumentsCache = Array(execution.stateBefore.parameters.size) { FieldStateCache() }
    -
    -            val staticFields = execution.stateBefore.statics.keys
    -            val classesWithStaticFields = staticFields.groupBy { it.declaringClass }.keys
    -            val staticFieldsCache = mutableMapOf().apply {
    -                for (classId in classesWithStaticFields) {
    -                    put(classId, FieldStateCache())
    -                }
    -            }
    -
    -            return EnvironmentFieldStateCache(
    -                thisInstance = FieldStateCache(),
    -                arguments = argumentsCache,
    -                classesWithStaticFields = staticFieldsCache
    -            )
    -        }
    -    }
    -
    -    override fun equals(other: Any?): Boolean {
    -        if (this === other) return true
    -        if (javaClass != other?.javaClass) return false
    -
    -        other as EnvironmentFieldStateCache
    -
    -        if (thisInstance != other.thisInstance) return false
    -        if (!arguments.contentEquals(other.arguments)) return false
    -        if (classesWithStaticFields != other.classesWithStaticFields) return false
    -
    -        return true
    -    }
    -
    -    override fun hashCode(): Int {
    -        var result = thisInstance.hashCode()
    -        result = 31 * result + arguments.contentHashCode()
    -        result = 31 * result + classesWithStaticFields.hashCode()
    -        return result
    -    }
    -}
    -
    -internal class FieldStateCache {
    -    val before: MutableMap = mutableMapOf()
    -    val after: MutableMap = mutableMapOf()
    -
    -    val paths: List
    -        get() = (before.keys union after.keys).toList()
    -
    -    override fun equals(other: Any?): Boolean {
    -        if (this === other) return true
    -        if (javaClass != other?.javaClass) return false
    -
    -        other as FieldStateCache
    -
    -        if (before != other.before) return false
    -        if (after != other.after) return false
    -
    -        return true
    -    }
    -
    -    override fun hashCode(): Int {
    -        var result = before.hashCode()
    -        result = 31 * result + after.hashCode()
    -        return result
    -    }
    -}
    -
    -internal data class CgFieldState(val variable: CgVariable, val model: UtModel)
    -
    -data class ExpressionWithType(val type: ClassId, val expression: CgExpression)
    -
    -/**
    - * Check if a method is an util method of the current class
    - */
    -internal fun CgContextOwner.isUtil(method: MethodId): Boolean {
    -    return method in utilMethodProvider.utilMethodIds
    -}
    -
    -val classCgClassId = CgClassId(Class::class.id, typeParameters = WildcardTypeParameter(), isNullable = false)
    -
    -/**
    - * A [MethodId] to add an item into [ArrayList].
    - */
    -internal val addToListMethodId: MethodId
    -    get() = methodId(
    -        classId = ArrayList::class.id,
    -        name = "add",
    -        returnType = booleanClassId,
    -        arguments = arrayOf(Object::class.id),
    -    )
    -
    -/**
    - * A [ClassId] of class `org.junit.jupiter.params.provider.Arguments`
    - */
    -internal val argumentsClassId: BuiltinClassId
    -    get() = BuiltinClassId(
    -        name = "org.junit.jupiter.params.provider.Arguments",
    -        simpleName = "Arguments",
    -        canonicalName = "org.junit.jupiter.params.provider.Arguments",
    -        packageName = "org.junit.jupiter.params.provider"
    -    )
    -
    -/**
    - * A [MethodId] to call JUnit Arguments method.
    - */
    -internal val argumentsMethodId: BuiltinMethodId
    -    get() = builtinStaticMethodId(
    -        classId = argumentsClassId,
    -        name = "arguments",
    -        returnType = argumentsClassId,
    -        // vararg of Objects
    -        arguments = arrayOf(objectArrayClassId)
    -    )
    -
    -internal fun getStaticFieldVariableName(owner: ClassId, path: FieldPath): String {
    -    val elements = mutableListOf()
    -    elements += owner.simpleName.decapitalize()
    -    // TODO: check how capitalize() works with numeric strings e.g. "0"
    -    elements += path.toStringList().map { it.capitalize() }
    -    return elements.joinToString("")
    -}
    -
    -internal fun getFieldVariableName(owner: CgValue, path: FieldPath): String {
    -    val elements = mutableListOf()
    -    if (owner is CgVariable) {
    -        elements += owner.name
    -    }
    -    // TODO: check how capitalize() works with numeric strings e.g. "0"
    -    elements += path.toStringList().map { it.capitalize() }
    -    if (elements.size > 0) {
    -        elements[0] = elements[0].decapitalize()
    -    }
    -    return elements.joinToString("")
    -}
    -
    -private fun FieldPath.toStringList(): List =
    -    elements.map {
    -        when (it) {
    -            is FieldAccess -> it.field.name
    -            is ArrayElementAccess -> it.index.toString()
    -        }
    -    }
    -
    -internal fun infiniteInts(): Sequence =
    -    generateSequence(1) { it + 1 }
    -
    -internal const val MAX_ARRAY_INITIALIZER_SIZE = 10
    -
    -/**
    - * Checks if we have already imported a class with such simple name.
    - * If so, we cannot import [type] (because it will be used with simple name and will be clashed with already imported)
    - * and should use its fully qualified name instead.
    - */
    -private fun CgContextOwner.doesNotHaveSimpleNameClash(type: ClassId): Boolean =
    -    importedClasses.none { it.simpleName == type.simpleName }
    -
    -internal fun CgContextOwner.importIfNeeded(type: ClassId) {
    -    // TODO: for now we consider that tests are generated in the same package as CUT, but this may change
    -    val underlyingType = type.underlyingType
    -
    -    underlyingType
    -        .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
    -        ?.takeIf { doesNotHaveSimpleNameClash(it) }
    -        ?.let {
    -            importedClasses += it
    -            collectedImports += it.toImport()
    -        }
    -
    -    // for nested classes we need to import enclosing class
    -    if (underlyingType.isNested) {
    -        importIfNeeded(underlyingType.enclosingClass!!)
    -    }
    -}
    -
    -internal fun CgContextOwner.importIfNeeded(method: MethodId) {
    -    val name = method.name
    -    val packageName = method.classId.packageName
    -    method.takeIf { it.isStatic && packageName != testClassPackageName && packageName != "java.lang" }
    -        .takeIf { importedStaticMethods.none { it.name == name } }
    -        // do not import method under test in order to specify the declaring class directly for its calls
    -        .takeIf { currentExecutable != method }
    -        ?.let {
    -            importedStaticMethods += method
    -            collectedImports += StaticImport(method.classId.canonicalName, method.name)
    -        }
    -}
    -
    -/**
    - * Casts [expression] to [targetType].
    - *
    - * This method uses [denotableType] of the given [targetType] to ensure
    - * that we are using a denotable type in the type cast.
    - *
    - * Specifically, if we attempt to do a type cast to an anonymous class,
    - * then we will actually perform a type cast to its supertype.
    - *
    - * @see denotableType
    - *
    - * @param isSafetyCast shows if we should render "as?" instead of "as" in Kotlin
    - */
    -internal fun CgContextOwner.typeCast(
    -    targetType: ClassId,
    -    expression: CgExpression,
    -    isSafetyCast: Boolean = false
    -): CgExpression {
    -    val denotableTargetType = targetType.denotableType
    -    importIfNeeded(denotableTargetType)
    -    return CgTypeCast(denotableTargetType, expression, isSafetyCast)
    -}
    -
    -/**
    - * Sets an element of arguments array in parameterized test,
    - * if test framework represents arguments as array.
    - */
    -internal fun  T.setArgumentsArrayElement(
    -    array: CgVariable,
    -    index: Int,
    -    value: CgExpression,
    -    constructor: CgStatementConstructor
    -) where T : CgContextOwner, T: CgCallableAccessManager {
    -    when (array.type) {
    -        objectClassId -> {
    -            +java.lang.reflect.Array::class.id[setArrayElement](array, index, value)
    -        }
    -        else -> with(constructor) { array.at(index) `=` value }
    -    }
    -}
    -
    -@Suppress("unused")
    -internal fun newArrayOf(elementType: ClassId, values: List): CgAllocateInitializedArray {
    -    val arrayType = arrayTypeOf(elementType)
    -    return CgAllocateInitializedArray(arrayInitializer(arrayType, elementType, values))
    -}
    -
    -internal fun arrayInitializer(arrayType: ClassId, elementType: ClassId, values: List): CgArrayInitializer =
    -    CgArrayInitializer(arrayType, elementType, values)
    -
    -
    -/**
    - * For a given [elementType] returns a [ClassId] of an array with elements of this type.
    - * For example, for an id of `int` the result will be an id of `int[]`.
    - *
    - * @param elementType the element type of the returned array class id
    - * @param isNullable a flag whether returned array is nullable or not
    - */
    -fun arrayTypeOf(elementType: ClassId, isNullable: Boolean = false): ClassId {
    -    val arrayIdName = "[${elementType.arrayLikeName}"
    -    return when (elementType) {
    -        is BuiltinClassId -> BuiltinClassId(
    -            name = arrayIdName,
    -            canonicalName = "${elementType.canonicalName}[]",
    -            simpleName = "${elementType.simpleName}[]",
    -            elementClassId = elementType,
    -            isNullable = isNullable
    -        )
    -        else -> ClassId(
    -            name = arrayIdName,
    -            elementClassId = elementType,
    -            isNullable = isNullable
    -        )
    -    }
    -}
    -
    -internal fun Class<*>.overridesEquals(): Boolean =
    -    when {
    -        // Object does not override equals
    -        this == Any::class.java -> false
    -        id isSubtypeOf Map::class.id -> true
    -        id isSubtypeOf Collection::class.id -> true
    -        else -> declaredMethods.any { it.name == "equals" && it.parameterTypes.contentEquals(arrayOf(Any::class.java)) }
    -    }
    -
    -// NOTE: this function does not consider executable return type because it is not important in our case
    -internal fun ClassId.getAmbiguousOverloadsOf(executableId: ExecutableId): Sequence {
    -    val allExecutables = when (executableId) {
    -        is MethodId -> allMethods
    -        is ConstructorId -> allConstructors
    -    }
    -
    -    return allExecutables.filter {
    -        it.name == executableId.name && it.parameters.size == executableId.executable.parameters.size && it.classId == executableId.classId
    -    }
    -}
    -
    -
    -internal infix fun ClassId.hasAmbiguousOverloadsOf(executableId: ExecutableId): Boolean {
    -    // TODO: for now we assume that builtin classes do not have ambiguous overloads
    -    if (this is BuiltinClassId) return false
    -
    -    return getAmbiguousOverloadsOf(executableId).toList().size > 1
    -}
    -
    -private val defaultByPrimitiveType: Map = mapOf(
    -    booleanClassId to false,
    -    byteClassId to 0.toByte(),
    -    charClassId to '\u0000',
    -    shortClassId to 0.toShort(),
    -    intClassId to 0,
    -    longClassId to 0L,
    -    floatClassId to 0.0f,
    -    doubleClassId to 0.0
    -)
    -
    -/**
    - * By 'default' here we mean a value that is used for a type in one of the two cases:
    - * - When we allocate an array of some type in the following manner: `new int[10]`,
    - * the array is filled with some value. In case of `int` this value is `0`, for `boolean`
    - * it is `false`, etc.
    - * - When we allocate an instance of some reference type e.g. `new A()` and the class `A` has field `int a`.
    - * If the constructor we use does not initialize `a` directly and `a` is not assigned to some value
    - * at the declaration site, then `a` will be assigned to some `default` value, e.g. `0` for `int`.
    - *
    - * Here we do not consider default values of nested arrays of multidimensional arrays,
    - * because they depend on the way the outer array is allocated:
    - * - An array allocated using `new int[10][]` will contain 10 `null` elements.
    - * - An array allocated using `new int[10][5]` will contain 10 arrays of 5 elements,
    - *   where each element is `0`.
    - */
    -internal infix fun UtModel.isDefaultValueOf(type: ClassId): Boolean =
    -    when (this) {
    -        is UtNullModel -> type.isRefType // null is default for ref types
    -        is UtPrimitiveModel -> value == defaultByPrimitiveType[type]
    -        else -> false
    -    }
    -
    -internal infix fun UtModel.isNotDefaultValueOf(type: ClassId): Boolean = !this.isDefaultValueOf(type)
    -
    -/**
    - * If the model contains a store for the given [index], return the model of this store.
    - * Otherwise, return a [UtArrayModel.constModel] of this array model.
    - */
    -internal operator fun UtArrayModel.get(index: Int): UtModel = stores[index] ?: constModel
    -
    -
    -internal fun ClassId.utilMethodId(
    -    name: String,
    -    returnType: ClassId,
    -    vararg arguments: ClassId,
    -    // usually util methods are static, so this argument is true by default
    -    isStatic: Boolean = true
    -): MethodId =
    -    BuiltinMethodId(this, name, returnType, arguments.toList(), isStatic = isStatic)
    -
    -fun ClassId.toImport(): RegularImport = RegularImport(packageName, simpleNameWithEnclosings)
    -
    -// Immutable collections utils
    -
    -internal operator fun  PersistentList.plus(element: T): PersistentList =
    -    this.add(element)
    -
    -internal operator fun  PersistentList.plus(other: PersistentList): PersistentList =
    -    this.addAll(other)
    -
    -internal operator fun  PersistentSet.plus(element: T): PersistentSet =
    -    this.add(element)
    -
    -internal operator fun  PersistentSet.plus(other: PersistentSet): PersistentSet =
    -    this.addAll(other)
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/DeclarationUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/DeclarationUtils.kt
    deleted file mode 100644
    index 8100f48d40..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/DeclarationUtils.kt
    +++ /dev/null
    @@ -1,15 +0,0 @@
    -package org.utbot.framework.codegen.model.constructor.util
    -
    -import org.utbot.framework.plugin.api.UtModel
    -import org.utbot.framework.plugin.api.UtNullModel
    -import org.utbot.framework.plugin.api.UtPrimitiveModel
    -
    -/**
    - * Checks if an expected variable is needed,
    - * or it will be further asserted with assertNull or assertTrue/False.
    - */
    -fun needExpectedDeclaration(model: UtModel): Boolean {
    -    val representsNull = model is UtNullModel
    -    val representsBoolean = model is UtPrimitiveModel && model.value is Boolean
    -    return !(representsNull || representsBoolean)
    -}
    \ No newline at end of file
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/Builders.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/Builders.kt
    deleted file mode 100644
    index 55be22a41d..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/Builders.kt
    +++ /dev/null
    @@ -1,225 +0,0 @@
    -package org.utbot.framework.codegen.model.tree
    -
    -import org.utbot.framework.codegen.Import
    -import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport
    -import org.utbot.framework.codegen.model.util.CgExceptionHandler
    -import org.utbot.framework.plugin.api.ClassId
    -import org.utbot.framework.plugin.api.util.voidClassId
    -
    -interface CgBuilder {
    -    fun build(): T
    -}
    -
    -// Code entities
    -
    -class CgRegularClassFileBuilder : CgBuilder {
    -    val imports: MutableList = mutableListOf()
    -    lateinit var declaredClass: CgRegularClass
    -
    -    override fun build() = CgRegularClassFile(imports, declaredClass)
    -}
    -
    -fun buildRegularClassFile(init: CgRegularClassFileBuilder.() -> Unit) = CgRegularClassFileBuilder().apply(init).build()
    -
    -class CgTestClassFileBuilder : CgBuilder {
    -    val imports: MutableList = mutableListOf()
    -    lateinit var declaredClass: CgTestClass
    -    lateinit var testsGenerationReport: TestsGenerationReport
    -
    -    override fun build() = CgTestClassFile(imports, declaredClass, testsGenerationReport)
    -}
    -
    -fun buildTestClassFile(init: CgTestClassFileBuilder.() -> Unit) = CgTestClassFileBuilder().apply(init).build()
    -
    -class CgRegularClassBuilder : CgBuilder {
    -    lateinit var id: ClassId
    -    val annotations: MutableList = mutableListOf()
    -    var superclass: ClassId? = null
    -    val interfaces: MutableList = mutableListOf()
    -    lateinit var body: CgRegularClassBody
    -    var isStatic: Boolean = false
    -    var isNested: Boolean = false
    -
    -    override fun build() = CgRegularClass(id, annotations, superclass, interfaces, body, isStatic, isNested)
    -}
    -
    -fun buildRegularClass(init: CgRegularClassBuilder.() -> Unit) = CgRegularClassBuilder().apply(init).build()
    -
    -class CgTestClassBuilder : CgBuilder {
    -    lateinit var id: ClassId
    -    val annotations: MutableList = mutableListOf()
    -    var superclass: ClassId? = null
    -    val interfaces: MutableList = mutableListOf()
    -    var isStatic: Boolean = false
    -    var isNested: Boolean = false
    -    lateinit var body: CgTestClassBody
    -
    -    override fun build() = CgTestClass(id, annotations, superclass, interfaces, body, isStatic, isNested)
    -}
    -
    -fun buildTestClass(init: CgTestClassBuilder.() -> Unit) = CgTestClassBuilder().apply(init).build()
    -
    -class CgTestClassBodyBuilder : CgBuilder {
    -    val testMethodRegions: MutableList = mutableListOf()
    -    val staticDeclarationRegions: MutableList = mutableListOf()
    -    val nestedClassRegions: MutableList> = mutableListOf()
    -
    -    override fun build() = CgTestClassBody(testMethodRegions, staticDeclarationRegions, nestedClassRegions)
    -}
    -
    -fun buildTestClassBody(init: CgTestClassBodyBuilder.() -> Unit) = CgTestClassBodyBuilder().apply(init).build()
    -
    -class CgRegularClassBodyBuilder : CgBuilder {
    -    val content: MutableList = mutableListOf()
    -
    -    override fun build() = CgRegularClassBody(content)
    -}
    -
    -fun buildRegularClassBody(init: CgRegularClassBodyBuilder.() -> Unit) = CgRegularClassBodyBuilder().apply(init).build()
    -
    -// Methods
    -
    -interface CgMethodBuilder : CgBuilder {
    -    val name: String
    -    val returnType: ClassId
    -    val parameters: List
    -    val statements: List
    -    val exceptions: Set
    -    val annotations: List
    -    val documentation: CgDocumentationComment
    -}
    -
    -class CgTestMethodBuilder : CgMethodBuilder {
    -    override lateinit var name: String
    -    override val returnType: ClassId = voidClassId
    -    override lateinit var parameters: List
    -    override lateinit var statements: List
    -    override val exceptions: MutableSet = mutableSetOf()
    -    override val annotations: MutableList = mutableListOf()
    -    lateinit var methodType: CgTestMethodType
    -    override var documentation: CgDocumentationComment = CgDocumentationComment(emptyList())
    -
    -    override fun build() = CgTestMethod(
    -        name,
    -        returnType,
    -        parameters,
    -        statements,
    -        exceptions,
    -        annotations,
    -        methodType,
    -        documentation,
    -    )
    -}
    -
    -fun buildTestMethod(init: CgTestMethodBuilder.() -> Unit) = CgTestMethodBuilder().apply(init).build()
    -
    -class CgErrorTestMethodBuilder : CgMethodBuilder {
    -    override lateinit var name: String
    -    override val returnType: ClassId = voidClassId
    -    override val parameters: List = emptyList()
    -    override lateinit var statements: List
    -    override val exceptions: Set = emptySet()
    -    override val annotations: List = emptyList()
    -    override var documentation: CgDocumentationComment = CgDocumentationComment(emptyList())
    -
    -    override fun build() = CgErrorTestMethod(name, statements, documentation)
    -}
    -
    -fun buildErrorTestMethod(init: CgErrorTestMethodBuilder.() -> Unit) = CgErrorTestMethodBuilder().apply(init).build()
    -
    -class CgParameterizedTestDataProviderBuilder : CgMethodBuilder {
    -    override lateinit var name: String
    -
    -    override lateinit var returnType: ClassId
    -    override val parameters: List = mutableListOf()
    -    override lateinit var statements: List
    -    override val annotations: MutableList = mutableListOf()
    -    override val exceptions: MutableSet = mutableSetOf()
    -    override var documentation: CgDocumentationComment = CgDocumentationComment(emptyList())
    -
    -    override fun build() = CgParameterizedTestDataProviderMethod(name, statements, returnType, annotations, exceptions)
    -}
    -
    -fun buildParameterizedTestDataProviderMethod(
    -    init: CgParameterizedTestDataProviderBuilder.() -> Unit
    -) = CgParameterizedTestDataProviderBuilder().apply(init).build()
    -
    -// Variable declaration
    -
    -class CgDeclarationBuilder : CgBuilder {
    -    lateinit var variableType: ClassId
    -    lateinit var variableName: String
    -    var initializer: CgExpression? = null
    -    var isMutable: Boolean = false
    -
    -    override fun build() = CgDeclaration(variableType, variableName, initializer, isMutable)
    -}
    -
    -fun buildDeclaration(init: CgDeclarationBuilder.() -> Unit) = CgDeclarationBuilder().apply(init).build()
    -
    -// Variable assignment
    -class CgAssignmentBuilder : CgBuilder {
    -    lateinit var lValue: CgExpression
    -    lateinit var rValue: CgExpression
    -
    -    override fun build() = CgAssignment(lValue, rValue)
    -}
    -
    -fun buildAssignment(init: CgAssignmentBuilder.() -> Unit) = CgAssignmentBuilder().apply(init).build()
    -
    -class CgTryCatchBuilder : CgBuilder {
    -    lateinit var statements: List
    -    private val handlers: MutableList = mutableListOf()
    -    var finally: List? = null
    -    var resources: List? = null
    -
    -    override fun build(): CgTryCatch = CgTryCatch(statements, handlers, finally, resources)
    -}
    -
    -fun buildTryCatch(init: CgTryCatchBuilder.() -> Unit): CgTryCatch = CgTryCatchBuilder().apply(init).build()
    -
    -// Loops
    -interface CgLoopBuilder : CgBuilder {
    -    val condition: CgExpression
    -    val statements: List
    -}
    -
    -class CgForLoopBuilder : CgLoopBuilder {
    -    lateinit var initialization: CgDeclaration
    -    override lateinit var condition: CgExpression
    -    lateinit var update: CgStatement
    -    override lateinit var statements: List
    -
    -    override fun build() = CgForLoop(initialization, condition, update, statements)
    -}
    -
    -fun buildForLoop(init: CgForLoopBuilder.() -> Unit) = CgForLoopBuilder().apply(init).build()
    -
    -class CgWhileLoopBuilder : CgLoopBuilder {
    -    override lateinit var condition: CgExpression
    -    override val statements: MutableList = mutableListOf()
    -
    -    override fun build() = CgWhileLoop(condition, statements)
    -}
    -
    -fun buildWhileLoop(init: CgWhileLoopBuilder.() -> Unit) = CgWhileLoopBuilder().apply(init).build()
    -
    -class CgDoWhileLoopBuilder : CgLoopBuilder {
    -    override lateinit var condition: CgExpression
    -    override val statements: MutableList = mutableListOf()
    -
    -    override fun build() = CgDoWhileLoop(condition, statements)
    -}
    -
    -fun buildDoWhileLoop(init: CgDoWhileLoopBuilder.() -> Unit) = CgDoWhileLoopBuilder().apply(init).build()
    -
    -class CgForEachLoopBuilder : CgLoopBuilder {
    -    override lateinit var condition: CgExpression
    -    override lateinit var statements: List
    -    lateinit var iterable: CgReferenceExpression
    -
    -    override fun build(): CgForEachLoop = CgForEachLoop(condition, statements, iterable)
    -}
    -
    -fun buildCgForEachLoop(init: CgForEachLoopBuilder.() -> Unit): CgForEachLoop =
    -    CgForEachLoopBuilder().apply(init).build()
    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
    deleted file mode 100644
    index 05952f59f9..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt
    +++ /dev/null
    @@ -1,980 +0,0 @@
    -package org.utbot.framework.codegen.model.tree
    -
    -import org.utbot.common.WorkaroundReason
    -import org.utbot.common.workaround
    -import org.utbot.framework.codegen.Import
    -import org.utbot.framework.codegen.model.constructor.tree.CgUtilClassConstructor
    -import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport
    -import org.utbot.framework.codegen.model.util.CgExceptionHandler
    -import org.utbot.framework.codegen.model.visitor.CgRendererContext
    -import org.utbot.framework.codegen.model.visitor.CgVisitor
    -import org.utbot.framework.codegen.model.visitor.auxiliaryClassTextById
    -import org.utbot.framework.codegen.model.visitor.utilMethodTextById
    -import org.utbot.framework.plugin.api.BuiltinClassId
    -import org.utbot.framework.plugin.api.ClassId
    -import org.utbot.framework.plugin.api.ConstructorId
    -import org.utbot.framework.plugin.api.DocClassLinkStmt
    -import org.utbot.framework.plugin.api.DocCodeStmt
    -import org.utbot.framework.plugin.api.DocCustomTagStatement
    -import org.utbot.framework.plugin.api.DocMethodLinkStmt
    -import org.utbot.framework.plugin.api.DocPreTagStatement
    -import org.utbot.framework.plugin.api.DocRegularStmt
    -import org.utbot.framework.plugin.api.DocStatement
    -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.UtModel
    -import org.utbot.framework.plugin.api.util.booleanClassId
    -import org.utbot.framework.plugin.api.util.id
    -import org.utbot.framework.plugin.api.util.intClassId
    -import org.utbot.framework.plugin.api.util.objectArrayClassId
    -import org.utbot.framework.plugin.api.util.objectClassId
    -import org.utbot.framework.plugin.api.util.voidClassId
    -
    -interface CgElement {
    -    // TODO: order of cases is important here due to inheritance between some of the element types
    -    fun  accept(visitor: CgVisitor): R = visitor.run {
    -        when (val element = this@CgElement) {
    -            is CgRegularClassFile -> visit(element)
    -            is CgTestClassFile -> visit(element)
    -            is CgRegularClass -> visit(element)
    -            is CgTestClass -> visit(element)
    -            is CgRegularClassBody -> visit(element)
    -            is CgTestClassBody -> visit(element)
    -            is CgStaticsRegion -> visit(element)
    -            is CgSimpleRegion<*> -> visit(element)
    -            is CgTestMethodCluster -> visit(element)
    -            is CgExecutableUnderTestCluster -> visit(element)
    -            is CgAuxiliaryClass -> visit(element)
    -            is CgUtilMethod -> visit(element)
    -            is CgTestMethod -> visit(element)
    -            is CgErrorTestMethod -> visit(element)
    -            is CgParameterizedTestDataProviderMethod -> visit(element)
    -            is CgCommentedAnnotation -> visit(element)
    -            is CgSingleArgAnnotation -> visit(element)
    -            is CgMultipleArgsAnnotation -> visit(element)
    -            is CgArrayAnnotationArgument -> visit(element)
    -            is CgNamedAnnotationArgument -> visit(element)
    -            is CgSingleLineComment -> visit(element)
    -            is CgTripleSlashMultilineComment -> visit(element)
    -            is CgMultilineComment -> visit(element)
    -            is CgDocumentationComment -> visit(element)
    -            is CgDocPreTagStatement -> visit(element)
    -            is CgCustomTagStatement -> visit(element)
    -            is CgDocCodeStmt -> visit(element)
    -            is CgDocRegularStmt -> visit(element)
    -            is CgDocClassLinkStmt -> visit(element)
    -            is CgDocMethodLinkStmt -> visit(element)
    -            is CgAnonymousFunction -> visit(element)
    -            is CgReturnStatement -> visit(element)
    -            is CgArrayElementAccess -> visit(element)
    -            is CgSpread -> visit(element)
    -            is CgLessThan -> visit(element)
    -            is CgGreaterThan -> visit(element)
    -            is CgEqualTo -> visit(element)
    -            is CgIncrement -> visit(element)
    -            is CgDecrement -> visit(element)
    -            is CgTryCatch -> visit(element)
    -            is CgInnerBlock -> visit(element)
    -            is CgForLoop -> visit(element)
    -            is CgWhileLoop -> visit(element)
    -            is CgDoWhileLoop -> visit(element)
    -            is CgBreakStatement -> visit(element)
    -            is CgContinueStatement -> visit(element)
    -            is CgDeclaration -> visit(element)
    -            is CgAssignment -> visit(element)
    -            is CgTypeCast -> visit(element)
    -            is CgIsInstance -> visit(element)
    -            is CgThisInstance -> visit(element)
    -            is CgNotNullAssertion -> visit(element)
    -            is CgVariable -> visit(element)
    -            is CgParameterDeclaration -> visit(element)
    -            is CgLiteral -> visit(element)
    -            is CgNonStaticRunnable -> visit(element)
    -            is CgStaticRunnable -> visit(element)
    -            is CgAllocateInitializedArray -> visit(element)
    -            is CgArrayInitializer -> visit(element)
    -            is CgAllocateArray -> visit(element)
    -            is CgEnumConstantAccess -> visit(element)
    -            is CgFieldAccess -> visit(element)
    -            is CgStaticFieldAccess -> visit(element)
    -            is CgIfStatement -> visit(element)
    -            is CgSwitchCaseLabel -> visit(element)
    -            is CgSwitchCase -> visit(element)
    -            is CgLogicalAnd -> visit(element)
    -            is CgLogicalOr -> visit(element)
    -            is CgGetLength -> visit(element)
    -            is CgGetJavaClass -> visit(element)
    -            is CgGetKotlinClass -> visit(element)
    -            is CgStatementExecutableCall -> visit(element)
    -            is CgConstructorCall -> visit(element)
    -            is CgMethodCall -> visit(element)
    -            is CgThrowStatement -> visit(element)
    -            is CgErrorWrapper -> visit(element)
    -            is CgEmptyLine -> visit(element)
    -            else -> throw IllegalArgumentException("Can not visit element of type ${element::class}")
    -        }
    -    }
    -}
    -
    -// Code entities
    -
    -sealed class AbstractCgClassFile> : CgElement {
    -    abstract val imports: List
    -    abstract val declaredClass: T
    -}
    -
    -data class CgRegularClassFile(
    -    override val imports: List,
    -    override val declaredClass: CgRegularClass
    -) : AbstractCgClassFile()
    -
    -data class CgTestClassFile(
    -    override val imports: List,
    -    override val declaredClass: CgTestClass,
    -    val testsGenerationReport: TestsGenerationReport
    -) : AbstractCgClassFile()
    -
    -sealed class AbstractCgClass : CgElement {
    -    abstract val id: ClassId
    -    abstract val annotations: List
    -    abstract val superclass: ClassId?
    -    abstract val interfaces: List
    -    abstract val body: T
    -    abstract val isStatic: Boolean
    -    abstract val isNested: Boolean
    -
    -    val packageName
    -        get() = id.packageName
    -
    -    val simpleName
    -        get() = id.simpleName
    -}
    -
    -/**
    - * This class represents any class that we may want to generate other than the test class.
    - * At the moment the only such case is the generation of util class UtUtils.
    - *
    - * The difference with [CgTestClass] is in the body.
    - * The structure of a test class body is fixed (we know what it should contain),
    - * whereas an arbitrary class could contain anything.
    - * For example, the body of UtUtils class contains a comment with information
    - * about the version of UTBot it was generated with, and all the util methods.
    - *
    - * @see CgUtilClassConstructor
    - */
    -class CgRegularClass(
    -    override val id: ClassId,
    -    override val annotations: List,
    -    override val superclass: ClassId?,
    -    override val interfaces: List,
    -    override val body: CgRegularClassBody,
    -    override val isStatic: Boolean,
    -    override val isNested: Boolean
    -) : AbstractCgClass()
    -
    -data class CgTestClass(
    -    override val id: ClassId,
    -    override val annotations: List,
    -    override val superclass: ClassId?,
    -    override val interfaces: List,
    -    override val body: CgTestClassBody,
    -    override val isStatic: Boolean,
    -    override val isNested: Boolean
    -) : AbstractCgClass()
    -
    -
    -sealed class AbstractCgClassBody : CgElement
    -
    -data class CgRegularClassBody(val content: List) : AbstractCgClassBody()
    -
    -/**
    - * Body of the test class.
    - * @property testMethodRegions regions containing the test methods
    - * @property staticDeclarationRegions regions containing static declarations.
    - * This is usually util methods and data providers.
    - * In Kotlin all static declarations must be grouped together in a companion object.
    - * In Java there is no such restriction, but for uniformity we are grouping
    - * Java static declarations together as well. It can also improve code readability.
    - */
    -data class CgTestClassBody(
    -    val testMethodRegions: List,
    -    val staticDeclarationRegions: List,
    -    val nestedClassRegions: List>
    -) : AbstractCgClassBody()
    -
    -/**
    - * A class representing the IntelliJ IDEA's regions.
    - * A region is a part of code between the special starting and ending comments.
    - *
    - * [header] The header of the region.
    - */
    -sealed class CgRegion : CgElement {
    -    abstract val header: String?
    -    abstract val content: List
    -}
    -
    -open class CgSimpleRegion(
    -    override val header: String?,
    -    override val content: List
    -) : CgRegion()
    -
    -/**
    - * A region that stores some static declarations, e.g. data providers or util methods.
    - * There may be more than one static region in a class and they all are stored
    - * in a [CgTestClassBody.staticDeclarationRegions].
    - * In case of Kotlin, they all will be rendered inside of a companion object.
    - */
    -class CgStaticsRegion(
    -    override val header: String?,
    -    override val content: List
    -) : CgSimpleRegion(header, content)
    -
    -data class CgTestMethodCluster(
    -    override val header: String?,
    -    val description: CgTripleSlashMultilineComment?,
    -    override val content: List
    -) : CgRegion()
    -
    -/**
    - * Stores all clusters (ERROR, successful, timeouts, etc.) for executable under test.
    - */
    -data class CgExecutableUnderTestCluster(
    -    override val header: String?,
    -    override val content: List>
    -) : CgRegion>()
    -
    -/**
    - * Util entity is either an instance of [CgAuxiliaryClass] or [CgUtilMethod].
    - * Util methods are the helper methods that we use in our generated tests,
    - * and auxiliary classes are the classes that util methods use.
    - */
    -sealed class CgUtilEntity : CgElement {
    -    internal abstract fun getText(rendererContext: CgRendererContext): String
    -}
    -
    -/**
    - * This class represents classes that are required by our util methods.
    - * For example, class `CapturedArgument` that is used by util methods that construct lambda values.
    - *
    - * **Note** that we call such classes **auxiliary** instead of **util** in order to avoid confusion
    - * with class `org.utbot.runtime.utils.UtUtils`, which is generally called an **util class**.
    - * `UtUtils` is a class that contains all our util methods and **auxiliary classes**.
    - */
    -data class CgAuxiliaryClass(val id: ClassId) : CgUtilEntity() {
    -    override fun getText(rendererContext: CgRendererContext): String {
    -        return rendererContext.utilMethodProvider
    -            .auxiliaryClassTextById(id, rendererContext.codegenLanguage)
    -    }
    -}
    -
    -/**
    - * This class does not inherit from [CgMethod], because it only needs an [id],
    - * and it does not need to have info about all the other properties of [CgMethod].
    - * This is because util methods are hardcoded. On the rendering stage their text
    - * is retrieved by their [MethodId].
    - *
    - * @property id identifier of the util method.
    - */
    -data class CgUtilMethod(val id: MethodId) : CgUtilEntity() {
    -    override fun getText(rendererContext: CgRendererContext): String {
    -        return with(rendererContext) {
    -            rendererContext.utilMethodProvider
    -                .utilMethodTextById(id, mockFrameworkUsed, mockFramework, codegenLanguage)
    -        }
    -    }
    -}
    -
    -// Methods
    -
    -sealed class CgMethod(open val isStatic: Boolean) : CgElement {
    -    abstract val name: String
    -    abstract val returnType: ClassId
    -    abstract val parameters: List
    -    abstract val statements: List
    -    abstract val exceptions: Set
    -    abstract val annotations: List
    -    abstract val documentation: CgDocumentationComment
    -    abstract val requiredFields: List
    -}
    -
    -class CgTestMethod(
    -    override val name: String,
    -    override val returnType: ClassId,
    -    override val parameters: List,
    -    override val statements: List,
    -    override val exceptions: Set,
    -    override val annotations: List,
    -    val type: CgTestMethodType,
    -    override val documentation: CgDocumentationComment = CgDocumentationComment(emptyList()),
    -    override val requiredFields: List = emptyList(),
    -) : CgMethod(false)
    -
    -class CgErrorTestMethod(
    -    override val name: String,
    -    override val statements: List,
    -    override val documentation: CgDocumentationComment = CgDocumentationComment(emptyList())
    -) : CgMethod(false) {
    -    override val exceptions: Set = emptySet()
    -    override val returnType: ClassId = voidClassId
    -    override val parameters: List = emptyList()
    -    override val annotations: List = emptyList()
    -    override val requiredFields: List = emptyList()
    -}
    -
    -class CgParameterizedTestDataProviderMethod(
    -    override val name: String,
    -    override val statements: List,
    -    override val returnType: ClassId,
    -    override val annotations: List,
    -    override val exceptions: Set,
    -) : CgMethod(isStatic = true) {
    -    override val parameters: List = emptyList()
    -    override val documentation: CgDocumentationComment = CgDocumentationComment(emptyList())
    -    override val requiredFields: List = emptyList()
    -}
    -
    -enum class CgTestMethodType(val displayName: String) {
    -    SUCCESSFUL("Successful tests"),
    -    FAILING("Failing tests (with exceptions)"),
    -    TIMEOUT("Failing tests (with timeout)"),
    -    CRASH("Possibly crashing tests"),
    -    PARAMETRIZED("Parametrized tests");
    -
    -    override fun toString(): String = displayName
    -}
    -
    -// Annotations
    -
    -abstract class CgAnnotation : CgElement {
    -    abstract val classId: ClassId
    -}
    -
    -class CgCommentedAnnotation(val annotation: CgAnnotation) : CgAnnotation() {
    -    override val classId: ClassId = annotation.classId
    -}
    -
    -class CgSingleArgAnnotation(
    -    override val classId: ClassId,
    -    val argument: CgExpression
    -) : CgAnnotation()
    -
    -class CgMultipleArgsAnnotation(
    -    override val classId: ClassId,
    -    val arguments: MutableList
    -) : CgAnnotation()
    -
    -class CgArrayAnnotationArgument(
    -    val values: List
    -) : CgExpression {
    -    override val type: ClassId = objectArrayClassId // TODO: is this type correct?
    -}
    -
    -class CgNamedAnnotationArgument(
    -    val name: String,
    -    val value: CgExpression
    -) : CgElement
    -
    -// Statements
    -
    -interface CgStatement : CgElement
    -
    -// Comments
    -
    -sealed class CgComment : CgStatement
    -
    -class CgSingleLineComment(val comment: String) : CgComment()
    -
    -/**
    - * A comment that consists of multiple lines.
    - * The appearance of such comment may vary depending
    - * on the [CgAbstractMultilineComment] inheritor being used.
    - * Each inheritor is rendered differently.
    - */
    -sealed class CgAbstractMultilineComment : CgComment() {
    -    abstract val lines: List
    -}
    -
    -/**
    - * Multiline comment where each line starts with ///
    - */
    -class CgTripleSlashMultilineComment(override val lines: List) : CgAbstractMultilineComment()
    -
    -/**
    - * Classic Java multiline comment starting with /* and ending with */
    - */
    -class CgMultilineComment(override val lines: List) : CgAbstractMultilineComment() {
    -    constructor(line: String) : this(listOf(line))
    -}
    -
    -//class CgDocumentationComment(val lines: List) : CgComment {
    -//    constructor(text: String?) : this(text?.split("\n") ?: listOf())
    -//}
    -class CgDocumentationComment(val lines: List) : CgComment() {
    -    constructor(text: String?) : this(text?.split("\n")?.map { CgDocRegularStmt(it) }?.toList() ?: listOf())
    -
    -    override fun equals(other: Any?): Boolean =
    -        if (other is CgDocumentationComment) this.hashCode() == other.hashCode() else false
    -
    -    override fun hashCode(): Int = lines.hashCode()
    -}
    -
    -sealed class CgDocStatement : CgStatement { //todo is it really CgStatement or maybe something else?
    -    abstract fun isEmpty(): Boolean
    -}
    -
    -sealed class CgDocTagStatement(val content: List) : CgDocStatement() {
    -    override fun isEmpty(): Boolean = content.all { it.isEmpty() }
    -}
    -
    -class CgDocPreTagStatement(content: List) : CgDocTagStatement(content) {
    -    override fun equals(other: Any?): Boolean =
    -        if (other is CgDocPreTagStatement) this.hashCode() == other.hashCode() else false
    -
    -    override fun hashCode(): Int = content.hashCode()
    -}
    -
    -/**
    - * Represents a type for statements containing custom JavaDoc tags.
    - */
    -data class CgCustomTagStatement(val statements: List) : CgDocTagStatement(statements)
    -
    -class CgDocCodeStmt(val stmt: String) : CgDocStatement() {
    -    override fun isEmpty(): Boolean = stmt.isEmpty()
    -
    -    override fun equals(other: Any?): Boolean =
    -        if (other is CgDocCodeStmt) this.hashCode() == other.hashCode() else false
    -
    -    override fun hashCode(): Int = stmt.hashCode()
    -}
    -
    -class CgDocRegularStmt(val stmt: String) : CgDocStatement() {
    -    override fun isEmpty(): Boolean = stmt.isEmpty()
    -
    -    override fun equals(other: Any?): Boolean =
    -        if (other is CgDocCodeStmt) this.hashCode() == other.hashCode() else false
    -
    -    override fun hashCode(): Int = stmt.hashCode()
    -}
    -
    -open class CgDocClassLinkStmt(val className: String) : CgDocStatement() {
    -    override fun isEmpty(): Boolean = className.isEmpty()
    -
    -    override fun equals(other: Any?): Boolean =
    -        if (other is CgDocClassLinkStmt) this.hashCode() == other.hashCode() else false
    -
    -    override fun hashCode(): Int = className.hashCode()
    -}
    -
    -class CgDocMethodLinkStmt(val methodName: String, stmt: String) : CgDocClassLinkStmt(stmt) {
    -    override fun equals(other: Any?): Boolean =
    -        if (other is CgDocMethodLinkStmt) this.hashCode() == other.hashCode() else false
    -
    -    override fun hashCode(): Int {
    -        var result = super.hashCode()
    -        result = 31 * result + methodName.hashCode()
    -        return result
    -    }
    -}
    -
    -fun convertDocToCg(stmt: DocStatement): CgDocStatement {
    -    return when (stmt) {
    -        is DocPreTagStatement -> {
    -            val stmts = stmt.content.map { convertDocToCg(it) }
    -            CgDocPreTagStatement(content = stmts)
    -        }
    -        is DocCustomTagStatement -> {
    -            val stmts = stmt.content.map { convertDocToCg(it) }
    -            CgCustomTagStatement(statements = stmts)
    -        }
    -        is DocRegularStmt -> CgDocRegularStmt(stmt = stmt.stmt)
    -        is DocClassLinkStmt -> CgDocClassLinkStmt(className = stmt.className)
    -        is DocMethodLinkStmt -> CgDocMethodLinkStmt(methodName = stmt.methodName, stmt = stmt.className)
    -        is DocCodeStmt -> CgDocCodeStmt(stmt = stmt.stmt)
    -    }
    -}
    -
    -// Anonymous function (lambda)
    -
    -class CgAnonymousFunction(
    -    override val type: ClassId,
    -    val parameters: List,
    -    val body: List
    -) : CgExpression
    -
    -// Return statement
    -
    -class CgReturnStatement(val expression: CgExpression) : CgStatement
    -
    -// Array element access
    -
    -// TODO: check nested array element access expressions e.g. a[0][1][2]
    -// TODO in general it is not CgReferenceExpression because array element can have a primitive type
    -class CgArrayElementAccess(val array: CgExpression, val index: CgExpression) : CgReferenceExpression {
    -    override val type: ClassId = array.type.elementClassId ?: objectClassId
    -}
    -
    -// Loop conditions
    -sealed class CgComparison : CgExpression {
    -    abstract val left: CgExpression
    -    abstract val right: CgExpression
    -
    -    override val type: ClassId = booleanClassId
    -}
    -
    -class CgLessThan(
    -    override val left: CgExpression,
    -    override val right: CgExpression
    -) : CgComparison()
    -
    -class CgGreaterThan(
    -    override val left: CgExpression,
    -    override val right: CgExpression
    -) : CgComparison()
    -
    -class CgEqualTo(
    -    override val left: CgExpression,
    -    override val right: CgExpression
    -) : CgComparison()
    -
    -// Increment and decrement
    -
    -class CgIncrement(val variable: CgVariable) : CgStatement
    -
    -class CgDecrement(val variable: CgVariable) : CgStatement
    -
    -// Inner block in method (keeps parent method fields visible)
    -
    -class CgInnerBlock(val statements: List) : CgStatement
    -
    -// Try-catch
    -
    -// for now finally clause is not supported
    -data class CgTryCatch(
    -    val statements: List,
    -    val handlers: List,
    -    val finally: List?,
    -    val resources: List? = null
    -) : CgStatement
    -
    -// ?: error("")
    -
    -data class CgErrorWrapper(
    -    val message: String,
    -    val expression: CgExpression,
    -) : CgExpression {
    -    override val type: ClassId
    -        get() = expression.type
    -}
    -
    -// Loops
    -
    -sealed class CgLoop : CgStatement {
    -    abstract val condition: CgExpression // TODO: how to represent conditions
    -    abstract val statements: List
    -}
    -
    -class CgForLoop(
    -    val initialization: CgDeclaration,
    -    override val condition: CgExpression,
    -    val update: CgStatement,
    -    override val statements: List
    -) : CgLoop()
    -
    -class CgWhileLoop(
    -    override val condition: CgExpression,
    -    override val statements: List
    -) : CgLoop()
    -
    -class CgDoWhileLoop(
    -    override val condition: CgExpression,
    -    override val statements: List
    -) : CgLoop()
    -
    -/**
    - * @property condition represents variable in foreach loop
    - * @property iterable represents iterable in foreach loop
    - * @property statements represents statements in foreach loop
    - */
    -class CgForEachLoop(
    -    override val condition: CgExpression,
    -    override val statements: List,
    -    val iterable: CgReferenceExpression,
    -) : CgLoop()
    -
    -// Control statements
    -
    -object CgBreakStatement : CgStatement
    -object CgContinueStatement : CgStatement
    -
    -// Variable declaration
    -
    -class CgDeclaration(
    -    val variableType: ClassId,
    -    val variableName: String,
    -    val initializer: CgExpression?,
    -    val isMutable: Boolean = false,
    -) : CgStatement {
    -    val variable: CgVariable
    -        get() = CgVariable(variableName, variableType)
    -}
    -
    -// Variable assignment
    -
    -class CgAssignment(
    -    val lValue: CgExpression,
    -    val rValue: CgExpression
    -) : CgStatement
    -
    -// Expressions
    -
    -interface CgExpression : CgStatement {
    -    val type: ClassId
    -}
    -
    -// marker interface representing expressions returning reference
    -// TODO: it seems that not all [CgValue] implementations are reference expressions
    -interface CgReferenceExpression : CgExpression
    -
    -/**
    - * Type cast model
    - *
    - * @property isSafetyCast shows if we should use "as?" instead of "as" in Kotlin code
    - */
    -class CgTypeCast(
    -    val targetType: ClassId,
    -    val expression: CgExpression,
    -    val isSafetyCast: Boolean = false,
    -) : CgExpression {
    -    override val type: ClassId = targetType
    -}
    -
    -/**
    - * Represents [java.lang.Class.isInstance] method.
    - */
    -class CgIsInstance(
    -    val classExpression: CgExpression,
    -    val value: CgExpression,
    -): CgExpression {
    -    override val type: ClassId = booleanClassId
    -}
    -
    -// Value
    -
    -// TODO in general CgLiteral is not CgReferenceExpression because it can hold primitive values
    -interface CgValue : CgReferenceExpression
    -
    -// This instance
    -
    -class CgThisInstance(override val type: ClassId) : CgValue
    -
    -// Variables
    -
    -open class CgVariable(
    -    val name: String,
    -    override val type: ClassId,
    -) : CgValue {
    -    override fun equals(other: Any?): Boolean {
    -        if (this === other) return true
    -        if (javaClass != other?.javaClass) return false
    -
    -        other as CgVariable
    -
    -        if (name != other.name) return false
    -        if (type != other.type) return false
    -
    -        return true
    -    }
    -
    -    override fun hashCode(): Int {
    -        var result = name.hashCode()
    -        result = 31 * result + type.hashCode()
    -        return result
    -    }
    -
    -    override fun toString(): String {
    -        return "${type.simpleName} $name"
    -    }
    -}
    -
    -/**
    - * If expression is a variable, then this is a variable
    - * with explicit not null annotation if this is required in language.
    - *
    - * In Kotlin the difference is in addition of "!!" to the expression
    - */
    -class CgNotNullAssertion(val expression: CgExpression) : CgValue {
    -    override val type: ClassId
    -        get() = when (val expressionType = expression.type) {
    -            is BuiltinClassId -> BuiltinClassId(
    -                name = expressionType.name,
    -                canonicalName = expressionType.canonicalName,
    -                simpleName = expressionType.simpleName,
    -                isNullable = false,
    -            )
    -            else -> ClassId(
    -                expressionType.name,
    -                expressionType.elementClassId,
    -                isNullable = false,
    -            )
    -        }
    -}
    -
    -/**
    - * Method parameters declaration
    - *
    - * @property isReferenceType is used for rendering nullable types in Kotlin codegen.
    - */
    -data class CgParameterDeclaration(
    -    val parameter: CgVariable,
    -    val isVararg: Boolean = false,
    -    val isReferenceType: Boolean = false
    -) : CgElement {
    -    constructor(name: String, type: ClassId, isReferenceType: Boolean = false) : this(
    -        CgVariable(name, type),
    -        isReferenceType = isReferenceType
    -    )
    -
    -    val name: String
    -        get() = parameter.name
    -
    -    val type: ClassId
    -        get() = parameter.type
    -}
    -
    -/**
    - * Test method parameter can be one of the following types:
    - * - argument of MUT with a certain index
    - * - result expected from MUT with the given arguments
    - * - exception expected from MUT with the given arguments
    - */
    -sealed class CgParameterKind {
    -    data class Argument(val index: Int) : CgParameterKind()
    -    data class Statics(val model: UtModel) : CgParameterKind()
    -    object ExpectedResult : CgParameterKind()
    -    object ExpectedException : CgParameterKind()
    -}
    -
    -
    -// Primitive and String literals
    -
    -class CgLiteral(override val type: ClassId, val value: Any?) : CgValue {
    -    override fun equals(other: Any?): Boolean {
    -        if (this === other) return true
    -        if (javaClass != other?.javaClass) return false
    -
    -        other as CgLiteral
    -
    -        if (type != other.type) return false
    -        if (value != other.value) return false
    -
    -        return true
    -    }
    -
    -    override fun hashCode(): Int {
    -        var result = type.hashCode()
    -        result = 31 * result + (value?.hashCode() ?: 0)
    -        return result
    -    }
    -}
    -
    -// Runnable like this::toString or (new Object())::toString (non-static) or Random::nextRandomInt (static) etc
    -abstract class CgRunnable(override val type: ClassId, val methodId: MethodId) : CgValue
    -
    -/**
    - * [referenceExpression] is "this" in this::toString or (new Object()) in (new Object())::toString (non-static)
    - */
    -class CgNonStaticRunnable(
    -    type: ClassId,
    -    val referenceExpression: CgReferenceExpression,
    -    methodId: MethodId
    -) : CgRunnable(type, methodId)
    -
    -/**
    - * [classId] is Random is Random::nextRandomInt (static) etc
    - */
    -class CgStaticRunnable(type: ClassId, val classId: ClassId, methodId: MethodId) : CgRunnable(type, methodId)
    -
    -// Array allocation
    -
    -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 {
    -        workaround(WorkaroundReason.ARRAY_ELEMENT_TYPES_ALWAYS_NULLABLE) {
    -            // for now all array element types are nullable
    -            updateElementType(elementType)
    -        }
    -    }
    -
    -    private fun updateElementType(type: ClassId): ClassId =
    -        if (type.elementClassId != null) {
    -            CgClassId(type.name, updateElementType(type.elementClassId!!), isNullable = true)
    -        } else {
    -            CgClassId(type, isNullable = true)
    -        }
    -}
    -
    -/**
    - * Allocation of an array with initialization. For example: `new String[] {"a", "b", null}`.
    - */
    -class CgAllocateInitializedArray(val initializer: CgArrayInitializer) :
    -    CgAllocateArray(initializer.arrayType, initializer.elementType, initializer.size)
    -
    -class CgArrayInitializer(val arrayType: ClassId, val elementType: ClassId, val values: List) : CgExpression {
    -    val size: Int
    -        get() = values.size
    -
    -    override val type: ClassId
    -        get() = arrayType
    -}
    -
    -
    -// Spread operator (for Kotlin, empty for Java)
    -
    -class CgSpread(override val type: ClassId, val array: CgExpression) : CgExpression
    -
    -// Enum constant
    -
    -data class CgEnumConstantAccess(
    -    val enumClass: ClassId,
    -    val name: String
    -) : CgReferenceExpression {
    -    override val type: ClassId = enumClass
    -}
    -
    -// Property access
    -
    -// TODO in general it is not CgReferenceExpression because field can have a primitive type
    -abstract class CgAbstractFieldAccess : CgReferenceExpression {
    -    abstract val fieldId: FieldId
    -
    -    override val type: ClassId
    -        get() = fieldId.type
    -}
    -
    -class CgFieldAccess(
    -    val caller: CgExpression,
    -    override val fieldId: FieldId
    -) : CgAbstractFieldAccess()
    -
    -class CgStaticFieldAccess(
    -    override val fieldId: FieldId
    -) : CgAbstractFieldAccess() {
    -    val declaringClass: ClassId = fieldId.declaringClass
    -    val fieldName: String = fieldId.name
    -}
    -
    -// Conditional statements
    -
    -class CgIfStatement(
    -    val condition: CgExpression,
    -    val trueBranch: List,
    -    val falseBranch: List? = null // false branch may be absent
    -) : CgStatement
    -
    -data class CgSwitchCaseLabel(
    -    val label: CgLiteral? = null, // have to be compile time constant (null for default label)
    -    val statements: MutableList
    -) : CgStatement
    -
    -data class CgSwitchCase(
    -    val value: CgExpression, // TODO required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
    -    val labels: List,
    -    val defaultLabel: CgSwitchCaseLabel? = null
    -) : CgStatement
    -
    -// Binary logical operators
    -
    -class CgLogicalAnd(
    -    val left: CgExpression,
    -    val right: CgExpression
    -) : CgExpression {
    -    override val type: ClassId = booleanClassId
    -}
    -
    -class CgLogicalOr(
    -    val left: CgExpression,
    -    val right: CgExpression
    -) : CgExpression {
    -    override val type: ClassId = booleanClassId
    -}
    -
    -// Acquisition of array length, e.g. args.length
    -
    -/**
    - * @param variable represents an array variable
    - */
    -class CgGetLength(val variable: CgVariable) : CgExpression {
    -    override val type: ClassId = intClassId
    -}
    -
    -// Acquisition of java or kotlin class, e.g. MyClass.class in Java, MyClass::class.java in Kotlin or MyClass::class for Kotlin classes
    -
    -sealed class CgGetClass(val classId: ClassId) : CgReferenceExpression {
    -    override val type: ClassId = Class::class.id
    -}
    -
    -class CgGetJavaClass(classId: ClassId) : CgGetClass(classId)
    -
    -class CgGetKotlinClass(classId: ClassId) : CgGetClass(classId)
    -
    -// Executable calls
    -
    -class CgStatementExecutableCall(val call: CgExecutableCall) : CgStatement
    -
    -// TODO in general it is not CgReferenceExpression because returned value can have a primitive type
    -//  (or no value can be returned)
    -abstract class CgExecutableCall : CgReferenceExpression {
    -    abstract val executableId: ExecutableId
    -    abstract val arguments: List
    -    abstract val typeParameters: TypeParameters
    -}
    -
    -class CgConstructorCall(
    -    override val executableId: ConstructorId,
    -    override val arguments: List,
    -    override val typeParameters: TypeParameters = TypeParameters()
    -) : CgExecutableCall() {
    -    override val type: ClassId = executableId.classId
    -}
    -
    -class CgMethodCall(
    -    val caller: CgExpression?,
    -    override val executableId: MethodId,
    -    override val arguments: List,
    -    override val typeParameters: TypeParameters = TypeParameters()
    -) : CgExecutableCall() {
    -    override val type: ClassId = executableId.returnType
    -}
    -
    -fun CgExecutableCall.toStatement(): CgStatementExecutableCall = CgStatementExecutableCall(this)
    -
    -// Throw statement
    -
    -class CgThrowStatement(
    -    val exception: CgExpression
    -) : CgStatement
    -
    -// Empty line
    -
    -class CgEmptyLine : CgStatement
    -
    -class CgClassId(
    -    name: String,
    -    elementClassId: ClassId? = null,
    -    override val typeParameters: TypeParameters = TypeParameters(),
    -    override val isNullable: Boolean = true,
    -) : ClassId(name, elementClassId) {
    -    constructor(
    -        classId: ClassId,
    -        typeParameters: TypeParameters = TypeParameters(),
    -        isNullable: Boolean = true,
    -    ) : this(classId.name, classId.elementClassId, typeParameters, isNullable)
    -}
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/ClassIdUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/ClassIdUtil.kt
    deleted file mode 100644
    index a215965853..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/ClassIdUtil.kt
    +++ /dev/null
    @@ -1,46 +0,0 @@
    -package org.utbot.framework.codegen.model.util
    -
    -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.util.allDeclaredFieldIds
    -import org.utbot.framework.plugin.api.util.id
    -import org.utbot.framework.plugin.api.util.isArray
    -
    -/**
    - * For now we will count class accessible if it is:
    - * - Public or package-private within package [packageName].
    - * - It's outer class (if exists) is accessible too.
    - * NOTE: local and synthetic classes are considered as inaccessible.
    - * NOTE: protected classes cannot be accessed because test class does not extend any classes.
    - *
    - * @param packageName name of the package we check accessibility from
    - */
    -infix fun ClassId.isAccessibleFrom(packageName: String): Boolean {
    -    if (this.isLocal || this.isAnonymous || this.isSynthetic) {
    -        return false
    -    }
    -
    -    val outerClassId = outerClass?.id
    -    if (outerClassId != null && !outerClassId.isAccessibleFrom(packageName)) {
    -        return false
    -    }
    -
    -    return if (this.isArray) {
    -        elementClassId!!.isAccessibleFrom(packageName)
    -    } else {
    -        isPublic || (this.packageName == packageName && (isPackagePrivate || isProtected))
    -    }
    -}
    -
    -/**
    - * Returns field of [this], such that [methodId] is a getter for it (or null if methodId doesn't represent a getter)
    - */
    -internal fun ClassId.fieldThatIsGotWith(methodId: MethodId): FieldId? =
    -    allDeclaredFieldIds.singleOrNull { !it.isStatic && it.getter == methodId }
    -
    -/**
    - * Returns field of [this], such that [methodId] is a setter for it (or null if methodId doesn't represent a setter)
    - */
    -internal fun ClassId.fieldThatIsSetWith(methodId: MethodId): FieldId? =
    -    allDeclaredFieldIds.singleOrNull { !it.isStatic && it.setter == methodId }
    \ No newline at end of file
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/ClassNameUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/ClassNameUtil.kt
    deleted file mode 100644
    index 35815952e7..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/ClassNameUtil.kt
    +++ /dev/null
    @@ -1,10 +0,0 @@
    -package org.utbot.framework.codegen.model.util
    -
    -/**
    - * Creates a name of test class.
    - * We need the name in code and the name of test class file be similar.
    - * On this way we need to avoid symbols like '$'.
    - */
    -fun createTestClassName(name: String): String = name
    -    .substringAfterLast('.')
    -    .replace('\$', '_')
    \ No newline at end of file
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt
    deleted file mode 100644
    index 0a905ccdfa..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt
    +++ /dev/null
    @@ -1,92 +0,0 @@
    -package org.utbot.framework.codegen.model.util
    -
    -import org.utbot.framework.codegen.Junit4
    -import org.utbot.framework.codegen.Junit5
    -import org.utbot.framework.codegen.TestFramework
    -import org.utbot.framework.codegen.TestNg
    -import org.utbot.framework.plugin.api.MockFramework
    -
    -data class Patterns(
    -    val moduleLibraryPatterns: List,
    -    val libraryPatterns: List,
    -)
    -
    -fun TestFramework.patterns(): Patterns {
    -    val moduleLibraryPatterns = when (this) {
    -        Junit4 -> junit4ModulePatterns
    -        Junit5 -> junit5ModulePatterns
    -        TestNg -> testNgModulePatterns
    -    }
    -    val libraryPatterns = when (this) {
    -        Junit4 -> junit4Patterns
    -        Junit5 -> junit5Patterns
    -        TestNg -> testNgPatterns
    -    }
    -
    -    return Patterns(moduleLibraryPatterns, libraryPatterns)
    -}
    -
    -
    -fun TestFramework.parametrizedTestsPatterns(): Patterns {
    -    val moduleLibraryPatterns = when (this) {
    -        Junit4 -> emptyList()
    -        Junit5 -> emptyList()   // emptyList here because JUnit5 module may not be enough for parametrized tests if :junit-jupiter-params: is not installed
    -        TestNg -> testNgModulePatterns
    -    }
    -    val libraryPatterns = when (this) {
    -        Junit4 -> emptyList()
    -        Junit5 -> junit5ParametrizedTestsPatterns
    -        TestNg -> testNgPatterns
    -    }
    -
    -    return Patterns(moduleLibraryPatterns, libraryPatterns)
    -}
    -
    -
    -fun MockFramework.patterns(): Patterns {
    -    val moduleLibraryPatterns = when (this) {
    -        MockFramework.MOCKITO -> mockitoModulePatterns
    -    }
    -    val libraryPatterns = when (this) {
    -        MockFramework.MOCKITO -> mockitoPatterns
    -    }
    -
    -    return Patterns(moduleLibraryPatterns, libraryPatterns)
    -}
    -
    -val JUNIT_4_JAR_PATTERN = Regex("junit-4(\\.1[2-9])(\\.[0-9]+)?")
    -val JUNIT_4_MVN_PATTERN = Regex("junit:junit:4(\\.1[2-9])(\\.[0-9]+)?")
    -val junit4Patterns = listOf(JUNIT_4_JAR_PATTERN, JUNIT_4_MVN_PATTERN)
    -val junit4ModulePatterns = listOf(JUNIT_4_JAR_PATTERN, JUNIT_4_MVN_PATTERN)
    -
    -val JUNIT_5_JAR_PATTERN = Regex("junit-jupiter-5(\\.[0-9]+){1,2}")
    -val JUNIT_5_MVN_PATTERN = Regex("org\\.junit\\.jupiter:junit-jupiter-api:5(\\.[0-9]+){1,2}")
    -val JUNIT_5_BASIC_PATTERN = Regex("JUnit5\\.4")
    -val junit5Patterns = listOf(JUNIT_5_JAR_PATTERN, JUNIT_5_MVN_PATTERN, JUNIT_5_BASIC_PATTERN)
    -
    -val JUNIT_5_PARAMETRIZED_JAR_PATTERN = Regex("junit-jupiter-params-5(\\.[0-9]+){1,2}")
    -val JUNIT_5_PARAMETRIZED_MVN_PATTERN = Regex("org\\.junit\\.jupiter\\.junit-jupiter-params:5(\\.[0-9]+){1,2}")
    -val junit5ParametrizedTestsPatterns = listOf(JUNIT_5_JAR_PATTERN, JUNIT_5_BASIC_PATTERN,
    -    JUNIT_5_PARAMETRIZED_JAR_PATTERN, JUNIT_5_PARAMETRIZED_MVN_PATTERN)
    -
    -val JUNIT5_BASIC_MODULE_PATTERN = Regex("junit-jupiter")
    -val junit5ModulePatterns = listOf(JUNIT5_BASIC_MODULE_PATTERN)
    -
    -val TEST_NG_JAR_PATTERN = Regex("testng-[0-9](\\.[0-9]+){2}")
    -val TEST_NG_MVN_PATTERN = Regex("org\\.testng:testng:[0-9](\\.[0-9]+){2}")
    -val TEST_NG_BASIC_PATTERN = Regex("testng")
    -val testNgPatterns = listOf(TEST_NG_JAR_PATTERN, TEST_NG_MVN_PATTERN, TEST_NG_BASIC_PATTERN)
    -
    -val TEST_NG_BASIC_MODULE_PATTERN = Regex("testng")
    -val testNgModulePatterns = listOf(TEST_NG_BASIC_MODULE_PATTERN)
    -
    -val MOCKITO_JAR_PATTERN = Regex("mockito-core-[3-9](\\.[0-9]+){2}")
    -val MOCKITO_MVN_PATTERN = Regex("org\\.mockito:mockito-core:[3-9](\\.[0-9]+){2}")
    -val mockitoPatterns = listOf(MOCKITO_JAR_PATTERN, MOCKITO_MVN_PATTERN)
    -
    -val MOCKITO_BASIC_MODULE_PATTERN = Regex("mockito-core")
    -val mockitoModulePatterns = listOf(MOCKITO_BASIC_MODULE_PATTERN)
    -
    -const val MOCKITO_EXTENSIONS_FOLDER = "mockito-extensions"
    -const val MOCKITO_MOCKMAKER_FILE_NAME = "org.mockito.plugins.MockMaker"
    -val MOCKITO_EXTENSIONS_FILE_CONTENT = "mock-maker-inline"
    \ No newline at end of file
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyUtils.kt
    deleted file mode 100644
    index cf09cbc321..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyUtils.kt
    +++ /dev/null
    @@ -1,97 +0,0 @@
    -package org.utbot.framework.codegen.model.util
    -
    -import org.utbot.framework.concrete.UtExecutionInstrumentation
    -import org.utbot.framework.plugin.api.MockFramework
    -import java.io.File
    -import java.util.jar.JarFile
    -import mu.KotlinLogging
    -
    -private val logger = KotlinLogging.logger {}
    -
    -/**
    - * Checks that dependency paths contains some frameworks
    - * and their versions correspond to our requirements.
    - *
    - * Note: [UtExecutionInstrumentation] must be in dependency path too
    - * as it is used by Engine in the child process in Concrete Executor.
    - */
    -fun checkFrameworkDependencies(dependencyPaths: String?) {
    -    if (dependencyPaths.isNullOrEmpty()) {
    -        error("Dependency paths is empty, no test framework and mock framework to generate tests")
    -    }
    -
    -    //TODO: JIRA:1659
    -    // check (somehow) that [UtExecutionInstrumentation] is in dependency path: in one of jars or folders
    -
    -    val dependencyPathsSequence = dependencyPaths.splitToSequence(File.pathSeparatorChar)
    -
    -    val dependencyNames = dependencyPathsSequence
    -        .mapNotNull { findDependencyName(it) }
    -        .map { it.toLowerCase() }
    -        .toSet()
    -
    -//todo: stopped working after transition to Java 11. Ask Egor to take a look.
    -//    val testFrameworkPatterns = TestFramework.allItems.map { it.patterns() }
    -//    val testFrameworkFound = dependencyNames.matchesAnyOf(testFrameworkPatterns) ||
    -//            dependencyPathsSequence.any { checkDependencyIsFatJar(it) }
    -//
    -//    if (!testFrameworkFound) {
    -//        error("""
    -//          Test frameworks are not found in dependency path $dependencyPaths, dependency names are:
    -//          ${dependencyNames.joinToString(System.lineSeparator())}
    -//          """
    -//        )
    -//    }
    -
    -    val mockFrameworkPatterns = MockFramework.allItems.map { it.patterns() }
    -    val mockFrameworkFound = dependencyNames.matchesAnyOf(mockFrameworkPatterns) ||
    -            dependencyPathsSequence.any { checkDependencyIsFatJar(it) }
    -
    -    if (!mockFrameworkFound) {
    -        error("""
    -          Mock frameworks are not found in dependency path $dependencyPaths, dependency names are:
    -          ${dependencyNames.joinToString(System.lineSeparator())}
    -          """
    -        )
    -    }
    -}
    -
    -private fun Set.matchesAnyOf(patterns: List): Boolean {
    -    val expressions = patterns.flatMap { it.moduleLibraryPatterns + it.libraryPatterns }
    -    return any { libraryName ->
    -        expressions.any { expr -> libraryName.let { expr.containsMatchIn(it) } }
    -    }
    -}
    -
    -private fun findDependencyName(jarPath: String): String? {
    -    try {
    -        val attributes = JarFile(jarPath).manifest.mainAttributes
    -
    -        val bundleName = attributes.getValue("Bundle-SymbolicName")
    -        val bundleVersion = attributes.getValue("Bundle-Version")
    -        val moduleName = attributes.getValue("Automatic-Module-Name")
    -        val implementationTitle = attributes.getValue("Implementation-Title")
    -        val implementationVersion = attributes.getValue("Implementation-Version")
    -
    -        if (bundleName != null) return "$bundleName:$bundleVersion"
    -        if (moduleName != null) return "$moduleName:$implementationTitle:$implementationVersion"
    -    } catch (e: Exception) {
    -        logger.warn { "Unexpected error during parsing $jarPath manifest file $e" }
    -    }
    -
    -    return null
    -}
    -
    -// We consider Fat JARs contain test frameworks and mock frameworks in the dependencies.
    -private fun checkDependencyIsFatJar(jarPath: String): Boolean {
    -    try {
    -        val attributes = JarFile(jarPath).manifest.mainAttributes
    -        val jarType = attributes.getValue("JAR-Type")
    -
    -        return jarType == "Fat JAR"
    -    } catch (e: Exception) {
    -        logger.warn { "Unexpected error during parsing $jarPath manifest file $e" }
    -    }
    -
    -    return false
    -}
    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
    deleted file mode 100644
    index 80583ef6ea..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DslUtil.kt
    +++ /dev/null
    @@ -1,111 +0,0 @@
    -package org.utbot.framework.codegen.model.util
    -
    -import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider
    -import org.utbot.framework.codegen.model.constructor.tree.CgMethodConstructor
    -import org.utbot.framework.codegen.model.tree.CgArrayElementAccess
    -import org.utbot.framework.codegen.model.tree.CgDecrement
    -import org.utbot.framework.codegen.model.tree.CgEqualTo
    -import org.utbot.framework.codegen.model.tree.CgExpression
    -import org.utbot.framework.codegen.model.tree.CgFieldAccess
    -import org.utbot.framework.codegen.model.tree.CgGetClass
    -import org.utbot.framework.codegen.model.tree.CgGetJavaClass
    -import org.utbot.framework.codegen.model.tree.CgGetKotlinClass
    -import org.utbot.framework.codegen.model.tree.CgGetLength
    -import org.utbot.framework.codegen.model.tree.CgGreaterThan
    -import org.utbot.framework.codegen.model.tree.CgIncrement
    -import org.utbot.framework.codegen.model.tree.CgLessThan
    -import org.utbot.framework.codegen.model.tree.CgLiteral
    -import org.utbot.framework.codegen.model.tree.CgStaticFieldAccess
    -import org.utbot.framework.codegen.model.tree.CgVariable
    -import org.utbot.framework.plugin.api.ClassId
    -import org.utbot.framework.plugin.api.CodegenLanguage
    -import org.utbot.framework.plugin.api.FieldId
    -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.isArray
    -import org.utbot.framework.plugin.api.util.longClassId
    -import org.utbot.framework.plugin.api.util.objectClassId
    -import org.utbot.framework.plugin.api.util.shortClassId
    -import org.utbot.framework.plugin.api.util.stringClassId
    -
    -fun CgExpression.at(index: Any?): CgArrayElementAccess =
    -    CgArrayElementAccess(this, index.resolve())
    -
    -infix fun CgExpression.equalTo(other: Any?): CgEqualTo =
    -    CgEqualTo(this, other.resolve())
    -
    -infix fun CgExpression.lessThan(other: Any?): CgLessThan =
    -    CgLessThan(this, other.resolve())
    -
    -infix fun CgExpression.greaterThan(other: Any?): CgGreaterThan =
    -    CgGreaterThan(this, other.resolve())
    -
    -// Literals
    -
    -// TODO: is it OK to use Object as a type of null literal?
    -fun nullLiteral() = CgLiteral(objectClassId, null)
    -
    -fun intLiteral(num: Int) = CgLiteral(intClassId, num)
    -
    -fun longLiteral(num: Long) = CgLiteral(longClassId, num)
    -
    -fun byteLiteral(num: Byte) = CgLiteral(byteClassId, num)
    -
    -fun shortLiteral(num: Short) = CgLiteral(shortClassId, num)
    -
    -fun floatLiteral(num: Float) = CgLiteral(floatClassId, num)
    -
    -fun doubleLiteral(num: Double) = CgLiteral(doubleClassId, num)
    -
    -fun booleanLiteral(b: Boolean) = CgLiteral(booleanClassId, b)
    -
    -fun charLiteral(c: Char) = CgLiteral(charClassId, c)
    -
    -fun stringLiteral(string: String) = CgLiteral(stringClassId, string)
    -
    -// Get array length
    -
    -/**
    - * Returns length field access for array type variable and [UtilMethodProvider.getArrayLengthMethodId] call otherwise.
    - */
    -internal fun CgVariable.length(methodConstructor: CgMethodConstructor): CgExpression {
    -    val thisVariable = this
    -
    -    return if (type.isArray) {
    -        CgGetLength(thisVariable)
    -    } else {
    -        with(methodConstructor) { utilsClassId[getArrayLength](thisVariable) }
    -    }
    -}
    -
    -// Increment and decrement
    -
    -fun CgVariable.inc(): CgIncrement = CgIncrement(this)
    -
    -fun CgVariable.dec(): CgDecrement = CgDecrement(this)
    -
    -fun Any?.resolve(): CgExpression = when (this) {
    -    null -> nullLiteral()
    -    is Int -> intLiteral(this)
    -    is Long -> longLiteral(this)
    -    is Byte -> byteLiteral(this)
    -    is Short -> shortLiteral(this)
    -    is Float -> floatLiteral(this)
    -    is Double -> doubleLiteral(this)
    -    is Boolean -> booleanLiteral(this)
    -    is Char -> charLiteral(this)
    -    is String -> stringLiteral(this)
    -    is CgExpression -> this
    -    else -> error("Expected primitive, string, null or CgExpression, but got: ${this::class}")
    -}
    -
    -fun Array<*>.resolve(): List = map { it.resolve() }
    -
    -fun classLiteralAnnotationArgument(id: ClassId, codegenLanguage: CodegenLanguage): CgGetClass = when (codegenLanguage) {
    -    CodegenLanguage.JAVA -> CgGetJavaClass(id)
    -    CodegenLanguage.KOTLIN -> CgGetKotlinClass(id)
    -}
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/FieldIdUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/FieldIdUtil.kt
    deleted file mode 100644
    index 8df76d3d39..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/FieldIdUtil.kt
    +++ /dev/null
    @@ -1,68 +0,0 @@
    -package org.utbot.framework.codegen.model.util
    -
    -import org.utbot.framework.codegen.model.constructor.context.CgContext
    -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.voidClassId
    -
    -/**
    - * For now we will count field accessible if it is not private and its class is also accessible,
    - * because we generate tests in the same package with the class under test,
    - * which means we can access public, protected and package-private fields
    - *
    - * @param context context in which code is generated (it is needed because the method needs to know package and language)
    - */
    -fun FieldId.isAccessibleFrom(packageName: String): Boolean {
    -    val isClassAccessible = declaringClass.isAccessibleFrom(packageName)
    -    val isAccessibleByVisibility = isPublic || (declaringClass.packageName == packageName && (isPackagePrivate || isProtected))
    -    val isAccessibleFromPackageByModifiers = isAccessibleByVisibility && !isSynthetic
    -
    -    return isClassAccessible && isAccessibleFromPackageByModifiers
    -}
    -
    -private fun FieldId.canBeReadViaGetterFrom(context: CgContext): Boolean =
    -    declaringClass.allMethods.contains(getter) && getter.isAccessibleFrom(context.testClassPackageName)
    -
    -/**
    - * Returns whether you can read field's value without reflection
    - */
    -internal infix fun FieldId.canBeReadFrom(context: CgContext): Boolean {
    -    if (context.codegenLanguage == CodegenLanguage.KOTLIN) {
    -        // Kotlin will allow direct field access for non-static fields with accessible getter
    -        if (!isStatic && canBeReadViaGetterFrom(context))
    -            return true
    -    }
    -
    -    return isAccessibleFrom(context.testClassPackageName)
    -}
    -
    -private fun FieldId.canBeSetViaSetterFrom(context: CgContext): Boolean =
    -    declaringClass.allMethods.contains(setter) && setter.isAccessibleFrom(context.testClassPackageName)
    -
    -/**
    - * Whether or not a field can be set without reflection
    - */
    -internal fun FieldId.canBeSetFrom(context: CgContext): Boolean {
    -    if (context.codegenLanguage == CodegenLanguage.KOTLIN) {
    -        // Kotlin will allow direct write access if both getter and setter is defined
    -        // !isAccessibleFrom(context) is important here because above rule applies to final fields only if they are not accessible in Java terms
    -        if (!isAccessibleFrom(context.testClassPackageName) && !isStatic && canBeReadViaGetterFrom(context) && canBeSetViaSetterFrom(context)) {
    -            return true
    -        }
    -    }
    -
    -    return isAccessibleFrom(context.testClassPackageName) && !isFinal
    -}
    -
    -/**
    - * The default getter method for field (the one which is generated by Kotlin compiler)
    - */
    -val FieldId.getter: MethodId
    -    get() = MethodId(declaringClass, "get${name.replaceFirstChar { it.uppercase() } }", type, emptyList())
    -
    -/**
    - * The default setter method for field (the one which is generated by Kotlin compiler)
    - */
    -val FieldId.setter: MethodId
    -    get() = MethodId(declaringClass, "set${name.replaceFirstChar { it.uppercase() } }", voidClassId, listOf(type))
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt
    deleted file mode 100644
    index c117ed9768..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt
    +++ /dev/null
    @@ -1,22 +0,0 @@
    -package org.utbot.framework.codegen.model.util
    -
    -import org.utbot.framework.codegen.model.tree.CgStatement
    -import org.utbot.framework.codegen.model.tree.CgVariable
    -
    -data class CgExceptionHandler(
    -        val exception: CgVariable,
    -        val statements: List
    -)
    -
    -class CgExceptionHandlerBuilder {
    -    lateinit var exception: CgVariable
    -    lateinit var statements: List
    -
    -    fun build(): CgExceptionHandler {
    -        return CgExceptionHandler(exception, statements)
    -    }
    -}
    -
    -internal fun buildExceptionHandler(init: CgExceptionHandlerBuilder.() -> Unit): CgExceptionHandler {
    -    return CgExceptionHandlerBuilder().apply(init).build()
    -}
    diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt
    deleted file mode 100644
    index f97dbc5c1a..0000000000
    --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt
    +++ /dev/null
    @@ -1,963 +0,0 @@
    -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
    -import org.utbot.framework.codegen.RegularImport
    -import org.utbot.framework.codegen.StaticImport
    -import org.utbot.framework.codegen.model.UtilClassKind
    -import org.utbot.framework.codegen.model.constructor.context.CgContext
    -import org.utbot.framework.codegen.model.tree.AbstractCgClass
    -import org.utbot.framework.codegen.model.tree.AbstractCgClassBody
    -import org.utbot.framework.codegen.model.tree.AbstractCgClassFile
    -import org.utbot.framework.codegen.model.tree.CgAbstractFieldAccess
    -import org.utbot.framework.codegen.model.tree.CgAbstractMultilineComment
    -import org.utbot.framework.codegen.model.tree.CgArrayElementAccess
    -import org.utbot.framework.codegen.model.tree.CgAssignment
    -import org.utbot.framework.codegen.model.tree.CgAuxiliaryClass
    -import org.utbot.framework.codegen.model.tree.CgBreakStatement
    -import org.utbot.framework.codegen.model.tree.CgComment
    -import org.utbot.framework.codegen.model.tree.CgCommentedAnnotation
    -import org.utbot.framework.codegen.model.tree.CgComparison
    -import org.utbot.framework.codegen.model.tree.CgContinueStatement
    -import org.utbot.framework.codegen.model.tree.CgCustomTagStatement
    -import org.utbot.framework.codegen.model.tree.CgDeclaration
    -import org.utbot.framework.codegen.model.tree.CgDecrement
    -import org.utbot.framework.codegen.model.tree.CgDoWhileLoop
    -import org.utbot.framework.codegen.model.tree.CgDocClassLinkStmt
    -import org.utbot.framework.codegen.model.tree.CgDocCodeStmt
    -import org.utbot.framework.codegen.model.tree.CgDocMethodLinkStmt
    -import org.utbot.framework.codegen.model.tree.CgDocPreTagStatement
    -import org.utbot.framework.codegen.model.tree.CgDocRegularStmt
    -import org.utbot.framework.codegen.model.tree.CgDocumentationComment
    -import org.utbot.framework.codegen.model.tree.CgElement
    -import org.utbot.framework.codegen.model.tree.CgEmptyLine
    -import org.utbot.framework.codegen.model.tree.CgEnumConstantAccess
    -import org.utbot.framework.codegen.model.tree.CgErrorTestMethod
    -import org.utbot.framework.codegen.model.tree.CgErrorWrapper
    -import org.utbot.framework.codegen.model.tree.CgExecutableCall
    -import org.utbot.framework.codegen.model.tree.CgExecutableUnderTestCluster
    -import org.utbot.framework.codegen.model.tree.CgExpression
    -import org.utbot.framework.codegen.model.tree.CgFieldAccess
    -import org.utbot.framework.codegen.model.tree.CgForLoop
    -import org.utbot.framework.codegen.model.tree.CgGreaterThan
    -import org.utbot.framework.codegen.model.tree.CgIfStatement
    -import org.utbot.framework.codegen.model.tree.CgIncrement
    -import org.utbot.framework.codegen.model.tree.CgInnerBlock
    -import org.utbot.framework.codegen.model.tree.CgIsInstance
    -import org.utbot.framework.codegen.model.tree.CgLessThan
    -import org.utbot.framework.codegen.model.tree.CgLiteral
    -import org.utbot.framework.codegen.model.tree.CgLogicalAnd
    -import org.utbot.framework.codegen.model.tree.CgLogicalOr
    -import org.utbot.framework.codegen.model.tree.CgLoop
    -import org.utbot.framework.codegen.model.tree.CgMethod
    -import org.utbot.framework.codegen.model.tree.CgMethodCall
    -import org.utbot.framework.codegen.model.tree.CgMultilineComment
    -import org.utbot.framework.codegen.model.tree.CgMultipleArgsAnnotation
    -import org.utbot.framework.codegen.model.tree.CgNamedAnnotationArgument
    -import org.utbot.framework.codegen.model.tree.CgNonStaticRunnable
    -import org.utbot.framework.codegen.model.tree.CgParameterDeclaration
    -import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod
    -import org.utbot.framework.codegen.model.tree.CgRegion
    -import org.utbot.framework.codegen.model.tree.CgRegularClass
    -import org.utbot.framework.codegen.model.tree.CgRegularClassBody
    -import org.utbot.framework.codegen.model.tree.CgRegularClassFile
    -import org.utbot.framework.codegen.model.tree.CgReturnStatement
    -import org.utbot.framework.codegen.model.tree.CgSimpleRegion
    -import org.utbot.framework.codegen.model.tree.CgSingleArgAnnotation
    -import org.utbot.framework.codegen.model.tree.CgSingleLineComment
    -import org.utbot.framework.codegen.model.tree.CgSpread
    -import org.utbot.framework.codegen.model.tree.CgStatement
    -import org.utbot.framework.codegen.model.tree.CgStatementExecutableCall
    -import org.utbot.framework.codegen.model.tree.CgStaticFieldAccess
    -import org.utbot.framework.codegen.model.tree.CgStaticRunnable
    -import org.utbot.framework.codegen.model.tree.CgStaticsRegion
    -import org.utbot.framework.codegen.model.tree.CgTestClass
    -import org.utbot.framework.codegen.model.tree.CgTestClassFile
    -import org.utbot.framework.codegen.model.tree.CgTestMethod
    -import org.utbot.framework.codegen.model.tree.CgTestMethodCluster
    -import org.utbot.framework.codegen.model.tree.CgThisInstance
    -import org.utbot.framework.codegen.model.tree.CgThrowStatement
    -import org.utbot.framework.codegen.model.tree.CgTripleSlashMultilineComment
    -import org.utbot.framework.codegen.model.tree.CgTryCatch
    -import org.utbot.framework.codegen.model.tree.CgUtilMethod
    -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.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.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.isArray
    -import org.utbot.framework.plugin.api.util.isRefType
    -import org.utbot.framework.plugin.api.util.longClassId
    -import org.utbot.framework.plugin.api.util.shortClassId
    -
    -internal abstract class CgAbstractRenderer(
    -    val context: CgRendererContext,
    -    val printer: CgPrinter = CgPrinterImpl()
    -) : CgVisitor,
    -    CgPrinter by printer {
    -
    -    protected abstract val statementEnding: String
    -
    -    protected abstract val logicalAnd: String
    -    protected abstract val logicalOr: String
    -
    -    protected val regionStart: String = "///region"
    -    protected val regionEnd: String = "///endregion"
    -
    -    protected abstract val language: CodegenLanguage
    -
    -    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 element type.
    -    protected fun arrayElementsInLine(elementType: ClassId): Int {
    -        if (elementType.isRefType) return 10
    -        if (elementType.isArray) return 1
    -        return when (elementType) {
    -            intClassId, byteClassId, longClassId, charClassId -> 8
    -            booleanClassId, shortClassId, doubleClassId, floatClassId -> 6
    -            else -> error("Non primitive value of type $elementType is unexpected in array initializer")
    -        }
    -    }
    -
    -    private val MethodId.accessibleByName: Boolean
    -        get() = (context.shouldOptimizeImports && this in context.importedStaticMethods) || classId == context.generatedClass
    -
    -    override fun visit(element: CgElement) {
    -        val error =
    -            "CgRenderer has reached the top of Cg elements hierarchy and did not find a method for ${element::class}"
    -        throw IllegalArgumentException(error)
    -    }
    -
    -    override fun visit(element: AbstractCgClassFile<*>) {
    -        renderClassPackage(element.declaredClass)
    -        renderClassFileImports(element)
    -        element.declaredClass.accept(this)
    -    }
    -
    -    override fun visit(element: CgRegularClassFile) {
    -        visit(element as AbstractCgClassFile<*>)
    -    }
    -
    -    override fun visit(element: CgTestClassFile) {
    -        visit(element as AbstractCgClassFile<*>)
    -    }
    -
    -    override fun visit(element: CgRegularClass) {
    -        visit(element as AbstractCgClass<*>)
    -    }
    -
    -    override fun visit(element: CgTestClass) {
    -        visit(element as AbstractCgClass<*>)
    -    }
    -
    -    override fun visit(element: AbstractCgClassBody) {
    -        visit(element as CgElement)
    -    }
    -
    -    override fun visit(element: CgRegularClassBody) {
    -        val content = element.content
    -        for ((index, item) in content.withIndex()) {
    -            item.accept(this)
    -            println()
    -            if (index < content.lastIndex) {
    -                println()
    -            }
    -        }
    -    }
    -
    -    /**
    -     * Render the region only if it is not empty.
    -     */
    -    override fun visit(element: CgStaticsRegion) {
    -        element.render()
    -    }
    -
    -    /**
    -     * Render the region only if it is not empty.
    -     */
    -    override fun visit(element: CgSimpleRegion<*>) {
    -        element.render()
    -    }
    -
    -    /**
    -     * Render the cluster only if it is not empty.
    -     */
    -    override fun visit(element: CgTestMethodCluster) {
    -        element.render()
    -    }
    -
    -    /**
    -     * Render the cluster only if it is not empty.
    -     */
    -    override fun visit(element: CgExecutableUnderTestCluster) {
    -        // We print the next line after all contained regions to prevent gluing of region ends
    -        element.render(printLineAfterContentEnd = true)
    -    }
    -
    -    /**
    -     * Renders the region with a specific rendering for [CgTestMethodCluster.description]
    -     */
    -    private fun CgRegion<*>.render(printLineAfterContentEnd: Boolean = false) {
    -        if (content.isEmpty()) return
    -
    -        print(regionStart)
    -        header?.let { print(" $it") }
    -        println()
    -
    -        if (this is CgTestMethodCluster) description?.accept(this@CgAbstractRenderer)
    -
    -        for (method in content) {
    -            println()
    -            method.accept(this@CgAbstractRenderer)
    -        }
    -
    -        if (printLineAfterContentEnd) println()
    -
    -        println(regionEnd)
    -    }
    -
    -    override fun visit(element: CgAuxiliaryClass) {
    -        val auxiliaryClassText = element.getText(context)
    -        auxiliaryClassText.split("\n")
    -            .forEach { line -> println(line) }
    -    }
    -
    -    override fun visit(element: CgUtilMethod) {
    -        val utilMethodText = element.getText(context)
    -        utilMethodText.split("\n")
    -            .forEach { line -> println(line) }
    -    }
    -
    -    // Methods
    -
    -    override fun visit(element: CgMethod) {
    -        // TODO introduce CgBlock
    -        print(" ")
    -        visit(element.statements, printNextLine = true)
    -    }
    -
    -    override fun visit(element: CgTestMethod) {
    -        renderMethodDocumentation(element)
    -        for (annotation in element.annotations) {
    -            annotation.accept(this)
    -        }
    -        renderMethodSignature(element)
    -        visit(element as CgMethod)
    -    }
    -
    -    override fun visit(element: CgErrorTestMethod) {
    -        renderMethodDocumentation(element)
    -        renderMethodSignature(element)
    -        visit(element as CgMethod)
    -    }
    -
    -    override fun visit(element: CgParameterizedTestDataProviderMethod) {
    -        for (annotation in element.annotations) {
    -            annotation.accept(this)
    -        }
    -        renderMethodSignature(element)
    -        visit(element as CgMethod)
    -    }
    -
    -    // Annotations
    -
    -    override fun visit(element: CgCommentedAnnotation) {
    -        print("//")
    -        element.annotation.accept(this)
    -    }
    -
    -    override fun visit(element: CgSingleArgAnnotation) {
    -        print("@${element.classId.asString()}")
    -        print("(")
    -        element.argument.accept(this)
    -        println(")")
    -    }
    -
    -    override fun visit(element: CgMultipleArgsAnnotation) {
    -        print("@${element.classId.asString()}")
    -        if (element.arguments.isNotEmpty()) {
    -            print("(")
    -            element.arguments.renderSeparated()
    -            print(")")
    -        }
    -        println()
    -    }
    -
    -    override fun visit(element: CgNamedAnnotationArgument) {
    -        print(element.name)
    -        print(" = ")
    -        element.value.accept(this)
    -    }
    -
    -    // Comments
    -
    -    override fun visit(element: CgComment) {
    -        visit(element as CgElement)
    -    }
    -
    -    override fun visit(element: CgSingleLineComment) {
    -        println("// ${element.comment}")
    -    }
    -
    -    override fun visit(element: CgAbstractMultilineComment) {
    -        visit(element as CgElement)
    -    }
    -
    -    override fun visit(element: CgTripleSlashMultilineComment) {
    -        for (line in element.lines) {
    -            println("/// $line")
    -        }
    -    }
    -
    -    override fun visit(element: CgMultilineComment) {
    -        val lines = element.lines
    -        if (lines.isEmpty()) return
    -
    -        if (lines.size == 1) {
    -            print("/* ${lines.first()} */")
    -            return
    -        }
    -
    -        // print lines saving indentation
    -        print("/* ")
    -        println(lines.first())
    -        lines.subList(1, lines.lastIndex).forEach { println(it) }
    -        print(lines.last())
    -        println(" */")
    -    }
    -
    -    override fun visit(element: CgDocumentationComment) {
    -        if (element.lines.all { it.isEmpty() }) return
    -
    -        println("/**")
    -        for (line in element.lines) line.accept(this)
    -        println(" */")
    -    }
    -    override fun visit(element: CgDocPreTagStatement) {
    -        if (element.content.all { it.isEmpty() }) return
    -        println("
    ")
    -        for (stmt in element.content) stmt.accept(this)
    -        println("
    ") - } - - override fun visit(element: CgCustomTagStatement) { - if (element.statements.all { it.isEmpty() }) return - - for (stmt in element.statements) { - stmt.accept(this) - } - } - - override fun visit(element: CgDocCodeStmt) { - if (element.isEmpty()) return - - val text = element.stmt - .replace("\n", "\n * ") - //remove multiline comment symbols to avoid comment in comment effect - .replace("/*", "") - .replace("*/", "") - print("{@code $text }") - } - override fun visit(element: CgDocRegularStmt){ - if (element.isEmpty()) return - - print(element.stmt.replace("\n", "\n * ")) - } - override fun visit(element: CgDocClassLinkStmt) { - if (element.isEmpty()) return - - print(element.className) - } - override fun visit(element: CgDocMethodLinkStmt){ - if (element.isEmpty()) return - - print("${element.className}::${element.methodName}") //todo make it as link {@link org.utbot.examples.online.Loops#whileLoop(int) } - } - - /** - * Renders any block of code with curly braces. - * - * NOTE: [printNextLine] has default false value in [CgVisitor] - * - * NOTE: due to JVM restrictions for methods size - * [in 65536 bytes](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.3) - * we comment long blocks - */ - override fun visit(block: List, printNextLine: Boolean) { - println("{") - - val isBlockTooLarge = workaround(LONG_CODE_FRAGMENTS) { block.size > LARGE_CODE_BLOCK_SIZE } - - if (isBlockTooLarge) { - print("/*") - println(" This block of code is ${block.size} lines long and could lead to compilation error") - } - - withIndent { - for (statement in block) { - statement.accept(this) - } - } - - if (isBlockTooLarge) println("*/") - - print("}") - - if (printNextLine) println() - } - - // Return statement - - override fun visit(element: CgReturnStatement) { - print("return ") - element.expression.accept(this) - println(statementEnding) - } - - // Array element access - - override fun visit(element: CgArrayElementAccess) { - element.array.accept(this) - print("[") - element.index.accept(this) - print("]") - } - - // Spread operator - - override fun visit(element: CgSpread) { - element.array.accept(this) - } - - // Loop conditions - - override fun visit(element: CgComparison) { - return visit(element as CgElement) - } - - override fun visit(element: CgLessThan) { - element.left.accept(this) - print(" < ") - element.right.accept(this) - } - - override fun visit(element: CgGreaterThan) { - element.left.accept(this) - print(" > ") - element.right.accept(this) - } - - // Increment and decrement - - override fun visit(element: CgIncrement) { - print("${element.variable.name}++") - } - - override fun visit(element: CgDecrement) { - print("${element.variable.name}--") - } - - // isInstance check - - override fun visit(element: CgIsInstance) { - element.classExpression.accept(this) - print(".isInstance(") - element.value.accept(this) - print(")") - } - - // Try-catch - - override fun visit(element: CgTryCatch) { - print("try ") - // TODO: SAT-1329 actually Kotlin does not support try-with-resources so we have to use "use" method here - element.resources?.let { - println("(") - withIndent { - for (resource in element.resources) { - resource.accept(this) - } - } - print(") ") - } - // TODO introduce CgBlock - visit(element.statements) - for ((exception, statements) in element.handlers) { - print(" catch (") - renderExceptionCatchVariable(exception) - print(") ") - // TODO introduce CgBlock - visit(statements, printNextLine = element.finally == null) - } - element.finally?.let { - print(" finally ") - // TODO introduce CgBlock - visit(element.finally, printNextLine = true) - } - } - - abstract override fun visit(element: CgErrorWrapper) - - //Simple block - - abstract override fun visit(element: CgInnerBlock) - - // Loops - - override fun visit(element: CgLoop) { - return visit(element as CgElement) - } - - override fun visit(element: CgForLoop) { - renderForLoopVarControl(element) - print(") ") - // TODO introduce CgBlock - visit(element.statements) - println() - } - - override fun visit(element: CgWhileLoop) { - print("while (") - element.condition.accept(this) - print(") ") - // TODO introduce CgBlock - visit(element.statements) - println() - } - - override fun visit(element: CgDoWhileLoop) { - print("do ") - // TODO introduce CgBlock - visit(element.statements) - print(" while (") - element.condition.accept(this) - println(");") - } - - // Control statements - override fun visit(element: CgBreakStatement) { - println("break$statementEnding") - } - - override fun visit(element: CgContinueStatement) { - println("continue$statementEnding") - } - - // Variable declaration - - override fun visit(element: CgDeclaration) { - renderDeclarationLeftPart(element) - element.initializer?.let { - print(" = ") - it.accept(this) - } - println(statementEnding) - } - - // Variable assignment - - override fun visit(element: CgAssignment) { - element.lValue.accept(this) - print(" = ") - element.rValue.accept(this) - println(statementEnding) - } - - // Expressions - - override fun visit(element: CgExpression) { - visit(element as CgElement) - } - - // This instance - - override fun visit(element: CgThisInstance) { - print("this") - } - - // Variables - - override fun visit(element: CgVariable) { - print(element.name.escapeNamePossibleKeyword()) - } - - // Method parameters - - abstract override fun visit(element: CgParameterDeclaration) - - // Primitive and String literals - - override fun visit(element: CgLiteral) { - val value = with(element.value) { - when (this) { - is Byte -> toStringConstant() - is Char -> toStringConstant() - is Short -> toStringConstant() - is Int -> toStringConstant() - is Long -> toStringConstant() - is Float -> toStringConstant() - is Double -> toStringConstant() - is Boolean -> toStringConstant() - is String -> toStringConstant() - else -> "$this" - } - } - print(value) - } - - // Non-static runnable like this::toString or (new Object())::toString etc - override fun visit(element: CgNonStaticRunnable) { - // TODO we need braces for expressions like (new Object())::toString but not for this::toString - print("(") - element.referenceExpression.accept(this) - print(")::") - print(element.methodId.name) - } - - // Static runnable like Random::nextRandomInt etc - override fun visit(element: CgStaticRunnable) { - print(element.classId.asString()) - print("::") - print(element.methodId.name) - } - - // Enum constant - - override fun visit(element: CgEnumConstantAccess) { - print(element.enumClass.asString()) - print(".") - print(element.name) - } - - // Property access - - override fun visit(element: CgAbstractFieldAccess) { - visit(element as CgElement) - } - - override fun visit(element: CgFieldAccess) { - element.caller.accept(this) - print(".") - print(element.fieldId.name) - } - - override fun visit(element: CgStaticFieldAccess) { - print(element.declaringClass.asString()) - print(".") - print(element.fieldName) - } - - // Conditional statement - - override fun visit(element: CgIfStatement) { - print("if (") - element.condition.accept(this) - print(") ") - // TODO introduce CgBlock - visit(element.trueBranch) - element.falseBranch?.let { - print(" else ") - // TODO introduce CgBlock - visit(element.falseBranch) - } - println() - } - - // Binary logical operators - - override fun visit(element: CgLogicalAnd) { - element.left.accept(this) - print(" $logicalAnd ") - element.right.accept(this) - } - - override fun visit(element: CgLogicalOr) { - element.left.accept(this) - print(" $logicalOr ") - element.right.accept(this) - } - - // Executable calls - - override fun visit(element: CgStatementExecutableCall) { - element.call.accept(this) - println(statementEnding) - } - - override fun visit(element: CgExecutableCall) { - visit(element as CgElement) - } - - // TODO: consider the case of generic functions - // TODO: write tests for all cases of method call rendering (with or without caller, etc.) - override fun visit(element: CgMethodCall) { - val caller = element.caller - if (caller != null) { - // 'this' can be omitted, otherwise render caller - if (caller !is CgThisInstance) { - caller.accept(this) - renderAccess(caller) - } - } else { - // for static methods render declaring class only if required - if (!element.executableId.accessibleByName) { - val method = element.executableId - print(method.classId.asString()) - print(".") - } - } - print(element.executableId.name.escapeNamePossibleKeyword()) - - renderTypeParameters(element.typeParameters) - renderExecutableCallArguments(element) - } - - // Throw statement - - override fun visit(element: CgThrowStatement) { - print("throw ") - element.exception.accept(this) - println(statementEnding) - } - - override fun visit(element: CgEmptyLine) { - println() - } - - override fun toString(): String = printer.toString() - - protected abstract fun renderRegularImport(regularImport: RegularImport) - protected abstract fun renderStaticImport(staticImport: StaticImport) - - //we render parameters in method signature on one line or on separate lines depending their amount - protected val maxParametersAmountInOneLine = 3 - - protected abstract fun renderMethodSignature(element: CgTestMethod) - protected abstract fun renderMethodSignature(element: CgErrorTestMethod) - protected abstract fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) - - protected abstract fun renderForLoopVarControl(element: CgForLoop) - - protected abstract fun renderDeclarationLeftPart(element: CgDeclaration) - - protected abstract fun toStringConstantImpl(byte: Byte): String - protected abstract fun toStringConstantImpl(short: Short): String - protected abstract fun toStringConstantImpl(int: Int): String - protected abstract fun toStringConstantImpl(long: Long): String - protected abstract fun toStringConstantImpl(float: Float): String - - protected abstract fun renderAccess(caller: CgExpression) - protected abstract fun renderTypeParameters(typeParameters: TypeParameters) - protected abstract fun renderExecutableCallArguments(executableCall: CgExecutableCall) - - protected abstract fun renderExceptionCatchVariable(exception: CgVariable) - - protected fun getEscapedImportRendering(import: Import): String = - import.qualifiedName - .split(".") - .joinToString(".") { it.escapeNamePossibleKeyword() } - - protected fun List.printSeparated(newLines: Boolean = false) { - for ((index, element) in this.withIndex()) { - print(element) - when { - index < lastIndex -> { - print(",") - if (newLines) println() else print(" ") - } - index == lastIndex -> if (newLines) println() - } - } - } - - protected fun List.renderSeparated(newLines: Boolean = false) { - for ((index, element) in this.withIndex()) { - element.accept(this@CgAbstractRenderer) - when { - index < lastIndex -> { - print(",") - if (newLines) println() else print(" ") - } - index == lastIndex -> if (newLines) println() - } - } - } - - protected fun List.renderElements(elementsInLine: Int) { - val length = this.size - if (length <= elementsInLine) { // one-line array - for (i in 0 until length) { - val expr = this[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[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() - block() - } finally { - popIndent() - } - } - - protected open fun isAccessibleBySimpleNameImpl(classId: ClassId): Boolean = - classId in context.importedClasses || - classId.simpleName !in context.importedClasses.map { it.simpleName } && classId.packageName == context.classPackageName - - protected abstract fun escapeNamePossibleKeywordImpl(s: String): String - - protected fun String.escapeNamePossibleKeyword(): String = escapeNamePossibleKeywordImpl(this) - - protected fun ClassId.asString(): String { - if (!context.shouldOptimizeImports) return canonicalName - - // use simpleNameWithEnclosings instead of simpleName to consider nested classes case - return if (this.isAccessibleBySimpleName()) simpleNameWithEnclosings else canonicalName - } - - private fun renderClassPackage(element: AbstractCgClass<*>) { - if (element.packageName.isNotEmpty()) { - println("package ${element.packageName}${statementEnding}") - println() - } - } - - private fun renderClassFileImports(element: AbstractCgClassFile<*>) { - val regularImports = element.imports.filterIsInstance() - val staticImports = element.imports.filterIsInstance() - - for (import in regularImports) { - renderRegularImport(import) - } - if (regularImports.isNotEmpty()) { - println() - } - - for (import in staticImports) { - renderStaticImport(import) - } - if (staticImports.isNotEmpty()) { - println() - } - } - - protected abstract fun renderClassVisibility(classId: ClassId) - - protected abstract fun renderClassModality(aClass: AbstractCgClass<*>) - - private fun renderMethodDocumentation(element: CgMethod) { - element.documentation.accept(this) - } - - private fun Byte.toStringConstant() = when { - this == Byte.MAX_VALUE -> "${langPackage}.Byte.MAX_VALUE" - this == Byte.MIN_VALUE -> "${langPackage}.Byte.MIN_VALUE" - else -> toStringConstantImpl(this) - } - - private fun Char.toStringConstant() = when (this) { - '\'' -> "'\\''" - else -> "'" + StringEscapeUtils.escapeJava("$this") + "'" - } - - private fun Short.toStringConstant() = when (this) { - Short.MAX_VALUE -> "${langPackage}.Short.MAX_VALUE" - Short.MIN_VALUE -> "${langPackage}.Short.MIN_VALUE" - else -> toStringConstantImpl(this) - } - - private fun Int.toStringConstant(): String = toStringConstantImpl(this) - - private fun Long.toStringConstant() = when { - this == Long.MAX_VALUE -> "${langPackage}.Long.MAX_VALUE" - this == Long.MIN_VALUE -> "${langPackage}.Long.MIN_VALUE" - else -> toStringConstantImpl(this) - } - - private fun Float.toStringConstant() = when { - isNaN() -> "${langPackage}.Float.NaN" - this == Float.POSITIVE_INFINITY -> "${langPackage}.Float.POSITIVE_INFINITY" - this == Float.NEGATIVE_INFINITY -> "${langPackage}.Float.NEGATIVE_INFINITY" - else -> toStringConstantImpl(this) - } - - private fun Double.toStringConstant() = when { - isNaN() -> "${langPackage}.Double.NaN" - this == Double.POSITIVE_INFINITY -> "${langPackage}.Double.POSITIVE_INFINITY" - this == Double.NEGATIVE_INFINITY -> "${langPackage}.Double.NEGATIVE_INFINITY" - else -> "$this" - } - - private fun Boolean.toStringConstant() = - if (this) "true" else "false" - - private fun String.toStringConstant(): String = "\"" + escapeCharacters() + "\"" - - protected abstract fun String.escapeCharacters(): String - - private fun ClassId.isAccessibleBySimpleName(): Boolean = isAccessibleBySimpleNameImpl(this) - - companion object { - fun makeRenderer( - context: CgContext, - printer: CgPrinter = CgPrinterImpl() - ): CgAbstractRenderer { - val rendererContext = CgRendererContext.fromCgContext(context) - return makeRenderer(rendererContext, printer) - } - - fun makeRenderer( - utilClassKind: UtilClassKind, - codegenLanguage: CodegenLanguage, - printer: CgPrinter = CgPrinterImpl() - ): CgAbstractRenderer { - val rendererContext = CgRendererContext.fromUtilClassKind(utilClassKind, codegenLanguage) - return makeRenderer(rendererContext, printer) - } - - private fun makeRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer { - return when (context.codegenLanguage) { - CodegenLanguage.JAVA -> CgJavaRenderer(context, printer) - CodegenLanguage.KOTLIN -> CgKotlinRenderer(context, printer) - } - } - - /** - * @see [LONG_CODE_FRAGMENTS] - */ - private const val LARGE_CODE_BLOCK_SIZE: Int = 1000 - } -} 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 deleted file mode 100644 index 88bbba9ffa..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt +++ /dev/null @@ -1,387 +0,0 @@ -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.tree.AbstractCgClass -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.CgArrayInitializer -import org.utbot.framework.codegen.model.tree.CgBreakStatement -import org.utbot.framework.codegen.model.tree.CgConstructorCall -import org.utbot.framework.codegen.model.tree.CgDeclaration -import org.utbot.framework.codegen.model.tree.CgEqualTo -import org.utbot.framework.codegen.model.tree.CgErrorTestMethod -import org.utbot.framework.codegen.model.tree.CgErrorWrapper -import org.utbot.framework.codegen.model.tree.CgExecutableCall -import org.utbot.framework.codegen.model.tree.CgExpression -import org.utbot.framework.codegen.model.tree.CgForLoop -import org.utbot.framework.codegen.model.tree.CgGetJavaClass -import org.utbot.framework.codegen.model.tree.CgGetKotlinClass -import org.utbot.framework.codegen.model.tree.CgGetLength -import org.utbot.framework.codegen.model.tree.CgInnerBlock -import org.utbot.framework.codegen.model.tree.CgMethod -import org.utbot.framework.codegen.model.tree.CgNotNullAssertion -import org.utbot.framework.codegen.model.tree.CgParameterDeclaration -import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod -import org.utbot.framework.codegen.model.tree.CgRegularClass -import org.utbot.framework.codegen.model.tree.CgReturnStatement -import org.utbot.framework.codegen.model.tree.CgStatement -import org.utbot.framework.codegen.model.tree.CgStatementExecutableCall -import org.utbot.framework.codegen.model.tree.CgSwitchCase -import org.utbot.framework.codegen.model.tree.CgSwitchCaseLabel -import org.utbot.framework.codegen.model.tree.CgTestClass -import org.utbot.framework.codegen.model.tree.CgTestClassBody -import org.utbot.framework.codegen.model.tree.CgTestMethod -import org.utbot.framework.codegen.model.tree.CgTypeCast -import org.utbot.framework.codegen.model.tree.CgVariable -import org.utbot.framework.codegen.model.util.CgPrinter -import org.utbot.framework.codegen.model.util.CgPrinterImpl -import org.utbot.framework.codegen.model.util.nullLiteral -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 - -internal class CgJavaRenderer(context: CgRendererContext, printer: CgPrinter = CgPrinterImpl()) : - CgAbstractRenderer(context, printer) { - - override val statementEnding: String = ";" - - override val logicalAnd: String - get() = "&&" - - override val logicalOr: String - get() = "||" - - override val language: CodegenLanguage = CodegenLanguage.JAVA - - override val langPackage: String = "java.lang" - - override fun visit(element: AbstractCgClass<*>) { - for (annotation in element.annotations) { - annotation.accept(this) - } - - renderClassVisibility(element.id) - renderClassModality(element) - if (element.isStatic) { - print("static ") - } - print("class ") - print(element.simpleName) - - val superclass = element.superclass - if (superclass != null) { - print(" extends ${superclass.asString()}") - } - if (element.interfaces.isNotEmpty()) { - print(" implements ") - element.interfaces.map { it.asString() }.printSeparated() - } - println(" {") - withIndent { element.body.accept(this) } - println("}") - } - - override fun visit(element: CgTestClassBody) { - // render regions for test methods and utils - val allRegions = element.testMethodRegions + element.nestedClassRegions + element.staticDeclarationRegions - for ((i, region) in allRegions.withIndex()) { - if (i != 0) println() - - region.accept(this) - } - } - - override fun visit(element: CgArrayAnnotationArgument) { - print("{") - element.values.renderSeparated() - print("}") - } - - override fun visit(element: CgAnonymousFunction) { - print("(") - element.parameters.renderSeparated() - print(") -> ") - // TODO introduce CgBlock - - val expression = element.body.singleExpressionOrNull() - // expression lambda can be rendered without curly braces - if (expression != null) { - expression.accept(this) - - return - } - - visit(element.body) - } - - override fun visit(element: CgEqualTo) { - element.left.accept(this) - print(" == ") - element.right.accept(this) - } - - override fun visit(element: CgTypeCast) { - 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 (exprTypeIsSimilar && expr != nullLiteral()) { - element.expression.accept(this) - return - } - - print("(") - print("(") - print(wrappedTargetType.asString()) - print(") ") - element.expression.accept(this) - print(")") - } - - override fun visit(element: CgErrorWrapper) { - element.expression.accept(this) - } - - // Not-null assertion - - override fun visit(element: CgNotNullAssertion) { - element.expression.accept(this) - } - - override fun visit(element: CgParameterDeclaration) { - if (element.isVararg) { - print(element.type.elementClassId!!.asString()) - print("...") - } else { - print(element.type.asString()) - } - print(" ") - print(element.name.escapeNamePossibleKeyword()) - } - - override fun visit(element: CgGetJavaClass) { - // TODO: check how it works on ref types, primitives, ref arrays, primitive arrays, etc. - print(element.classId.asString()) - print(".class") - } - - override fun visit(element: CgGetKotlinClass) { - // For now we assume that we never need KClass in the generated Java test classes. - // If it changes, this error may be removed. - error("KClass attempted to be used in the Java test class") - } - - override fun visit(element: CgAllocateArray) { - // 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) { - // TODO: same as in visit(CgAllocateArray): we should rewrite the typeName and otherDimensions variables declaration - // to avoid using substringBefore() and substringAfter() directly - val typeName = element.type.canonicalName.substringBefore("[") - val otherDimensions = element.type.canonicalName.substringAfter("]") - // we can't specify the size of the first dimension when using initializer, - // as opposed to CgAllocateArray where there is no initializer - print("new $typeName[]$otherDimensions") - element.initializer.accept(this) - } - - override fun visit(element: CgArrayInitializer) { - val elementsInLine = arrayElementsInLine(element.elementType) - - print("{") - element.values.renderElements(elementsInLine) - print("}") - } - - override fun visit(element: CgGetLength) { - element.variable.accept(this) - print(".length") - } - - override fun visit(element: CgConstructorCall) { - print("new ") - print(element.executableId.classId.asString()) - renderExecutableCallArguments(element) - } - - override fun renderRegularImport(regularImport: RegularImport) { - val escapedImport = getEscapedImportRendering(regularImport) - println("import $escapedImport$statementEnding") - } - - override fun renderStaticImport(staticImport: StaticImport) { - val escapedImport = getEscapedImportRendering(staticImport) - println("import static $escapedImport$statementEnding") - } - - override fun renderMethodSignature(element: CgTestMethod) { - // test methods always have void return type - print("public void ") - print(element.name) - - print("(") - val newLinesNeeded = element.parameters.size > maxParametersAmountInOneLine - element.parameters.renderSeparated(newLinesNeeded) - print(")") - - renderExceptions(element) - } - - override fun renderMethodSignature(element: CgErrorTestMethod) { - // error test methods always have void return type - println("public void ${element.name}()") - } - - override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { - //we do not have a good string representation for two-dimensional array, so this strange if-else is required - val returnType = - if (element.returnType.simpleName == "Object[][]") "java.lang.Object[][]" else "${element.returnType}" - print("public static $returnType ${element.name}()") - renderExceptions(element) - } - - override fun visit(element: CgInnerBlock) { - println("{") - withIndent { - for (statement in element.statements) { - statement.accept(this) - } - } - println("}") - } - - override fun renderForLoopVarControl(element: CgForLoop) { - print("for (") - // TODO: rewrite in the future - with(element.initialization) { - print(variableType.asString()) - print(" ") - visit(variable) - initializer?.let { - print(" = ") - it.accept(this@CgJavaRenderer) - } - print("$statementEnding ") - } - element.condition.accept(this) - print("$statementEnding ") - element.update.accept(this) - } - - override fun renderDeclarationLeftPart(element: CgDeclaration) { - print(element.variableType.asString()) - print(" ") - visit(element.variable) - } - - override fun renderAccess(caller: CgExpression) { - print(".") - } - - override fun visit(element: CgSwitchCaseLabel) { - if (element.label != null) { - print("case ") - element.label.accept(this) - } else { - print("default") - } - println(":") - withIndent { - for (statement in element.statements) { - statement.accept(this) - } - // break statement in the end - CgBreakStatement.accept(this) - } - } - - override fun visit(element: CgSwitchCase) { - print("switch (") - element.value.accept(this) - println(") {") - withIndent { - for (caseLabel in element.labels) { - caseLabel.accept(this) - } - element.defaultLabel?.accept(this) - } - println("}") - } - - override fun toStringConstantImpl(byte: Byte): String = "(byte) $byte" - - override fun toStringConstantImpl(short: Short): String = "(short) $short" - - override fun toStringConstantImpl(long: Long): String = "${long}L" - - override fun toStringConstantImpl(float: Float): String = "${float}f" - - override fun toStringConstantImpl(int: Int): String = when (int) { - Int.MAX_VALUE -> "Integer.MAX_VALUE" - Int.MIN_VALUE -> "Integer.MIN_VALUE" - else -> "$int" - } - - override fun String.escapeCharacters(): String = StringEscapeUtils.escapeJava(this) - - override fun renderExecutableCallArguments(executableCall: CgExecutableCall) { - print("(") - executableCall.arguments.renderSeparated() - print(")") - } - - override fun renderTypeParameters(typeParameters: TypeParameters) {} - - override fun renderExceptionCatchVariable(exception: CgVariable) { - print("${exception.type} ${exception.name.escapeNamePossibleKeyword()}") - } - - override fun isAccessibleBySimpleNameImpl(classId: ClassId): Boolean = - super.isAccessibleBySimpleNameImpl(classId) || classId.packageName == "java.lang" - - override fun escapeNamePossibleKeywordImpl(s: String): String = s - - override fun renderClassVisibility(classId: ClassId) { - when { - classId.isPublic -> print("public ") - classId.isProtected -> print("protected ") - classId.isPrivate -> print("private ") - } - } - - override fun renderClassModality(aClass: AbstractCgClass<*>) { - when (aClass) { - is CgTestClass -> Unit - is CgRegularClass -> if (aClass.id.isFinal) print("final ") - } - } - - private fun renderExceptions(method: CgMethod) { - method.exceptions.takeIf { it.isNotEmpty() }?.let { exceptions -> - print(" throws ") - print(exceptions.joinToString(separator = ", ", postfix = " ") { it.asString() }) - } - } - - /** - * Returns containing [CgExpression] if [this] represents return statement or one executable call, and null otherwise. - */ - private fun List.singleExpressionOrNull(): CgExpression? = - singleOrNull().let { - when (it) { - is CgReturnStatement -> it.expression - is CgStatementExecutableCall -> it.call - else -> null - } - } -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt deleted file mode 100644 index 92963984cb..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.utbot.framework.codegen.model.visitor - -import org.utbot.framework.codegen.model.UtilClassKind -import org.utbot.framework.codegen.model.UtilClassKind.Companion.UT_UTILS_PACKAGE_NAME -import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider -import org.utbot.framework.codegen.model.constructor.builtin.utUtilsClassId -import org.utbot.framework.codegen.model.constructor.context.CgContext -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.MethodId -import org.utbot.framework.plugin.api.MockFramework - -/** - * Information from [CgContext] that is relevant for the renderer. - * Not all the information from [CgContext] is required to render a class, - * so this more lightweight context is created for this purpose. - */ -internal class CgRendererContext( - val shouldOptimizeImports: Boolean, - val importedClasses: Set, - val importedStaticMethods: Set, - val classPackageName: String, - val generatedClass: ClassId, - val utilMethodProvider: UtilMethodProvider, - val codegenLanguage: CodegenLanguage, - val mockFrameworkUsed: Boolean, - val mockFramework: MockFramework, -) { - companion object { - fun fromCgContext(context: CgContext): CgRendererContext { - return CgRendererContext( - shouldOptimizeImports = context.shouldOptimizeImports, - importedClasses = context.importedClasses, - importedStaticMethods = context.importedStaticMethods, - classPackageName = context.testClassPackageName, - generatedClass = context.outerMostTestClass, - utilMethodProvider = context.utilMethodProvider, - codegenLanguage = context.codegenLanguage, - mockFrameworkUsed = context.mockFrameworkUsed, - mockFramework = context.mockFramework - ) - } - - fun fromUtilClassKind(utilClassKind: UtilClassKind, language: CodegenLanguage): CgRendererContext { - return CgRendererContext( - shouldOptimizeImports = false, - importedClasses = emptySet(), - importedStaticMethods = emptySet(), - classPackageName = UT_UTILS_PACKAGE_NAME, - generatedClass = utUtilsClassId, - utilMethodProvider = utilClassKind.utilMethodProvider, - codegenLanguage = language, - mockFrameworkUsed = utilClassKind.mockFrameworkUsed, - mockFramework = utilClassKind.mockFramework - ) - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 1b89761575..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt +++ /dev/null @@ -1,269 +0,0 @@ -package org.utbot.framework.codegen.model.visitor - -import org.utbot.framework.codegen.model.tree.AbstractCgClass -import org.utbot.framework.codegen.model.tree.AbstractCgClassBody -import org.utbot.framework.codegen.model.tree.AbstractCgClassFile -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 -import org.utbot.framework.codegen.model.tree.CgArrayInitializer -import org.utbot.framework.codegen.model.tree.CgAssignment -import org.utbot.framework.codegen.model.tree.CgAuxiliaryClass -import org.utbot.framework.codegen.model.tree.CgBreakStatement -import org.utbot.framework.codegen.model.tree.CgComment -import org.utbot.framework.codegen.model.tree.CgCommentedAnnotation -import org.utbot.framework.codegen.model.tree.CgComparison -import org.utbot.framework.codegen.model.tree.CgConstructorCall -import org.utbot.framework.codegen.model.tree.CgContinueStatement -import org.utbot.framework.codegen.model.tree.CgDeclaration -import org.utbot.framework.codegen.model.tree.CgDecrement -import org.utbot.framework.codegen.model.tree.CgDoWhileLoop -import org.utbot.framework.codegen.model.tree.CgDocClassLinkStmt -import org.utbot.framework.codegen.model.tree.CgDocCodeStmt -import org.utbot.framework.codegen.model.tree.CgDocMethodLinkStmt -import org.utbot.framework.codegen.model.tree.CgDocPreTagStatement -import org.utbot.framework.codegen.model.tree.CgCustomTagStatement -import org.utbot.framework.codegen.model.tree.CgDocRegularStmt -import org.utbot.framework.codegen.model.tree.CgDocumentationComment -import org.utbot.framework.codegen.model.tree.CgElement -import org.utbot.framework.codegen.model.tree.CgEmptyLine -import org.utbot.framework.codegen.model.tree.CgEnumConstantAccess -import org.utbot.framework.codegen.model.tree.CgEqualTo -import org.utbot.framework.codegen.model.tree.CgErrorTestMethod -import org.utbot.framework.codegen.model.tree.CgErrorWrapper -import org.utbot.framework.codegen.model.tree.CgExecutableCall -import org.utbot.framework.codegen.model.tree.CgExecutableUnderTestCluster -import org.utbot.framework.codegen.model.tree.CgExpression -import org.utbot.framework.codegen.model.tree.CgFieldAccess -import org.utbot.framework.codegen.model.tree.CgForLoop -import org.utbot.framework.codegen.model.tree.CgGetJavaClass -import org.utbot.framework.codegen.model.tree.CgGetKotlinClass -import org.utbot.framework.codegen.model.tree.CgGetLength -import org.utbot.framework.codegen.model.tree.CgGreaterThan -import org.utbot.framework.codegen.model.tree.CgIfStatement -import org.utbot.framework.codegen.model.tree.CgIncrement -import org.utbot.framework.codegen.model.tree.CgInnerBlock -import org.utbot.framework.codegen.model.tree.CgIsInstance -import org.utbot.framework.codegen.model.tree.CgLessThan -import org.utbot.framework.codegen.model.tree.CgLiteral -import org.utbot.framework.codegen.model.tree.CgLogicalAnd -import org.utbot.framework.codegen.model.tree.CgLogicalOr -import org.utbot.framework.codegen.model.tree.CgLoop -import org.utbot.framework.codegen.model.tree.CgMethod -import org.utbot.framework.codegen.model.tree.CgMethodCall -import org.utbot.framework.codegen.model.tree.CgMultilineComment -import org.utbot.framework.codegen.model.tree.CgMultipleArgsAnnotation -import org.utbot.framework.codegen.model.tree.CgNamedAnnotationArgument -import org.utbot.framework.codegen.model.tree.CgNonStaticRunnable -import org.utbot.framework.codegen.model.tree.CgNotNullAssertion -import org.utbot.framework.codegen.model.tree.CgParameterDeclaration -import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod -import org.utbot.framework.codegen.model.tree.CgRegularClass -import org.utbot.framework.codegen.model.tree.CgRegularClassBody -import org.utbot.framework.codegen.model.tree.CgRegularClassFile -import org.utbot.framework.codegen.model.tree.CgReturnStatement -import org.utbot.framework.codegen.model.tree.CgSimpleRegion -import org.utbot.framework.codegen.model.tree.CgSingleArgAnnotation -import org.utbot.framework.codegen.model.tree.CgSingleLineComment -import org.utbot.framework.codegen.model.tree.CgSpread -import org.utbot.framework.codegen.model.tree.CgStatement -import org.utbot.framework.codegen.model.tree.CgStatementExecutableCall -import org.utbot.framework.codegen.model.tree.CgStaticFieldAccess -import org.utbot.framework.codegen.model.tree.CgStaticRunnable -import org.utbot.framework.codegen.model.tree.CgStaticsRegion -import org.utbot.framework.codegen.model.tree.CgSwitchCase -import org.utbot.framework.codegen.model.tree.CgSwitchCaseLabel -import org.utbot.framework.codegen.model.tree.CgTestClass -import org.utbot.framework.codegen.model.tree.CgTestClassBody -import org.utbot.framework.codegen.model.tree.CgTestClassFile -import org.utbot.framework.codegen.model.tree.CgTestMethod -import org.utbot.framework.codegen.model.tree.CgTestMethodCluster -import org.utbot.framework.codegen.model.tree.CgThisInstance -import org.utbot.framework.codegen.model.tree.CgThrowStatement -import org.utbot.framework.codegen.model.tree.CgTripleSlashMultilineComment -import org.utbot.framework.codegen.model.tree.CgTryCatch -import org.utbot.framework.codegen.model.tree.CgTypeCast -import org.utbot.framework.codegen.model.tree.CgUtilMethod -import org.utbot.framework.codegen.model.tree.CgVariable -import org.utbot.framework.codegen.model.tree.CgWhileLoop - -interface CgVisitor { - fun visit(element: CgElement): R - - fun visit(element: AbstractCgClassFile<*>): R - fun visit(element: CgRegularClassFile): R - fun visit(element: CgTestClassFile): R - - fun visit(element: AbstractCgClass<*>): R - fun visit(element: CgRegularClass): R - fun visit(element: CgTestClass): R - - fun visit(element: AbstractCgClassBody): R - fun visit(element: CgRegularClassBody): R - fun visit(element: CgTestClassBody): R - - fun visit(element: CgStaticsRegion): R - fun visit(element: CgSimpleRegion<*>): R - fun visit(element: CgTestMethodCluster): R - fun visit(element: CgExecutableUnderTestCluster): R - - fun visit(element: CgAuxiliaryClass): R - fun visit(element: CgUtilMethod): R - - // Methods - fun visit(element: CgMethod): R - fun visit(element: CgTestMethod): R - fun visit(element: CgErrorTestMethod): R - fun visit(element: CgParameterizedTestDataProviderMethod): R - - // Annotations - fun visit(element: CgCommentedAnnotation): R - fun visit(element: CgSingleArgAnnotation): R - fun visit(element: CgMultipleArgsAnnotation): R - fun visit(element: CgNamedAnnotationArgument): R - fun visit(element: CgArrayAnnotationArgument): R - - // Comments - fun visit(element: CgComment): R - fun visit(element: CgSingleLineComment): R - fun visit(element: CgAbstractMultilineComment): R - fun visit(element: CgTripleSlashMultilineComment): R - fun visit(element: CgMultilineComment): R - fun visit(element: CgDocumentationComment): R - - // Comment statements - fun visit(element: CgDocPreTagStatement): R - fun visit(element: CgCustomTagStatement): R - fun visit(element: CgDocCodeStmt): R - fun visit(element: CgDocRegularStmt): R - fun visit(element: CgDocClassLinkStmt): R - fun visit(element: CgDocMethodLinkStmt): R - - // Any block - // IMPORTANT: there is no line separator in the end by default - // because some blocks have it (like loops) but some do not (like try .. catch, if .. else etc) - fun visit(block: List, printNextLine: Boolean = false): R - - // Anonymous function (lambda) - fun visit(element: CgAnonymousFunction): R - - // Return statement - fun visit(element: CgReturnStatement): R - - // Array element access - fun visit(element: CgArrayElementAccess): R - - // Loop conditions - fun visit(element: CgComparison): R - fun visit(element: CgLessThan): R - fun visit(element: CgGreaterThan): R - fun visit(element: CgEqualTo): R - - // Increment and decrement - fun visit(element: CgIncrement): R - fun visit(element: CgDecrement): R - - fun visit(element: CgErrorWrapper): R - - // Try-catch - fun visit(element: CgTryCatch): R - - //Simple block - fun visit(element: CgInnerBlock): R - - // Loops - fun visit(element: CgLoop): R - fun visit(element: CgForLoop): R - fun visit(element: CgWhileLoop): R - fun visit(element: CgDoWhileLoop): R - - // Control statements - fun visit(element: CgBreakStatement): R - fun visit(element: CgContinueStatement): R - - // Variable declaration - fun visit(element: CgDeclaration): R - - // Variable assignment - fun visit(element: CgAssignment): R - - // Expressions - fun visit(element: CgExpression): R - - // Type cast - fun visit(element: CgTypeCast): R - - // isInstance check - fun visit(element: CgIsInstance): R - - // This instance - fun visit(element: CgThisInstance): R - - // Variables - fun visit(element: CgVariable): R - - // Not-null assertion - - fun visit(element: CgNotNullAssertion): R - - // Method parameters - fun visit(element: CgParameterDeclaration): R - - // Primitive and String literals - fun visit(element: CgLiteral): R - - // Non-static runnable like this::toString or (new Object())::toString etc - fun visit(element: CgNonStaticRunnable): R - // Static runnable like Random::nextRandomInt etc - fun visit(element: CgStaticRunnable): R - - // Array allocation - fun visit(element: CgAllocateArray): R - fun visit(element: CgAllocateInitializedArray): R - fun visit(element: CgArrayInitializer): R - - // Spread operator - fun visit(element: CgSpread): R - - // Enum constant - fun visit(element: CgEnumConstantAccess): R - - // Property access - fun visit(element: CgAbstractFieldAccess): R - fun visit(element: CgFieldAccess): R - fun visit(element: CgStaticFieldAccess): R - - // Conditional statement - - fun visit(element: CgIfStatement): R - fun visit(element: CgSwitchCaseLabel): R - fun visit(element: CgSwitchCase): R - - // Binary logical operators - - fun visit(element: CgLogicalAnd): R - fun visit(element: CgLogicalOr): R - - // Acquisition of array length, e.g. args.length - fun visit(element: CgGetLength): R - - // Acquisition of java or kotlin class, e.g. MyClass.class in Java, MyClass::class.java in Kotlin or MyClass::class for Kotlin classes - fun visit(element: CgGetJavaClass): R - fun visit(element: CgGetKotlinClass): R - - // Executable calls - fun visit(element: CgStatementExecutableCall): R - fun visit(element: CgExecutableCall): R - fun visit(element: CgConstructorCall): R - fun visit(element: CgMethodCall): R - - // Throw statement - fun visit(element: CgThrowStatement): R - - // Empty line - fun visit(element: CgEmptyLine): 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 deleted file mode 100644 index fb51419a41..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt +++ /dev/null @@ -1,1432 +0,0 @@ -package org.utbot.framework.codegen.model.visitor - -import org.utbot.framework.codegen.StaticImport -import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider -import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner -import org.utbot.framework.codegen.model.constructor.util.importIfNeeded -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.MethodId -import org.utbot.framework.plugin.api.MockFramework -import org.utbot.framework.plugin.api.util.fieldClassId -import org.utbot.framework.plugin.api.util.id -import java.lang.invoke.CallSite -import java.lang.invoke.LambdaMetafactory -import java.lang.invoke.MethodHandle -import java.lang.invoke.MethodHandles -import java.lang.invoke.MethodType -import java.lang.reflect.Field -import java.lang.reflect.Method -import java.lang.reflect.Modifier -import java.util.Arrays -import java.util.Objects -import java.util.stream.Collectors - -private enum class Visibility(val text: String) { - PRIVATE("private"), - @Suppress("unused") - PROTECTED("protected"), - PUBLIC("public"); - - infix fun by(language: CodegenLanguage): String { - if (this == PUBLIC && language == CodegenLanguage.KOTLIN) { - // public is default in Kotlin - return "" - } - return "$text " - } -} - -// TODO: This method may throw an exception that will crash rendering. -// TODO: Add isolation on rendering: https://github.com/UnitTestBot/UTBotJava/issues/853 -internal fun UtilMethodProvider.utilMethodTextById( - id: MethodId, - mockFrameworkUsed: Boolean, - mockFramework: MockFramework, - codegenLanguage: CodegenLanguage -): String { - // If util methods are declared in the test class, then they are private. Otherwise, they are public. - val visibility = if (this is TestClassUtilMethodProvider) Visibility.PRIVATE else Visibility.PUBLIC - return with(this) { - when (id) { - getUnsafeInstanceMethodId -> getUnsafeInstance(visibility, codegenLanguage) - createInstanceMethodId -> createInstance(visibility, codegenLanguage) - createArrayMethodId -> createArray(visibility, codegenLanguage) - setFieldMethodId -> setField(visibility, codegenLanguage) - setStaticFieldMethodId -> setStaticField(visibility, codegenLanguage) - getFieldValueMethodId -> getFieldValue(visibility, codegenLanguage) - getStaticFieldValueMethodId -> getStaticFieldValue(visibility, codegenLanguage) - getEnumConstantByNameMethodId -> getEnumConstantByName(visibility, codegenLanguage) - deepEqualsMethodId -> deepEquals(visibility, codegenLanguage, mockFrameworkUsed, mockFramework) - arraysDeepEqualsMethodId -> arraysDeepEquals(visibility, codegenLanguage) - iterablesDeepEqualsMethodId -> iterablesDeepEquals(visibility, codegenLanguage) - streamsDeepEqualsMethodId -> streamsDeepEquals(visibility, codegenLanguage) - mapsDeepEqualsMethodId -> mapsDeepEquals(visibility, codegenLanguage) - hasCustomEqualsMethodId -> hasCustomEquals(visibility, codegenLanguage) - getArrayLengthMethodId -> getArrayLength(visibility, codegenLanguage) - buildStaticLambdaMethodId -> buildStaticLambda(visibility, codegenLanguage) - buildLambdaMethodId -> buildLambda(visibility, codegenLanguage) - // the following methods are used only by other util methods, so they can always be private - getLookupInMethodId -> getLookupIn(codegenLanguage) - getLambdaCapturedArgumentTypesMethodId -> getLambdaCapturedArgumentTypes(codegenLanguage) - getLambdaCapturedArgumentValuesMethodId -> getLambdaCapturedArgumentValues(codegenLanguage) - getInstantiatedMethodTypeMethodId -> getInstantiatedMethodType(codegenLanguage) - getLambdaMethodMethodId -> getLambdaMethod(codegenLanguage) - getSingleAbstractMethodMethodId -> getSingleAbstractMethod(codegenLanguage) - else -> error("Unknown util method for class $this: $id") - } - } -} - -// TODO: This method may throw an exception that will crash rendering. -// TODO: Add isolation on rendering: https://github.com/UnitTestBot/UTBotJava/issues/853 -internal fun UtilMethodProvider.auxiliaryClassTextById(id: ClassId, codegenLanguage: CodegenLanguage): String = - with(this) { - when (id) { - capturedArgumentClassId -> capturedArgumentClass(codegenLanguage) - else -> error("Unknown auxiliary class: $id") - } - } - -private fun getEnumConstantByName(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static Object getEnumConstantByName(Class enumClass, String name) throws IllegalAccessException { - java.lang.reflect.Field[] fields = enumClass.getDeclaredFields(); - for (java.lang.reflect.Field field : fields) { - String fieldName = field.getName(); - if (field.isEnumConstant() && fieldName.equals(name)) { - field.setAccessible(true); - - return field.get(null); - } - } - - return null; - } - """ - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}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) { - field.isAccessible = true - - return field.get(null) - } - } - - return null - } - """ - } - }.trimIndent() - -private fun getStaticFieldValue(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static Object getStaticFieldValue(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { - java.lang.reflect.Field field; - Class originClass = clazz; - do { - try { - field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); - - return field.get(null); - } catch (NoSuchFieldException e) { - clazz = clazz.getSuperclass(); - } - } while (clazz != null); - - throw new NoSuchFieldException("Field '" + fieldName + "' not found on class " + originClass); - } - """ - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}fun getStaticFieldValue(clazz: Class<*>, fieldName: String): kotlin.Any? { - var currentClass: Class<*>? = clazz - var field: java.lang.reflect.Field - do { - try { - field = currentClass!!.getDeclaredField(fieldName) - field.isAccessible = true - val modifiersField: java.lang.reflect.Field = java.lang.reflect.Field::class.java.getDeclaredField("modifiers") - modifiersField.isAccessible = true - modifiersField.setInt(field, field.modifiers and java.lang.reflect.Modifier.FINAL.inv()) - - return field.get(null) - } catch (e: NoSuchFieldException) { - currentClass = currentClass!!.superclass - } - } while (currentClass != null) - - throw NoSuchFieldException("Field '" + fieldName + "' not found on class " + clazz) - } - """ - } - }.trimIndent() - -private fun getFieldValue(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static Object getFieldValue(Object obj, String fieldClassName, String fieldName) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException { - Class clazz = Class.forName(fieldClassName); - java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); - - field.setAccessible(true); - java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); - - return field.get(obj); - } - """ - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}fun getFieldValue(any: kotlin.Any, fieldClassName: String, fieldName: String): kotlin.Any? { - val clazz: Class<*> = Class.forName(fieldClassName) - val field: java.lang.reflect.Field = clazz.getDeclaredField(fieldName) - - field.isAccessible = true - val modifiersField: java.lang.reflect.Field = java.lang.reflect.Field::class.java.getDeclaredField("modifiers") - modifiersField.isAccessible = true - modifiersField.setInt(field, field.modifiers and java.lang.reflect.Modifier.FINAL.inv()) - - return field.get(any) - } - """ - } - }.trimIndent() - -private fun setStaticField(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static void setStaticField(Class clazz, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { - java.lang.reflect.Field field; - - do { - try { - field = clazz.getDeclaredField(fieldName); - } catch (Exception e) { - clazz = clazz.getSuperclass(); - field = null; - } - } while (field == null); - - java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); - - field.setAccessible(true); - field.set(null, fieldValue); - } - """ - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}fun setStaticField(defaultClass: Class<*>, fieldName: String, fieldValue: kotlin.Any?) { - var field: java.lang.reflect.Field? - var clazz = defaultClass - - do { - try { - field = clazz.getDeclaredField(fieldName) - } catch (e: Exception) { - clazz = clazz.superclass - field = null - } - } while (field == null) - - val modifiersField: java.lang.reflect.Field = java.lang.reflect.Field::class.java.getDeclaredField("modifiers") - modifiersField.isAccessible = true - modifiersField.setInt(field, field.modifiers and java.lang.reflect.Modifier.FINAL.inv()) - - field.isAccessible = true - field.set(null, fieldValue) - } - """ - } - }.trimIndent() - -private fun setField(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static void setField(Object object, String fieldClassName, String fieldName, Object fieldValue) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { - Class clazz = Class.forName(fieldClassName); - java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); - - java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); - - field.setAccessible(true); - field.set(object, fieldValue); - } - """ - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}fun setField(any: kotlin.Any, fieldClassName: String, fieldName: String, fieldValue: kotlin.Any?) { - val clazz: Class<*> = Class.forName(fieldClassName) - val field: java.lang.reflect.Field = clazz.getDeclaredField(fieldName) - - val modifiersField: java.lang.reflect.Field = java.lang.reflect.Field::class.java.getDeclaredField("modifiers") - modifiersField.isAccessible = true - modifiersField.setInt(field, field.modifiers and java.lang.reflect.Modifier.FINAL.inv()) - - field.isAccessible = true - field.set(any, fieldValue) - } - """ - } - }.trimIndent() - -private fun createArray(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static Object[] createArray(String className, int length, Object... values) throws ClassNotFoundException { - Object array = java.lang.reflect.Array.newInstance(Class.forName(className), length); - - for (int i = 0; i < values.length; i++) { - java.lang.reflect.Array.set(array, i, values[i]); - } - - return (Object[]) array; - } - """ - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}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 kotlin.Array - } - """ - } - }.trimIndent() - -private fun createInstance(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static Object createInstance(String className) throws Exception { - Class clazz = Class.forName(className); - return Class.forName("sun.misc.Unsafe").getDeclaredMethod("allocateInstance", Class.class) - .invoke(getUnsafeInstance(), clazz); - } - """ - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}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) - } - """ - } - }.trimIndent() - -private fun getUnsafeInstance(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}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); - } - """ - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}fun getUnsafeInstance(): kotlin.Any? { - val f: java.lang.reflect.Field = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe") - f.isAccessible = true - return f[null] - } - """ - } - }.trimIndent() - -/** - * Mockito mock uses its own equals which we cannot rely on - */ -private fun isMockCondition(mockFrameworkUsed: Boolean, mockFramework: MockFramework): String { - if (!mockFrameworkUsed) return "" - - return when (mockFramework) { - MockFramework.MOCKITO -> " && !org.mockito.Mockito.mockingDetails(o1).isMock()" - // in case we will add any other mock frameworks, newer Kotlin compiler versions - // will report a non-exhaustive 'when', so we will not forget to support them here as well - } -} - -private fun deepEquals( - visibility: Visibility, - language: CodegenLanguage, - mockFrameworkUsed: Boolean, - mockFramework: MockFramework -): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - static class FieldsPair { - final Object o1; - final Object o2; - - public FieldsPair(Object o1, Object o2) { - this.o1 = o1; - this.o2 = o2; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - FieldsPair that = (FieldsPair) o; - return java.util.Objects.equals(o1, that.o1) && java.util.Objects.equals(o2, that.o2); - } - - @Override - public int hashCode() { - return java.util.Objects.hash(o1, o2); - } - } - - ${visibility by language}static boolean deepEquals(Object o1, Object o2) { - return deepEquals(o1, o2, new java.util.HashSet<>()); - } - - private static boolean deepEquals(Object o1, Object o2, java.util.Set visited) { - visited.add(new FieldsPair(o1, o2)); - - if (o1 == o2) { - return true; - } - - if (o1 == null || o2 == null) { - return false; - } - - if (o1 instanceof Iterable) { - if (!(o2 instanceof Iterable)) { - return false; - } - - return iterablesDeepEquals((Iterable) o1, (Iterable) o2, visited); - } - - if (o2 instanceof Iterable) { - return false; - } - - if (o1 instanceof java.util.stream.BaseStream) { - if (!(o2 instanceof java.util.stream.BaseStream)) { - return false; - } - - return streamsDeepEquals((java.util.stream.BaseStream) o1, (java.util.stream.BaseStream) o2, visited); - } - - if (o2 instanceof java.util.stream.BaseStream) { - return false; - } - - if (o1 instanceof java.util.Map) { - if (!(o2 instanceof java.util.Map)) { - return false; - } - - return mapsDeepEquals((java.util.Map) o1, (java.util.Map) o2, visited); - } - - if (o2 instanceof java.util.Map) { - return false; - } - - Class firstClass = o1.getClass(); - if (firstClass.isArray()) { - if (!o2.getClass().isArray()) { - return false; - } - - // Primitive arrays should not appear here - return arraysDeepEquals(o1, o2, visited); - } - - // common classes - - // check if class has custom equals method (including wrappers and strings) - // It is very important to check it here but not earlier because iterables and maps also have custom equals - // based on elements equals - if (hasCustomEquals(firstClass)${isMockCondition(mockFrameworkUsed, mockFramework)}) { - return o1.equals(o2); - } - - // common classes without custom equals, use comparison by fields - final java.util.List fields = new java.util.ArrayList<>(); - while (firstClass != Object.class) { - fields.addAll(java.util.Arrays.asList(firstClass.getDeclaredFields())); - // Interface should not appear here - firstClass = firstClass.getSuperclass(); - } - - for (java.lang.reflect.Field field : fields) { - field.setAccessible(true); - try { - final Object field1 = field.get(o1); - final Object field2 = field.get(o2); - if (!visited.contains(new FieldsPair(field1, field2)) && !deepEquals(field1, field2, visited)) { - return false; - } - } catch (IllegalArgumentException e) { - return false; - } catch (IllegalAccessException e) { - // should never occur because field was set accessible - return false; - } - } - - return true; - } - """.trimIndent() - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}fun deepEquals(o1: kotlin.Any?, o2: kotlin.Any?): Boolean = deepEquals(o1, o2, hashSetOf()) - - 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 kotlin.collections.Iterable<*>) { - return if (o2 !is kotlin.collections.Iterable<*>) false else iterablesDeepEquals(o1, o2, visited) - } - - if (o2 is kotlin.collections.Iterable<*>) return false - - if (o1 is java.util.stream.BaseStream<*, *>) { - return if (o2 !is java.util.stream.BaseStream<*, *>) false else streamsDeepEquals(o1, o2, visited) - } - - if (o2 is java.util.stream.BaseStream<*, *>) return false - - if (o1 is kotlin.collections.Map<*, *>) { - return if (o2 !is kotlin.collections.Map<*, *>) false else mapsDeepEquals(o1, o2, visited) - } - - if (o2 is kotlin.collections.Map<*, *>) return false - - var firstClass: Class<*> = o1.javaClass - if (firstClass.isArray) { - return if (!o2.javaClass.isArray) { - false - } else { - arraysDeepEquals(o1, o2, visited) - } - } - - // check if class has custom equals method (including wrappers and strings) - // It is very important to check it here but not earlier because iterables and maps also have custom equals - // based on elements equals - if (hasCustomEquals(firstClass)${isMockCondition(mockFrameworkUsed, mockFramework)}) { - return o1 == o2 - } - - // common classes without custom equals, use comparison by fields - val fields: kotlin.collections.MutableList = mutableListOf() - while (firstClass != kotlin.Any::class.java) { - fields += listOf(*firstClass.declaredFields) - // Interface should not appear here - firstClass = firstClass.superclass - } - - for (field in fields) { - field.isAccessible = true - try { - val field1 = field[o1] - val field2 = field[o2] - if ((field1 to field2) !in visited && !deepEquals(field1, field2, visited)) return false - } catch (e: IllegalArgumentException) { - return false - } catch (e: IllegalAccessException) { - // should never occur - return false - } - } - - return true - } - """.trimIndent() - } - } - -private fun arraysDeepEquals(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static 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(java.lang.reflect.Array.get(arr1, i), java.lang.reflect.Array.get(arr2, i), visited)) { - return false; - } - } - - return true; - } - """.trimIndent() - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}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 - - for (i in 0 until size) { - if (!deepEquals(java.lang.reflect.Array.get(arr1, i), java.lang.reflect.Array.get(arr2, i), visited)) { - return false - } - } - - return true - } - """.trimIndent() - } - } - -private fun iterablesDeepEquals(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static 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; - } - } - - if (firstIterator.hasNext()) { - return false; - } - - return !secondIterator.hasNext(); - } - """.trimIndent() - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}fun iterablesDeepEquals( - i1: Iterable<*>, - i2: Iterable<*>, - visited: kotlin.collections.MutableSet> - ): Boolean { - val firstIterator = i1.iterator() - val secondIterator = i2.iterator() - while (firstIterator.hasNext() && secondIterator.hasNext()) { - if (!deepEquals(firstIterator.next(), secondIterator.next(), visited)) return false - } - - return if (firstIterator.hasNext()) false else !secondIterator.hasNext() - } - """.trimIndent() - } - } - -private fun streamsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static boolean streamsDeepEquals( - java.util.stream.BaseStream s1, - java.util.stream.BaseStream 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 -> { - """ - ${visibility by language}fun streamsDeepEquals( - s1: java.util.stream.BaseStream<*, *>, - s2: java.util.stream.BaseStream<*, *>, - 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() - } - } - -private fun mapsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static 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 java.util.Map.Entry firstEntry = firstIterator.next(); - final java.util.Map.Entry secondEntry = secondIterator.next(); - - if (!deepEquals(firstEntry.getKey(), secondEntry.getKey(), visited)) { - return false; - } - - if (!deepEquals(firstEntry.getValue(), secondEntry.getValue(), visited)) { - return false; - } - } - - if (firstIterator.hasNext()) { - return false; - } - - return !secondIterator.hasNext(); - } - """.trimIndent() - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}fun mapsDeepEquals( - m1: kotlin.collections.Map<*, *>, - m2: kotlin.collections.Map<*, *>, - visited: kotlin.collections.MutableSet> - ): Boolean { - val firstIterator = m1.entries.iterator() - val secondIterator = m2.entries.iterator() - while (firstIterator.hasNext() && secondIterator.hasNext()) { - val firstEntry = firstIterator.next() - val secondEntry = secondIterator.next() - - if (!deepEquals(firstEntry.key, secondEntry.key, visited)) return false - - if (!deepEquals(firstEntry.value, secondEntry.value, visited)) return false - } - - return if (firstIterator.hasNext()) false else !secondIterator.hasNext() - } - """.trimIndent() - } - } - -private fun hasCustomEquals(visibility: Visibility, language: CodegenLanguage): String = - when (language) { - CodegenLanguage.JAVA -> { - """ - ${visibility by language}static boolean hasCustomEquals(Class clazz) { - while (!Object.class.equals(clazz)) { - try { - clazz.getDeclaredMethod("equals", Object.class); - return true; - } catch (Exception e) { - // Interface should not appear here - clazz = clazz.getSuperclass(); - } - } - - return false; - } - """.trimIndent() - } - CodegenLanguage.KOTLIN -> { - """ - ${visibility by language}fun hasCustomEquals(clazz: Class<*>): Boolean { - var c = clazz - while (kotlin.Any::class.java != c) { - try { - c.getDeclaredMethod("equals", kotlin.Any::class.java) - return true - } catch (e: Exception) { - // Interface should not appear here - c = c.superclass - } - } - return false - } - """.trimIndent() - } - } - -private fun getArrayLength(visibility: Visibility, language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - ${visibility by language}static int getArrayLength(Object arr) { - return java.lang.reflect.Array.getLength(arr); - } - """.trimIndent() - CodegenLanguage.KOTLIN -> - """ - ${visibility by language}fun getArrayLength(arr: kotlin.Any?): Int = java.lang.reflect.Array.getLength(arr) - """.trimIndent() - } - -private fun buildStaticLambda(visibility: Visibility, language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - /** - * @param samType a class representing a functional interface. - * @param declaringClass a class where the lambda is declared. - * @param lambdaName a name of the synthetic method that represents a lambda. - * @param capturedArguments a vararg containing {@link CapturedArgument} instances representing - * values that the given lambda has captured. - * @return an {@link Object} that represents an instance of the given functional interface {@code samType} - * and implements its single abstract method with the behavior of the given lambda. - */ - ${visibility by language}static Object buildStaticLambda( - Class samType, - Class declaringClass, - String lambdaName, - CapturedArgument... capturedArguments - ) throws Throwable { - // Create lookup for class where the lambda is declared in. - java.lang.invoke.MethodHandles.Lookup caller = getLookupIn(declaringClass); - - // Obtain the single abstract method of a functional interface whose instance we are building. - // For example, for `java.util.function.Predicate` it will be method `test`. - java.lang.reflect.Method singleAbstractMethod = getSingleAbstractMethod(samType); - String invokedName = singleAbstractMethod.getName(); - // Method type of single abstract method of the target functional interface. - java.lang.invoke.MethodType samMethodType = java.lang.invoke.MethodType.methodType(singleAbstractMethod.getReturnType(), singleAbstractMethod.getParameterTypes()); - - java.lang.reflect.Method lambdaMethod = getLambdaMethod(declaringClass, lambdaName); - lambdaMethod.setAccessible(true); - java.lang.invoke.MethodType lambdaMethodType = java.lang.invoke.MethodType.methodType(lambdaMethod.getReturnType(), lambdaMethod.getParameterTypes()); - java.lang.invoke.MethodHandle lambdaMethodHandle = caller.findStatic(declaringClass, lambdaName, lambdaMethodType); - - Class[] capturedArgumentTypes = getLambdaCapturedArgumentTypes(capturedArguments); - java.lang.invoke.MethodType invokedType = java.lang.invoke.MethodType.methodType(samType, capturedArgumentTypes); - java.lang.invoke.MethodType instantiatedMethodType = getInstantiatedMethodType(lambdaMethod, capturedArgumentTypes); - - // Create a CallSite for the given lambda. - java.lang.invoke.CallSite site = java.lang.invoke.LambdaMetafactory.metafactory( - caller, - invokedName, - invokedType, - samMethodType, - lambdaMethodHandle, - instantiatedMethodType); - - Object[] capturedValues = getLambdaCapturedArgumentValues(capturedArguments); - - // Get MethodHandle and pass captured values to it to obtain an object - // that represents the target functional interface instance. - java.lang.invoke.MethodHandle handle = site.getTarget(); - return handle.invokeWithArguments(capturedValues); - } - """.trimIndent() - CodegenLanguage.KOTLIN -> - """ - /** - * @param samType a class representing a functional interface. - * @param declaringClass a class where the lambda is declared. - * @param lambdaName a name of the synthetic method that represents a lambda. - * @param capturedArguments a vararg containing [CapturedArgument] instances representing - * values that the given lambda has captured. - * @return an [Any] that represents an instance of the given functional interface `samType` - * and implements its single abstract method with the behavior of the given lambda. - */ - ${visibility by language}fun buildStaticLambda( - samType: Class<*>, - declaringClass: Class<*>, - lambdaName: String, - vararg capturedArguments: CapturedArgument - ): Any { - // Create lookup for class where the lambda is declared in. - val caller = getLookupIn(declaringClass) - - // Obtain the single abstract method of a functional interface whose instance we are building. - // For example, for `java.util.function.Predicate` it will be method `test`. - val singleAbstractMethod = getSingleAbstractMethod(samType) - val invokedName = singleAbstractMethod.name - // Method type of single abstract method of the target functional interface. - val samMethodType = java.lang.invoke.MethodType.methodType(singleAbstractMethod.returnType, singleAbstractMethod.parameterTypes) - - val lambdaMethod = getLambdaMethod(declaringClass, lambdaName) - lambdaMethod.isAccessible = true - val lambdaMethodType = java.lang.invoke.MethodType.methodType(lambdaMethod.returnType, lambdaMethod.parameterTypes) - val lambdaMethodHandle = caller.findStatic(declaringClass, lambdaName, lambdaMethodType) - - val capturedArgumentTypes = getLambdaCapturedArgumentTypes(*capturedArguments) - val invokedType = java.lang.invoke.MethodType.methodType(samType, capturedArgumentTypes) - val instantiatedMethodType = getInstantiatedMethodType(lambdaMethod, capturedArgumentTypes) - - // Create a CallSite for the given lambda. - val site = java.lang.invoke.LambdaMetafactory.metafactory( - caller, - invokedName, - invokedType, - samMethodType, - lambdaMethodHandle, - instantiatedMethodType - ) - val capturedValues = getLambdaCapturedArgumentValues(*capturedArguments) - - // Get MethodHandle and pass captured values to it to obtain an object - // that represents the target functional interface instance. - val handle = site.target - return handle.invokeWithArguments(*capturedValues) - } - """.trimIndent() - } - -private fun buildLambda(visibility: Visibility, language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - /** - * @param samType a class representing a functional interface. - * @param declaringClass a class where the lambda is declared. - * @param lambdaName a name of the synthetic method that represents a lambda. - * @param capturedReceiver an object of {@code declaringClass} that is captured by the lambda. - * When the synthetic lambda method is not static, it means that the lambda captures an instance - * of the class it is declared in. - * @param capturedArguments a vararg containing {@link CapturedArgument} instances representing - * values that the given lambda has captured. - * @return an {@link Object} that represents an instance of the given functional interface {@code samType} - * and implements its single abstract method with the behavior of the given lambda. - */ - ${visibility by language}static Object buildLambda( - Class samType, - Class declaringClass, - String lambdaName, - Object capturedReceiver, - CapturedArgument... capturedArguments - ) throws Throwable { - // Create lookup for class where the lambda is declared in. - java.lang.invoke.MethodHandles.Lookup caller = getLookupIn(declaringClass); - - // Obtain the single abstract method of a functional interface whose instance we are building. - // For example, for `java.util.function.Predicate` it will be method `test`. - java.lang.reflect.Method singleAbstractMethod = getSingleAbstractMethod(samType); - String invokedName = singleAbstractMethod.getName(); - // Method type of single abstract method of the target functional interface. - java.lang.invoke.MethodType samMethodType = java.lang.invoke.MethodType.methodType(singleAbstractMethod.getReturnType(), singleAbstractMethod.getParameterTypes()); - - java.lang.reflect.Method lambdaMethod = getLambdaMethod(declaringClass, lambdaName); - lambdaMethod.setAccessible(true); - java.lang.invoke.MethodType lambdaMethodType = java.lang.invoke.MethodType.methodType(lambdaMethod.getReturnType(), lambdaMethod.getParameterTypes()); - java.lang.invoke.MethodHandle lambdaMethodHandle = caller.findVirtual(declaringClass, lambdaName, lambdaMethodType); - - Class[] capturedArgumentTypes = getLambdaCapturedArgumentTypes(capturedArguments); - java.lang.invoke.MethodType invokedType = java.lang.invoke.MethodType.methodType(samType, capturedReceiver.getClass(), capturedArgumentTypes); - java.lang.invoke.MethodType instantiatedMethodType = getInstantiatedMethodType(lambdaMethod, capturedArgumentTypes); - - // Create a CallSite for the given lambda. - java.lang.invoke.CallSite site = java.lang.invoke.LambdaMetafactory.metafactory( - caller, - invokedName, - invokedType, - samMethodType, - lambdaMethodHandle, - instantiatedMethodType); - - Object[] capturedArgumentValues = getLambdaCapturedArgumentValues(capturedArguments); - - // This array will contain the value of captured receiver - // (`this` instance of class where the lambda is declared) - // and the values of captured arguments. - Object[] capturedValues = new Object[capturedArguments.length + 1]; - - // Setting the captured receiver value. - capturedValues[0] = capturedReceiver; - - // Setting the captured argument values. - System.arraycopy(capturedArgumentValues, 0, capturedValues, 1, capturedArgumentValues.length); - - // Get MethodHandle and pass captured values to it to obtain an object - // that represents the target functional interface instance. - java.lang.invoke.MethodHandle handle = site.getTarget(); - return handle.invokeWithArguments(capturedValues); - } - """.trimIndent() - CodegenLanguage.KOTLIN -> - """ - /** - * @param samType a class representing a functional interface. - * @param declaringClass a class where the lambda is declared. - * @param lambdaName a name of the synthetic method that represents a lambda. - * @param capturedReceiver an object of `declaringClass` that is captured by the lambda. - * When the synthetic lambda method is not static, it means that the lambda captures an instance - * of the class it is declared in. - * @param capturedArguments a vararg containing [CapturedArgument] instances representing - * values that the given lambda has captured. - * @return an [Any] that represents an instance of the given functional interface `samType` - * and implements its single abstract method with the behavior of the given lambda. - */ - ${visibility by language}fun buildLambda( - samType: Class<*>, - declaringClass: Class<*>, - lambdaName: String, - capturedReceiver: Any, - vararg capturedArguments: CapturedArgument - ): Any { - // Create lookup for class where the lambda is declared in. - val caller = getLookupIn(declaringClass) - - // Obtain the single abstract method of a functional interface whose instance we are building. - // For example, for `java.util.function.Predicate` it will be method `test`. - val singleAbstractMethod = getSingleAbstractMethod(samType) - val invokedName = singleAbstractMethod.name - // Method type of single abstract method of the target functional interface. - val samMethodType = java.lang.invoke.MethodType.methodType(singleAbstractMethod.returnType, singleAbstractMethod.parameterTypes) - - val lambdaMethod = getLambdaMethod(declaringClass, lambdaName) - lambdaMethod.isAccessible = true - val lambdaMethodType = java.lang.invoke.MethodType.methodType(lambdaMethod.returnType, lambdaMethod.parameterTypes) - val lambdaMethodHandle = caller.findVirtual(declaringClass, lambdaName, lambdaMethodType) - - val capturedArgumentTypes = getLambdaCapturedArgumentTypes(*capturedArguments) - val invokedType = java.lang.invoke.MethodType.methodType(samType, capturedReceiver.javaClass, *capturedArgumentTypes) - val instantiatedMethodType = getInstantiatedMethodType(lambdaMethod, capturedArgumentTypes) - - // Create a CallSite for the given lambda. - val site = java.lang.invoke.LambdaMetafactory.metafactory( - caller, - invokedName, - invokedType, - samMethodType, - lambdaMethodHandle, - instantiatedMethodType - ) - val capturedValues = mutableListOf() - .apply { - add(capturedReceiver) - addAll(getLambdaCapturedArgumentValues(*capturedArguments)) - }.toTypedArray() - - - // Get MethodHandle and pass captured values to it to obtain an object - // that represents the target functional interface instance. - val handle = site.target - return handle.invokeWithArguments(*capturedValues) - } - """.trimIndent() - } - -private fun getLookupIn(language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - /** - * @param clazz a class to create lookup instance for. - * @return {@link java.lang.invoke.MethodHandles.Lookup} instance for the given {@code clazz}. - * It can be used, for example, to search methods of this {@code clazz}, even the {@code private} ones. - */ - private static java.lang.invoke.MethodHandles.Lookup getLookupIn(Class clazz) throws IllegalAccessException, NoSuchFieldException { - java.lang.invoke.MethodHandles.Lookup lookup = java.lang.invoke.MethodHandles.lookup().in(clazz); - - // Allow lookup to access all members of declaringClass, including the private ones. - // For example, it is useful to access private synthetic methods representing lambdas. - java.lang.reflect.Field allowedModes = java.lang.invoke.MethodHandles.Lookup.class.getDeclaredField("allowedModes"); - allowedModes.setAccessible(true); - allowedModes.setInt(lookup, java.lang.reflect.Modifier.PUBLIC | java.lang.reflect.Modifier.PROTECTED | java.lang.reflect.Modifier.PRIVATE | java.lang.reflect.Modifier.STATIC); - - return lookup; - } - """.trimIndent() - CodegenLanguage.KOTLIN -> - """ - /** - * @param clazz a class to create lookup instance for. - * @return [java.lang.invoke.MethodHandles.Lookup] instance for the given [clazz]. - * It can be used, for example, to search methods of this [clazz], even the `private` ones. - */ - private fun getLookupIn(clazz: Class<*>): java.lang.invoke.MethodHandles.Lookup { - val lookup = java.lang.invoke.MethodHandles.lookup().`in`(clazz) - - // Allow lookup to access all members of declaringClass, including the private ones. - // For example, it is useful to access private synthetic methods representing lambdas. - val allowedModes = java.lang.invoke.MethodHandles.Lookup::class.java.getDeclaredField("allowedModes") - allowedModes.isAccessible = true - allowedModes.setInt(lookup, java.lang.reflect.Modifier.PUBLIC or java.lang.reflect.Modifier.PROTECTED or java.lang.reflect.Modifier.PRIVATE or java.lang.reflect.Modifier.STATIC) - - return lookup - } - """.trimIndent() - } - -private fun getLambdaCapturedArgumentTypes(language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - /** - * @param capturedArguments values captured by some lambda. Note that this argument does not contain - * the possibly captured instance of the class where the lambda is declared. - * It contains all the other captured values. They are represented as arguments of the synthetic method - * that the lambda is compiled into. Hence, the name of the argument. - * @return types of the given {@code capturedArguments}. - * These types are required to build {@code invokedType}, which represents - * the target functional interface with info about captured values' types. - * See {@link java.lang.invoke.LambdaMetafactory#metafactory} method documentation for more details on what {@code invokedType} is. - */ - private static Class[] getLambdaCapturedArgumentTypes(CapturedArgument... capturedArguments) { - Class[] capturedArgumentTypes = new Class[capturedArguments.length]; - for (int i = 0; i < capturedArguments.length; i++) { - capturedArgumentTypes[i] = capturedArguments[i].type; - } - return capturedArgumentTypes; - } - """.trimIndent() - CodegenLanguage.KOTLIN -> - """ - /** - * @param capturedArguments values captured by some lambda. Note that this argument does not contain - * the possibly captured instance of the class where the lambda is declared. - * It contains all the other captured values. They are represented as arguments of the synthetic method - * that the lambda is compiled into. Hence, the name of the argument. - * @return types of the given `capturedArguments`. - * These types are required to build `invokedType`, which represents - * the target functional interface with info about captured values' types. - * See [java.lang.invoke.LambdaMetafactory.metafactory] method documentation for more details on what `invokedType` is. - */ - private fun getLambdaCapturedArgumentTypes(vararg capturedArguments: CapturedArgument): Array> { - return capturedArguments - .map { it.type } - .toTypedArray() - } - """.trimIndent() - } - -private fun getLambdaCapturedArgumentValues(language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - /** - * Obtain captured values to be used as captured arguments in the lambda call. - */ - private static Object[] getLambdaCapturedArgumentValues(CapturedArgument... capturedArguments) { - return java.util.Arrays.stream(capturedArguments) - .map(argument -> argument.value) - .toArray(); - } - """.trimIndent() - CodegenLanguage.KOTLIN -> - """ - /** - * Obtain captured values to be used as captured arguments in the lambda call. - */ - private fun getLambdaCapturedArgumentValues(vararg capturedArguments: CapturedArgument): Array { - return capturedArguments - .map { it.value } - .toTypedArray() - } - """.trimIndent() - } - -private fun getInstantiatedMethodType(language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - /** - * @param lambdaMethod {@link java.lang.reflect.Method} that represents a synthetic method for lambda. - * @param capturedArgumentTypes types of values captured by lambda. - * @return {@link java.lang.invoke.MethodType} that represents the value of argument {@code instantiatedMethodType} - * of method {@link java.lang.invoke.LambdaMetafactory#metafactory}. - */ - private static java.lang.invoke.MethodType getInstantiatedMethodType(java.lang.reflect.Method lambdaMethod, Class[] capturedArgumentTypes) { - // Types of arguments of synthetic method (representing lambda) without the types of captured values. - java.util.List> instantiatedMethodParamTypeList = java.util.Arrays.stream(lambdaMethod.getParameterTypes()) - .skip(capturedArgumentTypes.length) - .collect(java.util.stream.Collectors.toList()); - - // The same types, but stored in an array. - Class[] instantiatedMethodParamTypes = new Class[instantiatedMethodParamTypeList.size()]; - for (int i = 0; i < instantiatedMethodParamTypeList.size(); i++) { - instantiatedMethodParamTypes[i] = instantiatedMethodParamTypeList.get(i); - } - - return java.lang.invoke.MethodType.methodType(lambdaMethod.getReturnType(), instantiatedMethodParamTypes); - } - """.trimIndent() - CodegenLanguage.KOTLIN -> - """ - /** - * @param lambdaMethod [java.lang.reflect.Method] that represents a synthetic method for lambda. - * @param capturedArgumentTypes types of values captured by lambda. - * @return [java.lang.invoke.MethodType] that represents the value of argument `instantiatedMethodType` - * of method [java.lang.invoke.LambdaMetafactory.metafactory]. - */ - private fun getInstantiatedMethodType( - lambdaMethod: java.lang.reflect.Method, - capturedArgumentTypes: Array> - ): java.lang.invoke.MethodType { - // Types of arguments of synthetic method (representing lambda) without the types of captured values. - val instantiatedMethodParamTypes = lambdaMethod.parameterTypes - .drop(capturedArgumentTypes.size) - .toTypedArray() - - return java.lang.invoke.MethodType.methodType(lambdaMethod.returnType, instantiatedMethodParamTypes) - } - """.trimIndent() - } - -private fun getLambdaMethod(language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - /** - * @param declaringClass class where a lambda is declared. - * @param lambdaName name of synthetic method that represents a lambda. - * @return {@link java.lang.reflect.Method} instance for the synthetic method that represent a lambda. - */ - private static java.lang.reflect.Method getLambdaMethod(Class declaringClass, String lambdaName) { - return java.util.Arrays.stream(declaringClass.getDeclaredMethods()) - .filter(method -> method.getName().equals(lambdaName)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No lambda method named " + lambdaName + " was found in class: " + declaringClass.getCanonicalName())); - } - """.trimIndent() - CodegenLanguage.KOTLIN -> - """ - /** - * @param declaringClass class where a lambda is declared. - * @param lambdaName name of synthetic method that represents a lambda. - * @return [java.lang.reflect.Method] instance for the synthetic method that represent a lambda. - */ - private fun getLambdaMethod(declaringClass: Class<*>, lambdaName: String): java.lang.reflect.Method { - return declaringClass.declaredMethods.firstOrNull { it.name == lambdaName } - ?: throw IllegalArgumentException("No lambda method named ${'$'}lambdaName was found in class: ${'$'}{declaringClass.canonicalName}") - } - """.trimIndent() - } - -private fun getSingleAbstractMethod(language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - private static java.lang.reflect.Method getSingleAbstractMethod(Class clazz) { - java.util.List abstractMethods = java.util.Arrays.stream(clazz.getMethods()) - .filter(method -> java.lang.reflect.Modifier.isAbstract(method.getModifiers())) - .collect(java.util.stream.Collectors.toList()); - - if (abstractMethods.isEmpty()) { - throw new IllegalArgumentException("No abstract methods found in class: " + clazz.getCanonicalName()); - } - if (abstractMethods.size() > 1) { - throw new IllegalArgumentException("More than one abstract method found in class: " + clazz.getCanonicalName()); - } - - return abstractMethods.get(0); - } - """.trimIndent() - CodegenLanguage.KOTLIN -> - """ - /** - * @param clazz functional interface - * @return a [java.lang.reflect.Method] for the single abstract method of the given functional interface `clazz`. - */ - private fun getSingleAbstractMethod(clazz: Class<*>): java.lang.reflect.Method { - val abstractMethods = clazz.methods.filter { java.lang.reflect.Modifier.isAbstract(it.modifiers) } - require(abstractMethods.isNotEmpty()) { "No abstract methods found in class: " + clazz.canonicalName } - require(abstractMethods.size <= 1) { "More than one abstract method found in class: " + clazz.canonicalName } - return abstractMethods[0] - } - """.trimIndent() - } - -private fun capturedArgumentClass(language: CodegenLanguage) = - when (language) { - CodegenLanguage.JAVA -> - """ - /** - * This class represents the {@code type} and {@code value} of a value captured by lambda. - * Captured values are represented as arguments of a synthetic method that lambda is compiled into, - * hence the name of the class. - */ - public static class CapturedArgument { - private Class type; - private Object value; - - public CapturedArgument(Class type, Object value) { - this.type = type; - this.value = value; - } - } - """.trimIndent() - CodegenLanguage.KOTLIN -> { - """ - /** - * This class represents the `type` and `value` of a value captured by lambda. - * Captured values are represented as arguments of a synthetic method that lambda is compiled into, - * hence the name of the class. - */ - data class CapturedArgument(val type: Class<*>, val value: Any?) - """.trimIndent() - } - } - -internal fun CgContextOwner.importUtilMethodDependencies(id: MethodId) { - // if util methods come from a separate UtUtils class and not from the test class, - // then we don't need to import any other methods, hence we return from method - val utilMethodProvider = utilMethodProvider as? TestClassUtilMethodProvider ?: return - for (classId in utilMethodProvider.regularImportsByUtilMethod(id, codegenLanguage)) { - importIfNeeded(classId) - } - for (methodId in utilMethodProvider.staticImportsByUtilMethod(id)) { - collectedImports += StaticImport(methodId.classId.canonicalName, methodId.name) - } -} - -private fun TestClassUtilMethodProvider.regularImportsByUtilMethod( - id: MethodId, - codegenLanguage: CodegenLanguage -): List { - return when (id) { - getUnsafeInstanceMethodId -> listOf(fieldClassId) - 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) - getFieldValueMethodId -> listOf(fieldClassId, Modifier::class.id) - getStaticFieldValueMethodId -> listOf(fieldClassId, Modifier::class.id) - getEnumConstantByNameMethodId -> listOf(fieldClassId) - deepEqualsMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf( - Objects::class.id, - Iterable::class.id, - Map::class.id, - List::class.id, - ArrayList::class.id, - Set::class.id, - HashSet::class.id, - fieldClassId, - Arrays::class.id - ) - CodegenLanguage.KOTLIN -> listOf(fieldClassId, Arrays::class.id) - } - arraysDeepEqualsMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf(java.lang.reflect.Array::class.id, Set::class.id) - CodegenLanguage.KOTLIN -> listOf(java.lang.reflect.Array::class.id) - } - iterablesDeepEqualsMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf(Iterable::class.id, Iterator::class.id, Set::class.id) - CodegenLanguage.KOTLIN -> emptyList() - } - streamsDeepEqualsMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf(java.util.stream.BaseStream::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 -> emptyList() - } - hasCustomEqualsMethodId -> emptyList() - getArrayLengthMethodId -> listOf(java.lang.reflect.Array::class.id) - buildStaticLambdaMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf( - MethodHandles::class.id, Method::class.id, MethodType::class.id, - MethodHandle::class.id, CallSite::class.id, LambdaMetafactory::class.id - ) - CodegenLanguage.KOTLIN -> listOf(MethodType::class.id, LambdaMetafactory::class.id) - } - buildLambdaMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf( - MethodHandles::class.id, Method::class.id, MethodType::class.id, - MethodHandle::class.id, CallSite::class.id, LambdaMetafactory::class.id - ) - CodegenLanguage.KOTLIN -> listOf(MethodType::class.id, LambdaMetafactory::class.id) - } - getLookupInMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf(MethodHandles::class.id, Field::class.id, Modifier::class.id) - CodegenLanguage.KOTLIN -> listOf(MethodHandles::class.id, Modifier::class.id) - } - getLambdaCapturedArgumentTypesMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf(LambdaMetafactory::class.id) - CodegenLanguage.KOTLIN -> listOf(LambdaMetafactory::class.id) - } - getLambdaCapturedArgumentValuesMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf(Arrays::class.id) - CodegenLanguage.KOTLIN -> emptyList() - } - getInstantiatedMethodTypeMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf( - Method::class.id, MethodType::class.id, LambdaMetafactory::class.id, - java.util.List::class.id, Arrays::class.id, Collectors::class.id - ) - CodegenLanguage.KOTLIN -> listOf(Method::class.id, MethodType::class.id, LambdaMetafactory::class.id) - } - getLambdaMethodMethodId -> when (codegenLanguage) { - CodegenLanguage.JAVA -> listOf(Method::class.id, Arrays::class.id) - CodegenLanguage.KOTLIN -> listOf(Method::class.id) - } - getSingleAbstractMethodMethodId -> listOf( - Method::class.id, java.util.List::class.id, Arrays::class.id, - Modifier::class.id, Collectors::class.id - ) - else -> error("Unknown util method for class $this: $id") - } -} - -// Note: for now always returns an empty list, because no util method -// requires static imports, but this may change in the future -@Suppress("unused", "unused_parameter") -private fun TestClassUtilMethodProvider.staticImportsByUtilMethod(id: MethodId): List = emptyList() \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgAbstractRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgAbstractRenderer.kt new file mode 100644 index 0000000000..2d3e357403 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgAbstractRenderer.kt @@ -0,0 +1,996 @@ +package org.utbot.framework.codegen.renderer + +import org.apache.commons.text.StringEscapeUtils +import org.utbot.common.FileUtil +import org.utbot.common.WorkaroundReason.LONG_CODE_FRAGMENTS +import org.utbot.common.workaround +import org.utbot.framework.UtSettings +import org.utbot.framework.codegen.domain.Import +import org.utbot.framework.codegen.domain.RegularImport +import org.utbot.framework.codegen.domain.StaticImport +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.domain.models.CgAbstractFieldAccess +import org.utbot.framework.codegen.domain.models.CgAbstractMultilineComment +import org.utbot.framework.codegen.domain.models.CgArrayElementAccess +import org.utbot.framework.codegen.domain.models.CgAssignment +import org.utbot.framework.codegen.domain.models.CgAuxiliaryClass +import org.utbot.framework.codegen.domain.models.CgBreakStatement +import org.utbot.framework.codegen.domain.models.CgClass +import org.utbot.framework.codegen.domain.models.CgComment +import org.utbot.framework.codegen.domain.models.CgCommentedAnnotation +import org.utbot.framework.codegen.domain.models.CgComparison +import org.utbot.framework.codegen.domain.models.CgContinueStatement +import org.utbot.framework.codegen.domain.models.CgCustomTagStatement +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgDecrement +import org.utbot.framework.codegen.domain.models.CgDoWhileLoop +import org.utbot.framework.codegen.domain.models.CgDocClassLinkStmt +import org.utbot.framework.codegen.domain.models.CgDocCodeStmt +import org.utbot.framework.codegen.domain.models.CgDocMethodLinkStmt +import org.utbot.framework.codegen.domain.models.CgDocPreTagStatement +import org.utbot.framework.codegen.domain.models.CgDocRegularLineStmt +import org.utbot.framework.codegen.domain.models.CgDocRegularStmt +import org.utbot.framework.codegen.domain.models.CgDocumentationComment +import org.utbot.framework.codegen.domain.models.CgElement +import org.utbot.framework.codegen.domain.models.CgEmptyLine +import org.utbot.framework.codegen.domain.models.CgEnumConstantAccess +import org.utbot.framework.codegen.domain.models.CgErrorTestMethod +import org.utbot.framework.codegen.domain.models.CgErrorWrapper +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgMethodsCluster +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgFieldAccess +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgForEachLoop +import org.utbot.framework.codegen.domain.models.CgForLoop +import org.utbot.framework.codegen.domain.models.CgFrameworkUtilMethod +import org.utbot.framework.codegen.domain.models.CgGreaterThan +import org.utbot.framework.codegen.domain.models.CgIfStatement +import org.utbot.framework.codegen.domain.models.CgIncrement +import org.utbot.framework.codegen.domain.models.CgInnerBlock +import org.utbot.framework.codegen.domain.models.CgIsInstance +import org.utbot.framework.codegen.domain.models.CgLessThan +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgLogicalAnd +import org.utbot.framework.codegen.domain.models.CgLogicalOr +import org.utbot.framework.codegen.domain.models.CgLoop +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgMultilineComment +import org.utbot.framework.codegen.domain.models.CgMultipleArgsAnnotation +import org.utbot.framework.codegen.domain.models.CgNamedAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgNestedClassesRegion +import org.utbot.framework.codegen.domain.models.CgNonStaticRunnable +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.domain.models.CgRegion +import org.utbot.framework.codegen.domain.models.CgReturnStatement +import org.utbot.framework.codegen.domain.models.CgSimpleRegion +import org.utbot.framework.codegen.domain.models.CgSingleArgAnnotation +import org.utbot.framework.codegen.domain.models.CgSingleLineComment +import org.utbot.framework.codegen.domain.models.CgSpread +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgStatementExecutableCall +import org.utbot.framework.codegen.domain.models.CgStaticFieldAccess +import org.utbot.framework.codegen.domain.models.CgStaticRunnable +import org.utbot.framework.codegen.domain.models.CgStaticsRegion +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTestMethodCluster +import org.utbot.framework.codegen.domain.models.CgThisInstance +import org.utbot.framework.codegen.domain.models.CgThrowStatement +import org.utbot.framework.codegen.domain.models.CgTripleSlashMultilineComment +import org.utbot.framework.codegen.domain.models.CgTryCatch +import org.utbot.framework.codegen.domain.models.CgUtilMethod +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.domain.models.CgWhileLoop +import org.utbot.framework.codegen.tree.VisibilityModifier +import org.utbot.framework.codegen.tree.ututils.UtilClassKind +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.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.isArray +import org.utbot.framework.plugin.api.util.isRefType +import org.utbot.framework.plugin.api.util.longClassId +import org.utbot.framework.plugin.api.util.shortClassId + +abstract class CgAbstractRenderer( + val context: CgRendererContext, + val printer: CgPrinter = CgPrinterImpl() +) : CgVisitor, + CgPrinter by printer { + + protected abstract val statementEnding: String + + protected abstract val logicalAnd: String + protected abstract val logicalOr: String + + protected open val regionStart: String = "///region" + protected open val regionEnd: String = "///endregion" + protected var isInterrupted = false + + 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 element type. + protected fun arrayElementsInLine(elementType: ClassId): Int { + if (elementType.isRefType) return 10 + if (elementType.isArray) return 1 + return when (elementType) { + intClassId, byteClassId, longClassId, charClassId -> 8 + booleanClassId, shortClassId, doubleClassId, floatClassId -> 6 + else -> error("Non primitive value of type $elementType is unexpected in array initializer") + } + } + + /** + * Returns true if one can call methods of this class without specifying a caller (for example if ClassId represents this instance) + */ + protected abstract val ClassId.methodsAreAccessibleAsTopLevel: Boolean + + private val MethodId.accessibleByName: Boolean + get() = (context.shouldOptimizeImports && this in context.importedStaticMethods) || classId.methodsAreAccessibleAsTopLevel + + override fun visit(element: CgElement) { + val error = + "CgRenderer has reached the top of Cg elements hierarchy and did not find a method for ${element::class}" + throw IllegalArgumentException(error) + } + + override fun visit(element: CgClassFile) { + renderClassPackage(element.declaredClass) + renderClassFileImports(element) + element.declaredClass.accept(this) + } + + /** + * Render the region only if it is not empty. + */ + override fun visit(element: CgStaticsRegion) { + element.render() + } + + /** + * Render the region only if it is not empty. + */ + override fun visit(element: CgNestedClassesRegion<*>) { + element.render() + } + + /** + * Render the region only if it is not empty. + */ + override fun visit(element: CgSimpleRegion<*>) { + element.render() + } + + /** + * Render the cluster only if it is not empty. + */ + override fun visit(element: CgTestMethodCluster) { + element.render() + } + + /** + * Render the cluster only if it is not empty. + */ + override fun visit(element: CgMethodsCluster) { + // We print the next line after all contained regions to prevent gluing of region ends + element.render(printLineAfterContentEnd = true) + } + + /** + * Renders the region with a specific rendering for [CgTestMethodCluster.description] + */ + private fun CgRegion<*>.render(printLineAfterContentEnd: Boolean = false) { + if (content.isEmpty() || isInterrupted) return + + header?.let { + print(regionStart) + println(" $it") + } + + if (this is CgTestMethodCluster) description?.accept(this@CgAbstractRenderer) + + var isLimitExceeded = false + for (method in content) { + if (printer.printedLength > UtSettings.maxTestFileSize) { + isLimitExceeded = true + break + } + println() + method.accept(this@CgAbstractRenderer) + } + + if (printLineAfterContentEnd) println() + + header?.let { + println(regionEnd) + } + + if (isLimitExceeded && !isInterrupted) { + visit(CgSingleLineComment("Abrupt generation termination: file size exceeds configured limit (${FileUtil.byteCountToDisplaySize(UtSettings.maxTestFileSize.toLong())})")) + visit(CgSingleLineComment("The limit can be configured in '{HOME_DIR}/.utbot/settings.properties' with 'maxTestsFileSize' property")) + isInterrupted = true + } + } + + override fun visit(element: CgAuxiliaryClass) { + val auxiliaryClassText = element.getText(context) + auxiliaryClassText.split("\n") + .forEach { line -> println(line) } + } + + override fun visit(element: CgUtilMethod) { + val utilMethodText = element.getText(context) + utilMethodText.split("\n") + .forEach { line -> println(line) } + } + + // Methods + + override fun visit(element: CgMethod) { + // TODO introduce CgBlock + print(" ") + visit(element.statements, printNextLine = true) + } + + override fun visit(element: CgFrameworkUtilMethod) { + for (annotation in element.annotations) { + annotation.accept(this) + } + renderMethodSignature(element) + visit(element as CgMethod) + } + + override fun visit(element: CgTestMethod) { + if (UtSettings.addTestMethodMarkers) { + visit(CgSingleLineComment(TEST_METHOD_START_MARKER)) + } + renderMethodDocumentation(element) + for (annotation in element.annotations) { + annotation.accept(this) + } + renderMethodSignature(element) + visit(element as CgMethod) + if (UtSettings.addTestMethodMarkers) { + visit(CgSingleLineComment(TEST_METHOD_END_MARKER)) + } + } + + override fun visit(element: CgErrorTestMethod) { + renderMethodDocumentation(element) + renderMethodSignature(element) + visit(element as CgMethod) + } + + override fun visit(element: CgParameterizedTestDataProviderMethod) { + for (annotation in element.annotations) { + annotation.accept(this) + } + renderMethodSignature(element) + visit(element as CgMethod) + } + + // Annotations + + override fun visit(element: CgCommentedAnnotation) { + print("//") + element.annotation.accept(this) + } + + override fun visit(element: CgSingleArgAnnotation) { + print("@${element.classId.asString()}") + print("(") + element.argument.accept(this) + println(")") + } + + override fun visit(element: CgMultipleArgsAnnotation) { + print("@${element.classId.asString()}") + if (element.arguments.isNotEmpty()) { + print("(") + element.arguments.renderSeparated() + print(")") + } + println() + } + + override fun visit(element: CgNamedAnnotationArgument) { + print(element.name) + print(" = ") + element.value.accept(this) + } + + // Comments + + override fun visit(element: CgComment) { + visit(element as CgElement) + } + + override fun visit(element: CgSingleLineComment) { + println("// ${element.comment}") + } + + override fun visit(element: CgAbstractMultilineComment) { + visit(element as CgElement) + } + + override fun visit(element: CgTripleSlashMultilineComment) { + for (line in element.lines) { + println("/// $line") + } + } + + override fun visit(element: CgMultilineComment) { + val lines = element.lines + if (lines.isEmpty()) return + + if (lines.size == 1) { + println("/* ${lines.first()} */") + return + } + + // print lines saving indentation + print("/* ") + println(lines.first()) + lines.subList(1, lines.lastIndex).forEach { println(it) } + print(lines.last()) + println(" */") + } + + override fun visit(element: CgDocumentationComment) { + if (element.lines.all { it.isEmpty() }) return + + println("/**") + element.lines.forEach { it.accept(this) } + println(" */") + } + override fun visit(element: CgDocPreTagStatement) { + if (element.content.all { it.isEmpty() }) return + println("
    ")
    +        for (stmt in element.content) stmt.accept(this)
    +        println("
    ") + } + + override fun visit(element: CgCustomTagStatement) { + if (element.content.all { it.isEmpty() }) return + + element.content.forEach { it.accept(this) } + } + + override fun visit(element: CgDocCodeStmt) { + if (element.isEmpty()) return + + val text = element.stmt + .replace("\n", "\n * ") + //remove multiline comment symbols to avoid comment in comment effect + .replace("/*", "") + .replace("*/", "") + print("{@code $text }") + } + override fun visit(element: CgDocRegularStmt){ + if (element.isEmpty()) return + + print(element.stmt.replace("\n", "\n * ")) + } + override fun visit(element: CgDocRegularLineStmt){ + if (element.isEmpty()) return + + // It is better to avoid using \n in print, using println is preferred. + // Mixing println's and print's with '\n' BREAKS indention. + // See [https://stackoverflow.com/questions/6685665/system-out-println-vs-n-in-java]. + println(" * " + element.stmt) + } + override fun visit(element: CgDocClassLinkStmt) { + if (element.isEmpty()) return + + print(element.className) + } + override fun visit(element: CgDocMethodLinkStmt){ + if (element.isEmpty()) return + + print("${element.className}::${element.methodName}") //todo make it as link {@link org.utbot.examples.online.Loops#whileLoop(int) } + } + + /** + * Renders any block of code with curly braces. + * + * NOTE: [printNextLine] has default false value in [CgVisitor] + * + * NOTE: due to JVM restrictions for methods size + * [in 65536 bytes](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.3) + * we comment long blocks + */ + override fun visit(block: List, printNextLine: Boolean) { + println("{") + + val isBlockTooLarge = workaround(LONG_CODE_FRAGMENTS) { block.size > LARGE_CODE_BLOCK_SIZE } + + if (isBlockTooLarge) { + print("/*") + println(" This block of code is ${block.size} lines long and could lead to compilation error") + } + + withIndent { + for (statement in block) { + statement.accept(this) + } + } + + if (isBlockTooLarge) println("*/") + + print("}") + + if (printNextLine) println() + } + + // Return statement + + override fun visit(element: CgReturnStatement) { + print("return ") + element.expression.accept(this) + println(statementEnding) + } + + // Array element access + + override fun visit(element: CgArrayElementAccess) { + element.array.accept(this) + print("[") + element.index.accept(this) + print("]") + } + + // Spread operator + + override fun visit(element: CgSpread) { + element.array.accept(this) + } + + // Loop conditions + + override fun visit(element: CgComparison) { + return visit(element as CgElement) + } + + override fun visit(element: CgLessThan) { + element.left.accept(this) + print(" < ") + element.right.accept(this) + } + + override fun visit(element: CgGreaterThan) { + element.left.accept(this) + print(" > ") + element.right.accept(this) + } + + // Increment and decrement + + override fun visit(element: CgIncrement) { + print("${element.variable.name}++") + } + + override fun visit(element: CgDecrement) { + print("${element.variable.name}--") + } + + // isInstance check + + override fun visit(element: CgIsInstance) { + element.classExpression.accept(this) + print(".isInstance(") + element.value.accept(this) + print(")") + } + + // Try-catch + + override fun visit(element: CgTryCatch) { + print("try ") + // TODO: SAT-1329 actually Kotlin does not support try-with-resources so we have to use "use" method here + element.resources?.let { + println("(") + withIndent { + for (resource in element.resources) { + resource.accept(this) + } + } + print(") ") + } + // TODO introduce CgBlock + visit(element.statements) + for ((exception, statements) in element.handlers) { + print(" catch (") + renderExceptionCatchVariable(exception) + print(") ") + // TODO introduce CgBlock + visit(statements, printNextLine = element.finally == null) + } + element.finally?.let { + print(" finally ") + // TODO introduce CgBlock + visit(element.finally, printNextLine = true) + } + } + + abstract override fun visit(element: CgErrorWrapper) + + //Simple block + + abstract override fun visit(element: CgInnerBlock) + + // Loops + + override fun visit(element: CgLoop) { + return visit(element as CgElement) + } + + override fun visit(element: CgForLoop) { + renderForLoopVarControl(element) + print(") ") + // TODO introduce CgBlock + visit(element.statements) + println() + } + + override fun visit(element: CgForEachLoop) {} + + override fun visit(element: CgWhileLoop) { + print("while (") + element.condition.accept(this) + print(") ") + // TODO introduce CgBlock + visit(element.statements) + println() + } + + override fun visit(element: CgDoWhileLoop) { + print("do ") + // TODO introduce CgBlock + visit(element.statements) + print(" while (") + element.condition.accept(this) + println(");") + } + + // Control statements + override fun visit(element: CgBreakStatement) { + println("break$statementEnding") + } + + override fun visit(element: CgContinueStatement) { + println("continue$statementEnding") + } + + // Variable declaration + + override fun visit(element: CgDeclaration) { + renderDeclarationLeftPart(element) + element.initializer?.let { + print(" = ") + it.accept(this) + } + println(statementEnding) + } + + // Class field declaration + + override fun visit(element: CgFieldDeclaration) { + element.annotation?.accept(this) + renderVisibility(element.visibility) + element.declaration.accept(this) + } + + // Variable assignment + + override fun visit(element: CgAssignment) { + element.lValue.accept(this) + print(" = ") + element.rValue.accept(this) + println(statementEnding) + } + + // Expressions + + override fun visit(element: CgExpression) { + visit(element as CgElement) + } + + // This instance + + override fun visit(element: CgThisInstance) { + print("this") + } + + // Variables + + override fun visit(element: CgVariable) { + print(element.name.escapeNamePossibleKeyword()) + } + + // Method parameters + + abstract override fun visit(element: CgParameterDeclaration) + + // Primitive and String literals + + override fun visit(element: CgLiteral) { + print(element.toStringConstant()) + } + + protected fun CgLiteral.toStringConstant(asRawString: Boolean = false) = + with(this.value) { + when (this) { + is Byte -> toStringConstant() + is Char -> toStringConstant() + is Short -> toStringConstant() + is Int -> toStringConstant() + is Long -> toStringConstant() + is Float -> toStringConstant() + is Double -> toStringConstant() + is Boolean -> toStringConstant() + // String is "\"" + "str" + "\"", RawString is "str" + is String -> if (asRawString) "$this".escapeCharacters() else toStringConstant() + else -> "$this" + } + } + + // Non-static runnable like this::toString or (new Object())::toString etc + override fun visit(element: CgNonStaticRunnable) { + // TODO we need braces for expressions like (new Object())::toString but not for this::toString + print("(") + element.referenceExpression.accept(this) + print(")::") + print(element.methodId.name) + } + + // Static runnable like Random::nextRandomInt etc + override fun visit(element: CgStaticRunnable) { + print(element.classId.asString()) + print("::") + print(element.methodId.name) + } + + // Enum constant + + override fun visit(element: CgEnumConstantAccess) { + print(element.enumClass.asString()) + print(".") + print(element.name) + } + + // Property access + + override fun visit(element: CgAbstractFieldAccess) { + visit(element as CgElement) + } + + override fun visit(element: CgFieldAccess) { + element.caller.accept(this) + print(".") + print(element.fieldId.name) + } + + override fun visit(element: CgStaticFieldAccess) { + if (!element.declaringClass.methodsAreAccessibleAsTopLevel) { + print(element.declaringClass.asString()) + print(".") + } + print(element.fieldName) + } + + // Conditional statement + + override fun visit(element: CgIfStatement) { + print("if (") + element.condition.accept(this) + print(") ") + // TODO introduce CgBlock + visit(element.trueBranch) + element.falseBranch?.let { + print(" else ") + // TODO introduce CgBlock + visit(element.falseBranch) + } + println() + } + + // Binary logical operators + + override fun visit(element: CgLogicalAnd) { + element.left.accept(this) + print(" $logicalAnd ") + element.right.accept(this) + } + + override fun visit(element: CgLogicalOr) { + element.left.accept(this) + print(" $logicalOr ") + element.right.accept(this) + } + + // Executable calls + + override fun visit(element: CgStatementExecutableCall) { + element.call.accept(this) + println(statementEnding) + } + + override fun visit(element: CgExecutableCall) { + visit(element as CgElement) + } + + // TODO: consider the case of generic functions + // TODO: write tests for all cases of method call rendering (with or without caller, etc.) + override fun visit(element: CgMethodCall) { + val caller = element.caller + if (caller != null) { + // 'this' can be omitted, otherwise render caller + if (caller !is CgThisInstance) { + // TODO: we need parentheses for calls like (-1).inv(), do something smarter here + if (caller !is CgVariable) print("(") + caller.accept(this) + if (caller !is CgVariable) print(")") + renderAccess(caller) + } + } else { + // for static methods render declaring class only if required + if (!element.executableId.accessibleByName) { + val method = element.executableId + print(method.classId.asString()) + print(".") + } + } + print(element.executableId.name.escapeNamePossibleKeyword()) + + renderTypeParameters(element.typeParameters) + renderExecutableCallArguments(element) + } + + // Throw statement + + override fun visit(element: CgThrowStatement) { + print("throw ") + element.exception.accept(this) + println(statementEnding) + } + + override fun visit(element: CgEmptyLine) { + println() + } + + override fun toString(): String = printer.toString() + + protected abstract fun renderRegularImport(regularImport: RegularImport) + protected abstract fun renderStaticImport(staticImport: StaticImport) + + //we render parameters in method signature on one line or on separate lines depending their amount + protected val maxParametersAmountInOneLine = 3 + + protected abstract fun renderMethodSignature(element: CgTestMethod) + protected abstract fun renderMethodSignature(element: CgErrorTestMethod) + protected abstract fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) + protected abstract fun renderMethodSignature(element: CgFrameworkUtilMethod) + + protected abstract fun renderForLoopVarControl(element: CgForLoop) + + protected abstract fun renderDeclarationLeftPart(element: CgDeclaration) + + protected abstract fun toStringConstantImpl(byte: Byte): String + protected abstract fun toStringConstantImpl(short: Short): String + protected abstract fun toStringConstantImpl(int: Int): String + protected abstract fun toStringConstantImpl(long: Long): String + protected abstract fun toStringConstantImpl(float: Float): String + + protected abstract fun renderAccess(caller: CgExpression) + protected abstract fun renderTypeParameters(typeParameters: TypeParameters) + protected abstract fun renderExecutableCallArguments(executableCall: CgExecutableCall) + + protected abstract fun renderExceptionCatchVariable(exception: CgVariable) + + protected fun getEscapedImportRendering(import: Import): String = + import.qualifiedName + .split(".") + .joinToString(".") { it.escapeNamePossibleKeyword() } + + protected fun List.printSeparated(newLines: Boolean = false) { + for ((index, element) in this.withIndex()) { + print(element) + when { + index < lastIndex -> { + print(",") + if (newLines) println() else print(" ") + } + index == lastIndex -> if (newLines) println() + } + } + } + + protected fun List.renderSeparated(newLines: Boolean = false) { + for ((index, element) in this.withIndex()) { + element.accept(this@CgAbstractRenderer) + when { + index < lastIndex -> { + print(",") + if (newLines) println() else print(" ") + } + index == lastIndex -> if (newLines) println() + } + } + } + + protected fun List.renderElements(elementsInLine: Int) { + val length = this.size + if (length <= elementsInLine) { // one-line array + for (i in 0 until length) { + val expr = this[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[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() + block() + } finally { + popIndent() + } + } + + protected open fun isAccessibleBySimpleNameImpl(classId: ClassId): Boolean = + classId in context.importedClasses || + classId.simpleName !in context.importedClasses.map { it.simpleName } && classId.packageName == context.classPackageName + + protected abstract fun escapeNamePossibleKeywordImpl(s: String): String + + protected fun String.escapeNamePossibleKeyword(): String = escapeNamePossibleKeywordImpl(this) + + protected fun ClassId.asString(): String { + if (!context.shouldOptimizeImports) return canonicalName + + // use simpleNameWithEnclosings instead of simpleName to consider nested classes case + return if (this.isAccessibleBySimpleName()) simpleNameWithEnclosingClasses else canonicalName + } + + private fun renderClassPackage(element: CgClass) { + if (element.packageName.isNotEmpty()) { + println("package ${element.packageName}${statementEnding}") + println() + } + } + + protected open fun renderClassFileImports(element: CgClassFile) { + val regularImports = element.imports.filterIsInstance() + val staticImports = element.imports.filterIsInstance() + + for (import in regularImports) { + renderRegularImport(import) + } + if (regularImports.isNotEmpty()) { + println() + } + + for (import in staticImports) { + renderStaticImport(import) + } + if (staticImports.isNotEmpty()) { + println() + } + } + + protected abstract fun renderVisibility(modifier: VisibilityModifier) + + protected abstract fun renderClassModality(aClass: CgClass) + + protected fun renderMethodDocumentation(element: CgMethod) { + element.documentation.accept(this) + } + + private fun Byte.toStringConstant() = when { + this == Byte.MAX_VALUE -> "${langPackage}.Byte.MAX_VALUE" + this == Byte.MIN_VALUE -> "${langPackage}.Byte.MIN_VALUE" + else -> toStringConstantImpl(this) + } + + private fun Char.toStringConstant() = when (this) { + '\'' -> "'\\''" + else -> "'" + StringEscapeUtils.escapeJava("$this") + "'" + } + + private fun Short.toStringConstant() = when (this) { + Short.MAX_VALUE -> "${langPackage}.Short.MAX_VALUE" + Short.MIN_VALUE -> "${langPackage}.Short.MIN_VALUE" + else -> toStringConstantImpl(this) + } + + private fun Int.toStringConstant(): String = toStringConstantImpl(this) + + private fun Long.toStringConstant() = when { + this == Long.MAX_VALUE -> "${langPackage}.Long.MAX_VALUE" + this == Long.MIN_VALUE -> "${langPackage}.Long.MIN_VALUE" + else -> toStringConstantImpl(this) + } + + private fun Float.toStringConstant() = when { + isNaN() -> "${langPackage}.Float.NaN" + this == Float.POSITIVE_INFINITY -> "${langPackage}.Float.POSITIVE_INFINITY" + this == Float.NEGATIVE_INFINITY -> "${langPackage}.Float.NEGATIVE_INFINITY" + else -> toStringConstantImpl(this) + } + + private fun Double.toStringConstant() = when { + isNaN() -> "${langPackage}.Double.NaN" + this == Double.POSITIVE_INFINITY -> "${langPackage}.Double.POSITIVE_INFINITY" + this == Double.NEGATIVE_INFINITY -> "${langPackage}.Double.NEGATIVE_INFINITY" + else -> "$this" + } + + private fun Boolean.toStringConstant() = + if (this) "true" else "false" + + protected fun String.toStringConstant(): String = "\"" + escapeCharacters() + "\"" + + protected abstract fun String.escapeCharacters(): String + + private fun ClassId.isAccessibleBySimpleName(): Boolean = isAccessibleBySimpleNameImpl(this) + + companion object { + const val TEST_METHOD_START_MARKER = "test method start marker" + const val TEST_METHOD_END_MARKER = "test method end marker" + + fun makeRenderer( + context: CgContext, + printer: CgPrinter = CgPrinterImpl() + ): CgAbstractRenderer { + val rendererContext = CgRendererContext.fromCgContext(context) + return makeRenderer(rendererContext, printer) + } + + fun makeRenderer( + utilClassKind: UtilClassKind, + codegenLanguage: CodegenLanguage, + printer: CgPrinter = CgPrinterImpl() + ): CgAbstractRenderer { + val rendererContext = CgRendererContext.fromUtilClassKind(utilClassKind, codegenLanguage) + return makeRenderer(rendererContext, printer) + } + + private fun makeRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer { + return context.cgLanguageAssistant.cgRenderer(context, printer) + } + + /** + * @see [LONG_CODE_FRAGMENTS] + */ + private const val LARGE_CODE_BLOCK_SIZE: Int = 1000 + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgJavaRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgJavaRenderer.kt new file mode 100644 index 0000000000..e607ccd0cc --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgJavaRenderer.kt @@ -0,0 +1,454 @@ +package org.utbot.framework.codegen.renderer + +import org.apache.commons.text.StringEscapeUtils +import org.utbot.framework.codegen.domain.RegularImport +import org.utbot.framework.codegen.domain.StaticImport +import org.utbot.framework.codegen.domain.models.CgAllocateArray +import org.utbot.framework.codegen.domain.models.CgAllocateInitializedArray +import org.utbot.framework.codegen.domain.models.CgAnonymousFunction +import org.utbot.framework.codegen.domain.models.CgArrayAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgArrayInitializer +import org.utbot.framework.codegen.domain.models.CgBreakStatement +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgEqualTo +import org.utbot.framework.codegen.domain.models.CgErrorTestMethod +import org.utbot.framework.codegen.domain.models.CgErrorWrapper +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgForLoop +import org.utbot.framework.codegen.domain.models.CgGetJavaClass +import org.utbot.framework.codegen.domain.models.CgGetKotlinClass +import org.utbot.framework.codegen.domain.models.CgGetLength +import org.utbot.framework.codegen.domain.models.CgInnerBlock +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgNotNullAssertion +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.domain.models.CgReturnStatement +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgStatementExecutableCall +import org.utbot.framework.codegen.domain.models.CgSwitchCase +import org.utbot.framework.codegen.domain.models.CgSwitchCaseLabel +import org.utbot.framework.codegen.domain.models.CgClass +import org.utbot.framework.codegen.domain.models.CgClassBody +import org.utbot.framework.codegen.domain.models.CgFormattedString +import org.utbot.framework.codegen.domain.models.CgFrameworkUtilMethod +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTypeCast +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.tree.VisibilityModifier +import org.utbot.framework.codegen.util.nullLiteral +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.TypeParameters +import org.utbot.framework.plugin.api.util.isFinal +import org.utbot.framework.plugin.api.util.wrapperByPrimitive + +internal class CgJavaRenderer(context: CgRendererContext, printer: CgPrinter = CgPrinterImpl()) : + CgAbstractRenderer(context, printer) { + + override val statementEnding: String = ";" + + override val logicalAnd: String + get() = "&&" + + override val logicalOr: String + get() = "||" + + override val langPackage: String = "java.lang" + + override val ClassId.methodsAreAccessibleAsTopLevel: Boolean + get() = this == context.generatedClass + + override fun visit(element: CgClass) { + element.documentation?.accept(this) + + for (annotation in element.annotations) { + annotation.accept(this) + } + + renderVisibility(element.visibility) + renderClassModality(element) + if (element.isStatic) { + print("static ") + } + print("class ") + print(element.simpleName) + + val superclass = element.superclass + if (superclass != null) { + print(" extends ${superclass.asString()}") + } + if (element.interfaces.isNotEmpty()) { + print(" implements ") + element.interfaces.map { it.asString() }.printSeparated() + } + println(" {") + withIndent { element.body.accept(this) } + println("}") + } + + override fun visit(element: CgClassBody) { + // render class fields + for (field in element.fields) { + field.accept(this) + println() + } + + // render regions for test methods and utils + val allRegions = element.methodRegions + element.nestedClassRegions + element.staticDeclarationRegions + for ((i, region) in allRegions.withIndex()) { + if (i != 0) println() + + region.accept(this) + } + } + + override fun visit(element: CgArrayAnnotationArgument) { + print("{") + element.values.renderSeparated() + print("}") + } + + override fun visit(element: CgAnonymousFunction) { + print("(") + element.parameters.renderSeparated() + print(") -> ") + // TODO introduce CgBlock + + val expression = element.body.singleExpressionOrNull() + // expression lambda can be rendered without curly braces + if (expression != null) { + expression.accept(this) + + return + } + + visit(element.body) + } + + override fun visit(element: CgEqualTo) { + element.left.accept(this) + print(" == ") + element.right.accept(this) + } + + override fun visit(element: CgTypeCast) { + 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 (exprTypeIsSimilar && expr != nullLiteral()) { + element.expression.accept(this) + return + } + + val isNegativeNumber = element.expression is CgLiteral + && element.expression.value is Number && element.expression.value.toDouble() < 0 + + print("(") + + print("(") + print(wrappedTargetType.asString()) + print(") ") + + if (isNegativeNumber) print("(") + element.expression.accept(this) + if (isNegativeNumber) print(")") + + print(")") + } + + override fun visit(element: CgErrorWrapper) { + element.expression.accept(this) + } + + // Not-null assertion + + override fun visit(element: CgNotNullAssertion) { + element.expression.accept(this) + } + + override fun visit(element: CgParameterDeclaration) { + if (element.isVararg) { + print(element.type.elementClassId!!.asString()) + print("...") + } else { + print(element.type.asString()) + } + print(" ") + print(element.name.escapeNamePossibleKeyword()) + } + + override fun visit(element: CgGetJavaClass) { + // TODO: check how it works on ref types, primitives, ref arrays, primitive arrays, etc. + print(element.classId.asString()) + print(".class") + } + + override fun visit(element: CgGetKotlinClass) { + // For now we assume that we never need KClass in the generated Java test classes. + // If it changes, this error may be removed. + error("KClass attempted to be used in the Java test class") + } + + override fun visit(element: CgAllocateArray) { + // 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) { + // TODO: same as in visit(CgAllocateArray): we should rewrite the typeName and otherDimensions variables declaration + // to avoid using substringBefore() and substringAfter() directly + val typeName = element.type.canonicalName.substringBefore("[") + val otherDimensions = element.type.canonicalName.substringAfter("]") + // we can't specify the size of the first dimension when using initializer, + // as opposed to CgAllocateArray where there is no initializer + print("new $typeName[]$otherDimensions") + element.initializer.accept(this) + } + + override fun visit(element: CgArrayInitializer) { + val elementsInLine = arrayElementsInLine(element.elementType) + + print("{") + element.values.renderElements(elementsInLine) + print("}") + } + + override fun visit(element: CgGetLength) { + element.variable.accept(this) + print(".length") + } + + override fun visit(element: CgConstructorCall) { + when(element.type.isInner){ + true -> { + // the first argument of inner classes is outer class + print(element.arguments.first().accept(this)) + print(".new ") + print(element.executableId.classId.simpleName) + + print("(") + element.arguments.drop(1).renderSeparated() + print(")") + } + false -> { + print("new ") + print(element.executableId.classId.asString()) + renderExecutableCallArguments(element) + } + } + } + + override fun renderRegularImport(regularImport: RegularImport) { + val escapedImport = getEscapedImportRendering(regularImport) + println("import $escapedImport$statementEnding") + } + + override fun renderStaticImport(staticImport: StaticImport) { + val escapedImport = getEscapedImportRendering(staticImport) + println("import static $escapedImport$statementEnding") + } + + override fun renderMethodSignature(element: CgTestMethod) { + renderVisibility(element.visibility) + // test methods always have void return type + print("void ") + print(element.name) + + print("(") + val newLinesNeeded = element.parameters.size > maxParametersAmountInOneLine + element.parameters.renderSeparated(newLinesNeeded) + print(")") + + renderExceptions(element) + } + + override fun renderMethodSignature(element: CgErrorTestMethod) { + renderVisibility(element.visibility) + // error test methods always have void return type + println("void ${element.name}()") + } + + override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { + //we do not have a good string representation for two-dimensional array, so this strange if-else is required + val returnType = + if (element.returnType.simpleName == "Object[][]") "java.lang.Object[][]" else "${element.returnType}" + + renderVisibility(element.visibility) + print("static $returnType ${element.name}()") + renderExceptions(element) + } + + override fun renderMethodSignature(element: CgFrameworkUtilMethod) { + renderVisibility(element.visibility) + // framework util methods always have void return type + print("void ") + print(element.name) + print("()") + + renderExceptions(element) + } + + override fun visit(element: CgInnerBlock) { + println("{") + withIndent { + for (statement in element.statements) { + statement.accept(this) + } + } + println("}") + } + + override fun renderForLoopVarControl(element: CgForLoop) { + print("for (") + // TODO: rewrite in the future + with(element.initialization) { + print(variableType.asString()) + print(" ") + visit(variable) + initializer?.let { + print(" = ") + it.accept(this@CgJavaRenderer) + } + print("$statementEnding ") + } + element.condition.accept(this) + print("$statementEnding ") + element.update.accept(this) + } + + override fun renderDeclarationLeftPart(element: CgDeclaration) { + print(element.variableType.asString()) + print(" ") + visit(element.variable) + } + + override fun renderAccess(caller: CgExpression) { + print(".") + } + + override fun visit(element: CgSwitchCaseLabel) { + if (element.label != null) { + print("case ") + element.label.accept(this) + } else { + print("default") + } + println(":") + withIndent { + for (statement in element.statements) { + statement.accept(this) + } + + if (element.addBreakStatementToEnd) { + CgBreakStatement.accept(this) + } + } + } + + override fun visit(element: CgSwitchCase) { + print("switch (") + element.value.accept(this) + println(") {") + withIndent { + for (caseLabel in element.labels) { + caseLabel.accept(this) + } + element.defaultLabel?.accept(this) + } + println("}") + } + + override fun visit(element: CgFormattedString) { + val nonLiteralElements = element.array.filterNot { it is CgLiteral } + + print("String.format(") + val constructedMsg = buildString { + element.array.forEachIndexed { index, cgElement -> + if (cgElement is CgLiteral) append( + cgElement.toStringConstant(asRawString = true) + ) else append("%s") + if (index < element.array.lastIndex) append(" ") + } + } + + print(constructedMsg.toStringConstant()) + + // Comma to separate msg from variables + if (nonLiteralElements.isNotEmpty()) print(", ") + nonLiteralElements.renderSeparated(newLines = false) + print(")") + } + + override fun toStringConstantImpl(byte: Byte): String = "(byte) $byte" + + override fun toStringConstantImpl(short: Short): String = "(short) $short" + + override fun toStringConstantImpl(long: Long): String = "${long}L" + + override fun toStringConstantImpl(float: Float): String = "${float}f" + + override fun toStringConstantImpl(int: Int): String = when (int) { + Int.MAX_VALUE -> "Integer.MAX_VALUE" + Int.MIN_VALUE -> "Integer.MIN_VALUE" + else -> "$int" + } + + override fun String.escapeCharacters(): String = StringEscapeUtils.escapeJava(this) + + override fun renderExecutableCallArguments(executableCall: CgExecutableCall) { + print("(") + executableCall.arguments.renderSeparated() + print(")") + } + + override fun renderTypeParameters(typeParameters: TypeParameters) {} + + override fun renderExceptionCatchVariable(exception: CgVariable) { + print("${exception.type} ${exception.name.escapeNamePossibleKeyword()}") + } + + override fun isAccessibleBySimpleNameImpl(classId: ClassId): Boolean = + super.isAccessibleBySimpleNameImpl(classId) || classId.packageName == "java.lang" + + override fun escapeNamePossibleKeywordImpl(s: String): String = s + + override fun renderVisibility(modifier: VisibilityModifier) { + when (modifier) { + VisibilityModifier.PUBLIC -> print("public ") + VisibilityModifier.PRIVATE -> print("private ") + VisibilityModifier.PROTECTED -> print("protected ") + VisibilityModifier.PACKAGE_PRIVATE -> Unit + VisibilityModifier.INTERNAL -> error("Java: unexpected visibility modifier -- $modifier") + } + } + + override fun renderClassModality(aClass: CgClass) { + if (aClass.id.isFinal) print("final ") + } + + private fun renderExceptions(method: CgMethod) { + method.exceptions.takeIf { it.isNotEmpty() }?.let { exceptions -> + print(" throws ") + print(exceptions.joinToString(separator = ", ", postfix = " ") { it.asString() }) + } + } + + /** + * Returns containing [CgExpression] if [this] represents return statement or one executable call, and null otherwise. + */ + private fun List.singleExpressionOrNull(): CgExpression? = + singleOrNull().let { + when (it) { + is CgReturnStatement -> it.expression + is CgStatementExecutableCall -> it.call + else -> null + } + } +} \ No newline at end of file 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/renderer/CgKotlinRenderer.kt similarity index 77% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgKotlinRenderer.kt index 09459b5f8b..8a92d09617 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/renderer/CgKotlinRenderer.kt @@ -1,58 +1,59 @@ -package org.utbot.framework.codegen.model.visitor +package org.utbot.framework.codegen.renderer import org.apache.commons.text.StringEscapeUtils import org.utbot.common.WorkaroundReason import org.utbot.common.workaround -import org.utbot.framework.codegen.RegularImport -import org.utbot.framework.codegen.StaticImport -import org.utbot.framework.codegen.isLanguageKeyword -import org.utbot.framework.codegen.model.tree.AbstractCgClass -import org.utbot.framework.codegen.model.tree.CgAllocateArray -import org.utbot.framework.codegen.model.tree.CgAllocateInitializedArray -import org.utbot.framework.codegen.model.tree.CgAnonymousFunction -import org.utbot.framework.codegen.model.tree.CgArrayAnnotationArgument -import org.utbot.framework.codegen.model.tree.CgArrayElementAccess -import org.utbot.framework.codegen.model.tree.CgArrayInitializer -import org.utbot.framework.codegen.model.tree.CgAuxiliaryClass -import org.utbot.framework.codegen.model.tree.CgComparison -import org.utbot.framework.codegen.model.tree.CgConstructorCall -import org.utbot.framework.codegen.model.tree.CgDeclaration -import org.utbot.framework.codegen.model.tree.CgEqualTo -import org.utbot.framework.codegen.model.tree.CgErrorTestMethod -import org.utbot.framework.codegen.model.tree.CgErrorWrapper -import org.utbot.framework.codegen.model.tree.CgExecutableCall -import org.utbot.framework.codegen.model.tree.CgExpression -import org.utbot.framework.codegen.model.tree.CgFieldAccess -import org.utbot.framework.codegen.model.tree.CgForLoop -import org.utbot.framework.codegen.model.tree.CgGetJavaClass -import org.utbot.framework.codegen.model.tree.CgGetKotlinClass -import org.utbot.framework.codegen.model.tree.CgGetLength -import org.utbot.framework.codegen.model.tree.CgInnerBlock -import org.utbot.framework.codegen.model.tree.CgMethod -import org.utbot.framework.codegen.model.tree.CgNotNullAssertion -import org.utbot.framework.codegen.model.tree.CgParameterDeclaration -import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod -import org.utbot.framework.codegen.model.tree.CgRegularClass -import org.utbot.framework.codegen.model.tree.CgSimpleRegion -import org.utbot.framework.codegen.model.tree.CgSpread -import org.utbot.framework.codegen.model.tree.CgStaticsRegion -import org.utbot.framework.codegen.model.tree.CgSwitchCase -import org.utbot.framework.codegen.model.tree.CgSwitchCaseLabel -import org.utbot.framework.codegen.model.tree.CgTestClass -import org.utbot.framework.codegen.model.tree.CgTestClassBody -import org.utbot.framework.codegen.model.tree.CgTestMethod -import org.utbot.framework.codegen.model.tree.CgTypeCast -import org.utbot.framework.codegen.model.tree.CgVariable -import org.utbot.framework.codegen.model.util.CgPrinter -import org.utbot.framework.codegen.model.util.CgPrinterImpl -import org.utbot.framework.codegen.model.util.nullLiteral +import org.utbot.framework.codegen.domain.RegularImport +import org.utbot.framework.codegen.domain.StaticImport +import org.utbot.framework.codegen.services.language.isLanguageKeyword +import org.utbot.framework.codegen.domain.models.CgAllocateArray +import org.utbot.framework.codegen.domain.models.CgAllocateInitializedArray +import org.utbot.framework.codegen.domain.models.CgAnonymousFunction +import org.utbot.framework.codegen.domain.models.CgArrayAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgArrayElementAccess +import org.utbot.framework.codegen.domain.models.CgArrayInitializer +import org.utbot.framework.codegen.domain.models.CgAuxiliaryClass +import org.utbot.framework.codegen.domain.models.CgComparison +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgEqualTo +import org.utbot.framework.codegen.domain.models.CgErrorTestMethod +import org.utbot.framework.codegen.domain.models.CgErrorWrapper +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgFieldAccess +import org.utbot.framework.codegen.domain.models.CgForLoop +import org.utbot.framework.codegen.domain.models.CgGetJavaClass +import org.utbot.framework.codegen.domain.models.CgGetKotlinClass +import org.utbot.framework.codegen.domain.models.CgGetLength +import org.utbot.framework.codegen.domain.models.CgInnerBlock +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgNotNullAssertion +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.domain.models.CgSimpleRegion +import org.utbot.framework.codegen.domain.models.CgSpread +import org.utbot.framework.codegen.domain.models.CgStaticsRegion +import org.utbot.framework.codegen.domain.models.CgSwitchCase +import org.utbot.framework.codegen.domain.models.CgSwitchCaseLabel +import org.utbot.framework.codegen.domain.models.CgClass +import org.utbot.framework.codegen.domain.models.CgClassBody +import org.utbot.framework.codegen.domain.models.CgFormattedString +import org.utbot.framework.codegen.domain.models.CgFrameworkUtilMethod +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTypeCast +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.tree.VisibilityModifier +import org.utbot.framework.codegen.util.nullLiteral 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.WildcardTypeParameter +import org.utbot.framework.plugin.api.util.isFinal import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.isArray +import org.utbot.framework.plugin.api.util.isKotlinFile import org.utbot.framework.plugin.api.util.isPrimitive import org.utbot.framework.plugin.api.util.isPrimitiveWrapper import org.utbot.framework.plugin.api.util.kClass @@ -68,21 +69,28 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = override val logicalOr: String get() = "or" - override val language: CodegenLanguage = CodegenLanguage.KOTLIN - override val langPackage: String = "kotlin" - override fun visit(element: AbstractCgClass<*>) { + override val ClassId.methodsAreAccessibleAsTopLevel: Boolean + // NB: the order of operands is important as `isKotlinFile` uses reflection and thus can't be called on context.generatedClass + get() = (this == context.generatedClass) || isKotlinFile + + override fun visit(element: CgClass) { + element.documentation?.accept(this) + for (annotation in element.annotations) { annotation.accept(this) } - renderClassVisibility(element.id) + renderVisibility(element.visibility) renderClassModality(element) if (!element.isStatic && element.isNested) { print("inner ") } - print("class ") + if (element.id.isKotlinObject) + print("object ") + else + print("class ") print(element.simpleName) if (element.superclass != null || element.interfaces.isNotEmpty()) { @@ -109,9 +117,9 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = println("}") } - override fun visit(element: CgTestClassBody) { + override fun visit(element: CgClassBody) { // render regions for test methods - for ((i, region) in (element.testMethodRegions + element.nestedClassRegions).withIndex()) { + for ((i, region) in (element.methodRegions + element.nestedClassRegions).withIndex()) { if (i != 0) println() region.accept(this) @@ -146,13 +154,20 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = CgStaticsRegion(region.header, updatedContent) } - renderCompanionObject { + fun renderAllStaticRegions() { for ((i, staticsRegion) in updatedStaticRegions.withIndex()) { if (i != 0) println() staticsRegion.accept(this) } } + + // We should generate static methods in companion object iff generated class is not an object + if (element.classId.isKotlinObject) { + renderAllStaticRegions() + } else { + renderCompanionObject(::renderAllStaticRegions) + } } /** @@ -165,23 +180,6 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = println("}") } - override fun visit(element: CgStaticsRegion) { - if (element.content.isEmpty()) return - - print(regionStart) - element.header?.let { print(" $it") } - println() - - for (item in element.content) { - println() - println("@JvmStatic") - item.accept(this) - } - - println(regionEnd) - } - - // Property access override fun visit(element: CgFieldAccess) { @@ -365,6 +363,12 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = print(")") } + override fun visit(element: CgParameterizedTestDataProviderMethod) { + println() + println("@JvmStatic") + super.visit(element) + } + override fun renderRegularImport(regularImport: RegularImport) { val escapedImport = getEscapedImportRendering(regularImport) println("import $escapedImport$statementEnding") @@ -398,6 +402,14 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = println("fun ${element.name}(): $returnType") } + override fun renderMethodSignature(element: CgFrameworkUtilMethod) { + print("fun ") + // TODO: resolve $ in name as in [CgTestMethod] + print(element.name) + print("()") + renderMethodReturnType(element) + } + private fun renderMethodReturnType(method: CgMethod) { if (method.returnType != voidClassId) { print(": ") @@ -466,6 +478,29 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = println("}") } + override fun visit(element: CgFormattedString) { + print("\"") + element.array.forEachIndexed { index, cgElement -> + if (cgElement is CgLiteral) + print(cgElement.toStringConstant(asRawString = true)) + else { + print("$") + when (cgElement) { + // It is not necessary to wrap variables with curly brackets + is CgVariable -> cgElement.accept(this) + else -> { + print("{") + cgElement.accept(this) + print("}") + } + } + } + + if (index < element.array.lastIndex) print(" ") + } + print("\"") + } + override fun toStringConstantImpl(byte: Byte): String = buildString { if (byte < 0) { append("(") @@ -529,23 +564,20 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = } override fun escapeNamePossibleKeywordImpl(s: String): String = - if (isLanguageKeyword(s, context.codegenLanguage)) "`$s`" else s + if (isLanguageKeyword(s, context.cgLanguageAssistant)) "`$s`" else s - override fun renderClassVisibility(classId: ClassId) { - when { - // Kotlin classes are public by default - classId.isPublic -> Unit - classId.isProtected -> print("protected ") - classId.isPrivate -> print("private ") + override fun renderVisibility(modifier: VisibilityModifier) { + when (modifier) { + VisibilityModifier.PUBLIC -> Unit + VisibilityModifier.PRIVATE -> print("private ") + VisibilityModifier.PROTECTED -> print("protected ") + VisibilityModifier.INTERNAL -> print("internal ") + VisibilityModifier.PACKAGE_PRIVATE -> error("Kotlin: unexpected visibility modifier -- $modifier") } } - override fun renderClassModality(aClass: AbstractCgClass<*>) { - when (aClass) { - is CgTestClass -> Unit - // Kotlin classes are final by default - is CgRegularClass -> if (!aClass.id.isFinal) print("open ") - } + override fun renderClassModality(aClass: CgClass) { + if (!aClass.id.isFinal) print("open ") } private fun getKotlinClassString(id: ClassId): String = @@ -609,4 +641,4 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = print(">") } } -} \ No newline at end of file +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/CgPrinter.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgPrinter.kt similarity index 84% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/CgPrinter.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgPrinter.kt index 0e4713f9cc..62daa3b6b4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/CgPrinter.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgPrinter.kt @@ -1,4 +1,6 @@ -package org.utbot.framework.codegen.model.util +package org.utbot.framework.codegen.renderer + +import org.utbot.framework.plugin.api.util.IndentUtil interface CgPrinter { fun print(text: String) @@ -8,6 +10,7 @@ interface CgPrinter { fun popIndent() override fun toString(): String + var printedLength: Int } class CgPrinterImpl( @@ -30,6 +33,8 @@ class CgPrinterImpl( override fun toString(): String = builder.toString() + override var printedLength: Int = builder.length + override fun print(text: String) { if (atLineStart) { printIndent() @@ -48,10 +53,6 @@ class CgPrinterImpl( atLineStart = true } - fun printLine() { - appendLine() - } - private fun printIndent() { append(indent) } @@ -59,6 +60,6 @@ class CgPrinterImpl( private operator fun String.times(n: Int): String = repeat(n) companion object { - private const val TAB = " " + private const val TAB = IndentUtil.TAB } } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgRendererContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgRendererContext.kt new file mode 100644 index 0000000000..c3276b9b22 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgRendererContext.kt @@ -0,0 +1,62 @@ +package org.utbot.framework.codegen.renderer + +import org.utbot.framework.codegen.domain.builtin.selectUtilClassId +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.tree.ututils.UtilClassKind +import org.utbot.framework.codegen.tree.ututils.UtilClassKind.Companion.utilsPackageFullName +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.codegen.services.language.CgLanguageAssistant +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.utils.UtilMethodProvider + +/** + * Information from [CgContext] that is relevant for the renderer. + * Not all the information from [CgContext] is required to render a class, + * so this more lightweight context is created for this purpose. + */ +class CgRendererContext( + val shouldOptimizeImports: Boolean, + val importedClasses: Set, + val importedStaticMethods: Set, + val classPackageName: String, + val generatedClass: ClassId, + val utilMethodProvider: UtilMethodProvider, + val codegenLanguage: CodegenLanguage, + val mockFrameworkUsed: Boolean, + val mockFramework: MockFramework, + val cgLanguageAssistant: CgLanguageAssistant, +) { + companion object { + fun fromCgContext(context: CgContext): CgRendererContext { + return CgRendererContext( + shouldOptimizeImports = context.shouldOptimizeImports, + importedClasses = context.importedClasses, + importedStaticMethods = context.importedStaticMethods, + classPackageName = context.testClassPackageName, + generatedClass = context.outerMostTestClass, + utilMethodProvider = context.utilMethodProvider, + codegenLanguage = context.codegenLanguage, + cgLanguageAssistant = context.cgLanguageAssistant, + mockFrameworkUsed = context.mockFrameworkUsed, + mockFramework = context.mockFramework + ) + } + + fun fromUtilClassKind(utilClassKind: UtilClassKind, language: CodegenLanguage): CgRendererContext { + return CgRendererContext( + shouldOptimizeImports = false, + importedClasses = emptySet(), + importedStaticMethods = emptySet(), + classPackageName = utilsPackageFullName(language), + generatedClass = selectUtilClassId(language), + utilMethodProvider = utilClassKind.utilMethodProvider, + codegenLanguage = language, + mockFrameworkUsed = utilClassKind.mockFrameworkUsed, + mockFramework = utilClassKind.mockFramework, + cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(language), + ) + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgVisitor.kt new file mode 100644 index 0000000000..fe75905f06 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgVisitor.kt @@ -0,0 +1,275 @@ +package org.utbot.framework.codegen.renderer + +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.domain.models.CgAbstractFieldAccess +import org.utbot.framework.codegen.domain.models.CgAbstractMultilineComment +import org.utbot.framework.codegen.domain.models.CgAllocateArray +import org.utbot.framework.codegen.domain.models.CgAllocateInitializedArray +import org.utbot.framework.codegen.domain.models.CgAnonymousFunction +import org.utbot.framework.codegen.domain.models.CgArrayAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgArrayElementAccess +import org.utbot.framework.codegen.domain.models.CgArrayInitializer +import org.utbot.framework.codegen.domain.models.CgAssignment +import org.utbot.framework.codegen.domain.models.CgAuxiliaryClass +import org.utbot.framework.codegen.domain.models.CgBreakStatement +import org.utbot.framework.codegen.domain.models.CgComment +import org.utbot.framework.codegen.domain.models.CgCommentedAnnotation +import org.utbot.framework.codegen.domain.models.CgComparison +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgContinueStatement +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgDecrement +import org.utbot.framework.codegen.domain.models.CgDoWhileLoop +import org.utbot.framework.codegen.domain.models.CgDocClassLinkStmt +import org.utbot.framework.codegen.domain.models.CgDocCodeStmt +import org.utbot.framework.codegen.domain.models.CgDocMethodLinkStmt +import org.utbot.framework.codegen.domain.models.CgDocPreTagStatement +import org.utbot.framework.codegen.domain.models.CgCustomTagStatement +import org.utbot.framework.codegen.domain.models.CgDocRegularStmt +import org.utbot.framework.codegen.domain.models.CgDocumentationComment +import org.utbot.framework.codegen.domain.models.CgElement +import org.utbot.framework.codegen.domain.models.CgEmptyLine +import org.utbot.framework.codegen.domain.models.CgEnumConstantAccess +import org.utbot.framework.codegen.domain.models.CgEqualTo +import org.utbot.framework.codegen.domain.models.CgErrorTestMethod +import org.utbot.framework.codegen.domain.models.CgErrorWrapper +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgMethodsCluster +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgFieldAccess +import org.utbot.framework.codegen.domain.models.CgForEachLoop +import org.utbot.framework.codegen.domain.models.CgForLoop +import org.utbot.framework.codegen.domain.models.CgGetJavaClass +import org.utbot.framework.codegen.domain.models.CgGetKotlinClass +import org.utbot.framework.codegen.domain.models.CgGetLength +import org.utbot.framework.codegen.domain.models.CgGreaterThan +import org.utbot.framework.codegen.domain.models.CgIfStatement +import org.utbot.framework.codegen.domain.models.CgIncrement +import org.utbot.framework.codegen.domain.models.CgInnerBlock +import org.utbot.framework.codegen.domain.models.CgIsInstance +import org.utbot.framework.codegen.domain.models.CgLessThan +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgLogicalAnd +import org.utbot.framework.codegen.domain.models.CgLogicalOr +import org.utbot.framework.codegen.domain.models.CgLoop +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgMultilineComment +import org.utbot.framework.codegen.domain.models.CgMultipleArgsAnnotation +import org.utbot.framework.codegen.domain.models.CgNamedAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgNonStaticRunnable +import org.utbot.framework.codegen.domain.models.CgNotNullAssertion +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.domain.models.CgReturnStatement +import org.utbot.framework.codegen.domain.models.CgSimpleRegion +import org.utbot.framework.codegen.domain.models.CgSingleArgAnnotation +import org.utbot.framework.codegen.domain.models.CgSingleLineComment +import org.utbot.framework.codegen.domain.models.CgSpread +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgStatementExecutableCall +import org.utbot.framework.codegen.domain.models.CgStaticFieldAccess +import org.utbot.framework.codegen.domain.models.CgStaticRunnable +import org.utbot.framework.codegen.domain.models.CgStaticsRegion +import org.utbot.framework.codegen.domain.models.CgSwitchCase +import org.utbot.framework.codegen.domain.models.CgSwitchCaseLabel +import org.utbot.framework.codegen.domain.models.CgClass +import org.utbot.framework.codegen.domain.models.CgClassBody +import org.utbot.framework.codegen.domain.models.CgDocRegularLineStmt +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgFormattedString +import org.utbot.framework.codegen.domain.models.CgFrameworkUtilMethod +import org.utbot.framework.codegen.domain.models.CgNestedClassesRegion +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTestMethodCluster +import org.utbot.framework.codegen.domain.models.CgThisInstance +import org.utbot.framework.codegen.domain.models.CgThrowStatement +import org.utbot.framework.codegen.domain.models.CgTripleSlashMultilineComment +import org.utbot.framework.codegen.domain.models.CgTryCatch +import org.utbot.framework.codegen.domain.models.CgTypeCast +import org.utbot.framework.codegen.domain.models.CgUtilMethod +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.domain.models.CgWhileLoop + +interface CgVisitor { + fun visit(element: CgElement): R + + fun visit(element: CgClassFile): R + + fun visit(element: CgClass): R + + fun visit(element: CgClassBody): R + + fun visit(element: CgStaticsRegion): R + + fun visit(element: CgNestedClassesRegion<*>): R + fun visit(element: CgSimpleRegion<*>): R + fun visit(element: CgTestMethodCluster): R + fun visit(element: CgMethodsCluster): R + + fun visit(element: CgAuxiliaryClass): R + fun visit(element: CgUtilMethod): R + + // Methods + fun visit(element: CgMethod): R + fun visit(element: CgTestMethod): R + fun visit(element: CgErrorTestMethod): R + fun visit(element: CgParameterizedTestDataProviderMethod): R + fun visit(element: CgFrameworkUtilMethod): R + + // Annotations + fun visit(element: CgCommentedAnnotation): R + fun visit(element: CgSingleArgAnnotation): R + fun visit(element: CgMultipleArgsAnnotation): R + fun visit(element: CgNamedAnnotationArgument): R + fun visit(element: CgArrayAnnotationArgument): R + + // Comments + fun visit(element: CgComment): R + fun visit(element: CgSingleLineComment): R + fun visit(element: CgAbstractMultilineComment): R + fun visit(element: CgTripleSlashMultilineComment): R + fun visit(element: CgMultilineComment): R + fun visit(element: CgDocumentationComment): R + + // Comment statements + fun visit(element: CgDocPreTagStatement): R + fun visit(element: CgCustomTagStatement): R + fun visit(element: CgDocCodeStmt): R + fun visit(element: CgDocRegularStmt): R + fun visit(element: CgDocRegularLineStmt): R + fun visit(element: CgDocClassLinkStmt): R + fun visit(element: CgDocMethodLinkStmt): R + + // Any block + // IMPORTANT: there is no line separator in the end by default + // because some blocks have it (like loops) but some do not (like try .. catch, if .. else etc) + fun visit(block: List, printNextLine: Boolean = false): R + + // Anonymous function (lambda) + fun visit(element: CgAnonymousFunction): R + + // Return statement + fun visit(element: CgReturnStatement): R + + // Array element access + fun visit(element: CgArrayElementAccess): R + + // Loop conditions + fun visit(element: CgComparison): R + fun visit(element: CgLessThan): R + fun visit(element: CgGreaterThan): R + fun visit(element: CgEqualTo): R + + // Increment and decrement + fun visit(element: CgIncrement): R + fun visit(element: CgDecrement): R + + fun visit(element: CgErrorWrapper): R + + // Try-catch + fun visit(element: CgTryCatch): R + + //Simple block + fun visit(element: CgInnerBlock): R + + // Loops + fun visit(element: CgLoop): R + fun visit(element: CgForLoop): R + fun visit(element: CgWhileLoop): R + fun visit(element: CgDoWhileLoop): R + + // Control statements + fun visit(element: CgBreakStatement): R + fun visit(element: CgContinueStatement): R + + // Variable declaration + fun visit(element: CgDeclaration): R + + // Field declaration + fun visit(element: CgFieldDeclaration): R + + // Variable assignment + fun visit(element: CgAssignment): R + + // Expressions + fun visit(element: CgExpression): R + + // Type cast + fun visit(element: CgTypeCast): R + + // isInstance check + fun visit(element: CgIsInstance): R + + // This instance + fun visit(element: CgThisInstance): R + + // Variables + fun visit(element: CgVariable): R + + // Not-null assertion + + fun visit(element: CgNotNullAssertion): R + + // Method parameters + fun visit(element: CgParameterDeclaration): R + + // Primitive and String literals + fun visit(element: CgLiteral): R + + // Non-static runnable like this::toString or (new Object())::toString etc + fun visit(element: CgNonStaticRunnable): R + // Static runnable like Random::nextRandomInt etc + fun visit(element: CgStaticRunnable): R + + // Array allocation + fun visit(element: CgAllocateArray): R + fun visit(element: CgAllocateInitializedArray): R + fun visit(element: CgArrayInitializer): R + + // Spread operator + fun visit(element: CgSpread): R + + // Formatted string + fun visit(element: CgFormattedString): R + + // Enum constant + fun visit(element: CgEnumConstantAccess): R + + // Property access + fun visit(element: CgAbstractFieldAccess): R + fun visit(element: CgFieldAccess): R + fun visit(element: CgStaticFieldAccess): R + + // Conditional statement + + fun visit(element: CgIfStatement): R + fun visit(element: CgSwitchCaseLabel): R + fun visit(element: CgSwitchCase): R + + // Binary logical operators + + fun visit(element: CgLogicalAnd): R + fun visit(element: CgLogicalOr): R + + // Acquisition of array length, e.g. args.length + fun visit(element: CgGetLength): R + + // Acquisition of java or kotlin class, e.g. MyClass.class in Java, MyClass::class.java in Kotlin or MyClass::class for Kotlin classes + fun visit(element: CgGetJavaClass): R + fun visit(element: CgGetKotlinClass): R + + // Executable calls + fun visit(element: CgStatementExecutableCall): R + fun visit(element: CgExecutableCall): R + fun visit(element: CgConstructorCall): R + fun visit(element: CgMethodCall): R + + // Throw statement + fun visit(element: CgThrowStatement): R + + // Empty line + fun visit(element: CgEmptyLine): R + + fun visit(element: CgForEachLoop): R +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/UtilMethodRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/UtilMethodRenderer.kt new file mode 100644 index 0000000000..8052b907f1 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/UtilMethodRenderer.kt @@ -0,0 +1,1506 @@ +package org.utbot.framework.codegen.renderer + +import org.utbot.framework.codegen.domain.StaticImport +import org.utbot.framework.codegen.domain.builtin.TestClassUtilMethodProvider +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.tree.importIfNeeded +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.util.fieldClassId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.utils.UtilMethodProvider +import java.lang.invoke.CallSite +import java.lang.invoke.LambdaMetafactory +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.Arrays +import java.util.Objects +import java.util.stream.Collectors + +private enum class Visibility(val text: String) { + PRIVATE("private"), + @Suppress("unused") + PROTECTED("protected"), + PUBLIC("public"); + + infix fun by(language: CodegenLanguage): String { + if (this == PUBLIC && language == CodegenLanguage.KOTLIN) { + // public is default in Kotlin + return "" + } + return "$text " + } +} + +// TODO: This method may throw an exception that will crash rendering. +// TODO: Add isolation on rendering: https://github.com/UnitTestBot/UTBotJava/issues/853 +internal fun UtilMethodProvider.utilMethodTextById( + id: MethodId, + mockFrameworkUsed: Boolean, + mockFramework: MockFramework, + codegenLanguage: CodegenLanguage +): String { + // If util methods are declared in the test class, then they are private. Otherwise, they are public. + val visibility = if (this is TestClassUtilMethodProvider) Visibility.PRIVATE else Visibility.PUBLIC + return with(this) { + when (id) { + getUnsafeInstanceMethodId -> getUnsafeInstance(visibility, codegenLanguage) + createInstanceMethodId -> createInstance(visibility, codegenLanguage) + createArrayMethodId -> createArray(visibility, codegenLanguage) + setFieldMethodId -> setField(visibility, codegenLanguage) + setStaticFieldMethodId -> setStaticField(visibility, codegenLanguage) + getFieldValueMethodId -> getFieldValue(visibility, codegenLanguage) + getStaticFieldValueMethodId -> getStaticFieldValue(visibility, codegenLanguage) + getEnumConstantByNameMethodId -> getEnumConstantByName(visibility, codegenLanguage) + deepEqualsMethodId -> deepEquals(visibility, codegenLanguage, mockFrameworkUsed, mockFramework) + arraysDeepEqualsMethodId -> arraysDeepEquals(visibility, codegenLanguage) + iterablesDeepEqualsMethodId -> iterablesDeepEquals(visibility, codegenLanguage) + streamsDeepEqualsMethodId -> streamsDeepEquals(visibility, codegenLanguage) + mapsDeepEqualsMethodId -> mapsDeepEquals(visibility, codegenLanguage) + hasCustomEqualsMethodId -> hasCustomEquals(visibility, codegenLanguage) + getArrayLengthMethodId -> getArrayLength(visibility, codegenLanguage) + consumeBaseStreamMethodId -> consumeBaseStream(visibility, codegenLanguage) + buildStaticLambdaMethodId -> buildStaticLambda(visibility, codegenLanguage) + buildLambdaMethodId -> buildLambda(visibility, codegenLanguage) + // the following methods are used only by other util methods, so they can always be private + getLookupInMethodId -> getLookupIn(codegenLanguage) + getLambdaCapturedArgumentTypesMethodId -> getLambdaCapturedArgumentTypes(codegenLanguage) + getLambdaCapturedArgumentValuesMethodId -> getLambdaCapturedArgumentValues(codegenLanguage) + getInstantiatedMethodTypeMethodId -> getInstantiatedMethodType(codegenLanguage) + getLambdaMethodMethodId -> getLambdaMethod(codegenLanguage) + getSingleAbstractMethodMethodId -> getSingleAbstractMethod(codegenLanguage) + else -> error("Unknown util method for class $this: $id") + } + } +} + +// TODO: This method may throw an exception that will crash rendering. +// TODO: Add isolation on rendering: https://github.com/UnitTestBot/UTBotJava/issues/853 +internal fun UtilMethodProvider.auxiliaryClassTextById(id: ClassId, codegenLanguage: CodegenLanguage): String = + with(this) { + when (id) { + capturedArgumentClassId -> capturedArgumentClass(codegenLanguage) + else -> error("Unknown auxiliary class: $id") + } + } + +private fun getEnumConstantByName(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static Object getEnumConstantByName(Class enumClass, String name) throws IllegalAccessException { + java.lang.reflect.Field[] fields = enumClass.getDeclaredFields(); + for (java.lang.reflect.Field field : fields) { + String fieldName = field.getName(); + if (field.isEnumConstant() && fieldName.equals(name)) { + field.setAccessible(true); + + return field.get(null); + } + } + + return null; + } + """ + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}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) { + field.isAccessible = true + + return field.get(null) + } + } + + return null + } + """ + } + }.trimIndent() + +private fun getFieldRetrievingBlock(language : CodegenLanguage, fullClassName : String, fieldName : String, resultName : String): String { + val methodName = "methodForGetDeclaredFields${System.nanoTime()}" + val fieldsName = "allFieldsFromFieldClass${System.nanoTime()}" + return when (language) { + CodegenLanguage.JAVA -> + """ + java.lang.reflect.Method $methodName = java.lang.Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class); + $methodName.setAccessible(true); + java.lang.reflect.Field[] $fieldsName = (java.lang.reflect.Field[]) $methodName.invoke($fullClassName.class, false); + $resultName = java.util.Arrays.stream($fieldsName).filter(field1 -> field1.getName().equals("$fieldName")).findFirst().get(); + """ + + CodegenLanguage.KOTLIN -> + """ + val $methodName = Class::class.java.getDeclaredMethod("getDeclaredFields0", Boolean::class.java) + $methodName.isAccessible = true + val $fieldsName = $methodName.invoke($fullClassName::class.java, false) as kotlin.Array + $resultName = $fieldsName.filter { field1: java.lang.reflect.Field -> field1.name == "$fieldName" }.first() + """ + } +} +private fun getStaticFieldValue(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static Object getStaticFieldValue(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { + java.lang.reflect.Field field; + Class originClass = clazz; + do { + try { + field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + + java.lang.reflect.Field modifiersField; + ${getFieldRetrievingBlock(language, "java.lang.reflect.Field", "modifiers", "modifiersField")} + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); + + return field.get(null); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } catch (NoSuchMethodException e2) { + e2.printStackTrace(); + } catch (java.lang.reflect.InvocationTargetException e3) { + e3.printStackTrace(); + } + } while (clazz != null); + + throw new NoSuchFieldException("Field '" + fieldName + "' not found on class " + originClass); + } + """ + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun getStaticFieldValue(clazz: Class<*>, fieldName: String): kotlin.Any? { + var currentClass: Class<*>? = clazz + var field: java.lang.reflect.Field + do { + try { + field = currentClass!!.getDeclaredField(fieldName) + field.isAccessible = true + + val modifiersField: java.lang.reflect.Field + ${getFieldRetrievingBlock(language, "java.lang.reflect.Field", "modifiers", "modifiersField")} + + modifiersField.isAccessible = true + modifiersField.setInt(field, field.modifiers and java.lang.reflect.Modifier.FINAL.inv()) + + return field.get(null) + } catch (e: NoSuchFieldException) { + currentClass = currentClass!!.superclass + } + } while (currentClass != null) + + throw NoSuchFieldException("Field '" + fieldName + "' not found on class " + clazz) + } + """ + } + }.trimIndent() + +private fun getFieldValue(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static Object getFieldValue(Object obj, String fieldClassName, String fieldName) throws ClassNotFoundException, NoSuchMethodException, java.lang.reflect.InvocationTargetException, IllegalAccessException, NoSuchFieldException { + Class clazz = Class.forName(fieldClassName); + java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); + + field.setAccessible(true); + + java.lang.reflect.Field modifiersField; + ${getFieldRetrievingBlock(language, "java.lang.reflect.Field", "modifiers", "modifiersField")} + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); + + return field.get(obj); + } + """ + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun getFieldValue(any: kotlin.Any, fieldClassName: String, fieldName: String): kotlin.Any? { + val clazz: Class<*> = Class.forName(fieldClassName) + val field: java.lang.reflect.Field = clazz.getDeclaredField(fieldName) + + field.isAccessible = true + + val modifiersField: java.lang.reflect.Field + ${getFieldRetrievingBlock(language, "java.lang.reflect.Field", "modifiers", "modifiersField")} + modifiersField.isAccessible = true + modifiersField.setInt(field, field.modifiers and java.lang.reflect.Modifier.FINAL.inv()) + + return field.get(any) + } + """ + } + }.trimIndent() + +private fun setStaticField(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static void setStaticField(Class clazz, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { + java.lang.reflect.Field field; + + try { + do { + try { + field = clazz.getDeclaredField(fieldName); + } catch (Exception e) { + clazz = clazz.getSuperclass(); + field = null; + } + } while (field == null); + + java.lang.reflect.Field modifiersField; + ${getFieldRetrievingBlock(language, "java.lang.reflect.Field", "modifiers", "modifiersField")} + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); + + field.setAccessible(true); + field.set(null, fieldValue); + } + catch(java.lang.reflect.InvocationTargetException e){ + e.printStackTrace(); + } + catch(NoSuchMethodException e2) { + e2.printStackTrace(); + } + } + """ + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun setStaticField(defaultClass: Class<*>, fieldName: String, fieldValue: kotlin.Any?) { + var field: java.lang.reflect.Field? + var clazz = defaultClass + + do { + try { + field = clazz.getDeclaredField(fieldName) + } catch (e: Exception) { + clazz = clazz.superclass + field = null + } + } while (field == null) + + val modifiersField: java.lang.reflect.Field + ${getFieldRetrievingBlock(language, "java.lang.reflect.Field", "modifiers", "modifiersField")} + modifiersField.isAccessible = true + modifiersField.setInt(field, field.modifiers and java.lang.reflect.Modifier.FINAL.inv()) + + field.isAccessible = true + field.set(null, fieldValue) + } + """ + } + }.trimIndent() + +private fun setField(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static void setField(Object object, String fieldClassName, String fieldName, Object fieldValue) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, java.lang.reflect.InvocationTargetException { + Class clazz = Class.forName(fieldClassName); + java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); + + java.lang.reflect.Field modifiersField; + ${getFieldRetrievingBlock(language, "java.lang.reflect.Field", "modifiers", "modifiersField")} + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); + + field.setAccessible(true); + field.set(object, fieldValue); + } + """ + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun setField(any: kotlin.Any, fieldClassName: String, fieldName: String, fieldValue: kotlin.Any?) { + val clazz: Class<*> = Class.forName(fieldClassName) + val field: java.lang.reflect.Field = clazz.getDeclaredField(fieldName) + + val modifiersField: java.lang.reflect.Field + ${getFieldRetrievingBlock(language, "java.lang.reflect.Field", "modifiers", "modifiersField")} + modifiersField.isAccessible = true + modifiersField.setInt(field, field.modifiers and java.lang.reflect.Modifier.FINAL.inv()) + + field.isAccessible = true + field.set(any, fieldValue) + } + """ + } + }.trimIndent() + +private fun createArray(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static Object[] createArray(String className, int length, Object... values) throws ClassNotFoundException { + Object array = java.lang.reflect.Array.newInstance(Class.forName(className), length); + + for (int i = 0; i < values.length; i++) { + java.lang.reflect.Array.set(array, i, values[i]); + } + + return (Object[]) array; + } + """ + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}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 kotlin.Array + } + """ + } + }.trimIndent() + +private fun createInstance(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static Object createInstance(String className) throws Exception { + Class clazz = Class.forName(className); + return Class.forName("sun.misc.Unsafe").getDeclaredMethod("allocateInstance", Class.class) + .invoke(getUnsafeInstance(), clazz); + } + """ + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}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) + } + """ + } + }.trimIndent() + +private fun getUnsafeInstance(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}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); + } + """ + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun getUnsafeInstance(): kotlin.Any? { + val f: java.lang.reflect.Field = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe") + f.isAccessible = true + return f[null] + } + """ + } + }.trimIndent() + +/** + * Mockito mock uses its own equals which we cannot rely on + */ +private fun isMockCondition(mockFrameworkUsed: Boolean, mockFramework: MockFramework): String { + if (!mockFrameworkUsed) return "" + + return when (mockFramework) { + MockFramework.MOCKITO -> " && !org.mockito.Mockito.mockingDetails(o1).isMock()" + // in case we will add any other mock frameworks, newer Kotlin compiler versions + // will report a non-exhaustive 'when', so we will not forget to support them here as well + } +} + +private fun deepEquals( + visibility: Visibility, + language: CodegenLanguage, + mockFrameworkUsed: Boolean, + mockFramework: MockFramework +): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + static class FieldsPair { + final Object o1; + final Object o2; + + public FieldsPair(Object o1, Object o2) { + this.o1 = o1; + this.o2 = o2; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FieldsPair that = (FieldsPair) o; + return java.util.Objects.equals(o1, that.o1) && java.util.Objects.equals(o2, that.o2); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(o1, o2); + } + } + + ${visibility by language}static boolean deepEquals(Object o1, Object o2) { + return deepEquals(o1, o2, new java.util.HashSet<>()); + } + + private static boolean deepEquals(Object o1, Object o2, java.util.Set visited) { + visited.add(new FieldsPair(o1, o2)); + + if (o1 == o2) { + return true; + } + + if (o1 == null || o2 == null) { + return false; + } + + if (o1 instanceof Iterable) { + if (!(o2 instanceof Iterable)) { + return false; + } + + return iterablesDeepEquals((Iterable) o1, (Iterable) o2, visited); + } + + if (o2 instanceof Iterable) { + return false; + } + + if (o1 instanceof java.util.stream.BaseStream) { + if (!(o2 instanceof java.util.stream.BaseStream)) { + return false; + } + + return streamsDeepEquals((java.util.stream.BaseStream) o1, (java.util.stream.BaseStream) o2, visited); + } + + if (o2 instanceof java.util.stream.BaseStream) { + return false; + } + + if (o1 instanceof java.util.Map) { + if (!(o2 instanceof java.util.Map)) { + return false; + } + + return mapsDeepEquals((java.util.Map) o1, (java.util.Map) o2, visited); + } + + if (o2 instanceof java.util.Map) { + return false; + } + + Class firstClass = o1.getClass(); + if (firstClass.isArray()) { + if (!o2.getClass().isArray()) { + return false; + } + + // Primitive arrays should not appear here + return arraysDeepEquals(o1, o2, visited); + } + + // common classes + + // check if class has custom equals method (including wrappers and strings) + // It is very important to check it here but not earlier because iterables and maps also have custom equals + // based on elements equals + if (hasCustomEquals(firstClass)${isMockCondition(mockFrameworkUsed, mockFramework)}) { + return o1.equals(o2); + } + + // common classes without custom equals, use comparison by fields + final java.util.List fields = new java.util.ArrayList<>(); + while (firstClass != Object.class) { + fields.addAll(java.util.Arrays.asList(firstClass.getDeclaredFields())); + // Interface should not appear here + firstClass = firstClass.getSuperclass(); + } + + for (java.lang.reflect.Field field : fields) { + field.setAccessible(true); + try { + final Object field1 = field.get(o1); + final Object field2 = field.get(o2); + if (!visited.contains(new FieldsPair(field1, field2)) && !deepEquals(field1, field2, visited)) { + return false; + } + } catch (IllegalArgumentException e) { + return false; + } catch (IllegalAccessException e) { + // should never occur because field was set accessible + return false; + } + } + + return true; + } + """.trimIndent() + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun deepEquals(o1: kotlin.Any?, o2: kotlin.Any?): Boolean = deepEquals(o1, o2, hashSetOf()) + + 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 kotlin.collections.Iterable<*>) { + return if (o2 !is kotlin.collections.Iterable<*>) false else iterablesDeepEquals(o1, o2, visited) + } + + if (o2 is kotlin.collections.Iterable<*>) return false + + if (o1 is java.util.stream.BaseStream<*, *>) { + return if (o2 !is java.util.stream.BaseStream<*, *>) false else streamsDeepEquals(o1, o2, visited) + } + + if (o2 is java.util.stream.BaseStream<*, *>) return false + + if (o1 is kotlin.collections.Map<*, *>) { + return if (o2 !is kotlin.collections.Map<*, *>) false else mapsDeepEquals(o1, o2, visited) + } + + if (o2 is kotlin.collections.Map<*, *>) return false + + var firstClass: Class<*> = o1.javaClass + if (firstClass.isArray) { + return if (!o2.javaClass.isArray) { + false + } else { + arraysDeepEquals(o1, o2, visited) + } + } + + // check if class has custom equals method (including wrappers and strings) + // It is very important to check it here but not earlier because iterables and maps also have custom equals + // based on elements equals + if (hasCustomEquals(firstClass)${isMockCondition(mockFrameworkUsed, mockFramework)}) { + return o1 == o2 + } + + // common classes without custom equals, use comparison by fields + val fields: kotlin.collections.MutableList = mutableListOf() + while (firstClass != kotlin.Any::class.java) { + fields += listOf(*firstClass.declaredFields) + // Interface should not appear here + firstClass = firstClass.superclass + } + + for (field in fields) { + field.isAccessible = true + try { + val field1 = field[o1] + val field2 = field[o2] + if ((field1 to field2) !in visited && !deepEquals(field1, field2, visited)) return false + } catch (e: IllegalArgumentException) { + return false + } catch (e: IllegalAccessException) { + // should never occur + return false + } + } + + return true + } + """.trimIndent() + } + } + +private fun arraysDeepEquals(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static 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(java.lang.reflect.Array.get(arr1, i), java.lang.reflect.Array.get(arr2, i), visited)) { + return false; + } + } + + return true; + } + """.trimIndent() + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}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 + + for (i in 0 until size) { + if (!deepEquals(java.lang.reflect.Array.get(arr1, i), java.lang.reflect.Array.get(arr2, i), visited)) { + return false + } + } + + return true + } + """.trimIndent() + } + } + +private fun iterablesDeepEquals(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static 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; + } + } + + if (firstIterator.hasNext()) { + return false; + } + + return !secondIterator.hasNext(); + } + """.trimIndent() + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun iterablesDeepEquals( + i1: Iterable<*>, + i2: Iterable<*>, + visited: kotlin.collections.MutableSet> + ): Boolean { + val firstIterator = i1.iterator() + val secondIterator = i2.iterator() + while (firstIterator.hasNext() && secondIterator.hasNext()) { + if (!deepEquals(firstIterator.next(), secondIterator.next(), visited)) return false + } + + return if (firstIterator.hasNext()) false else !secondIterator.hasNext() + } + """.trimIndent() + } + } + +private fun streamsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static boolean streamsDeepEquals( + java.util.stream.BaseStream s1, + java.util.stream.BaseStream 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 -> { + """ + ${visibility by language}fun streamsDeepEquals( + s1: java.util.stream.BaseStream<*, *>, + s2: java.util.stream.BaseStream<*, *>, + 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() + } + } + +private fun mapsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static 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 java.util.Map.Entry firstEntry = firstIterator.next(); + final java.util.Map.Entry secondEntry = secondIterator.next(); + + if (!deepEquals(firstEntry.getKey(), secondEntry.getKey(), visited)) { + return false; + } + + if (!deepEquals(firstEntry.getValue(), secondEntry.getValue(), visited)) { + return false; + } + } + + if (firstIterator.hasNext()) { + return false; + } + + return !secondIterator.hasNext(); + } + """.trimIndent() + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun mapsDeepEquals( + m1: kotlin.collections.Map<*, *>, + m2: kotlin.collections.Map<*, *>, + visited: kotlin.collections.MutableSet> + ): Boolean { + val firstIterator = m1.entries.iterator() + val secondIterator = m2.entries.iterator() + while (firstIterator.hasNext() && secondIterator.hasNext()) { + val firstEntry = firstIterator.next() + val secondEntry = secondIterator.next() + + if (!deepEquals(firstEntry.key, secondEntry.key, visited)) return false + + if (!deepEquals(firstEntry.value, secondEntry.value, visited)) return false + } + + return if (firstIterator.hasNext()) false else !secondIterator.hasNext() + } + """.trimIndent() + } + } + +private fun hasCustomEquals(visibility: Visibility, language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + ${visibility by language}static boolean hasCustomEquals(Class clazz) { + while (!Object.class.equals(clazz)) { + try { + clazz.getDeclaredMethod("equals", Object.class); + return true; + } catch (Exception e) { + // Interface should not appear here + clazz = clazz.getSuperclass(); + } + } + + return false; + } + """.trimIndent() + } + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun hasCustomEquals(clazz: Class<*>): Boolean { + var c = clazz + while (kotlin.Any::class.java != c) { + try { + c.getDeclaredMethod("equals", kotlin.Any::class.java) + return true + } catch (e: Exception) { + // Interface should not appear here + c = c.superclass + } + } + return false + } + """.trimIndent() + } + } + +private fun getArrayLength(visibility: Visibility, language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + ${visibility by language}static int getArrayLength(Object arr) { + return java.lang.reflect.Array.getLength(arr); + } + """.trimIndent() + CodegenLanguage.KOTLIN -> + """ + ${visibility by language}fun getArrayLength(arr: kotlin.Any?): Int = java.lang.reflect.Array.getLength(arr) + """.trimIndent() + } + +private fun consumeBaseStream(visibility: Visibility, language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + ${visibility by language}static void consumeBaseStream(java.util.stream.BaseStream stream) { + stream.iterator().forEachRemaining(value -> {}); + } + """.trimIndent() + CodegenLanguage.KOTLIN -> { + """ + ${visibility by language}fun consumeBaseStream(stream: java.util.stream.BaseStream<*, *>) { + stream.iterator().forEachRemaining {} + } + """.trimIndent() + } + } + +private fun buildStaticLambda(visibility: Visibility, language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + /** + * @param samType a class representing a functional interface. + * @param declaringClass a class where the lambda is declared. + * @param lambdaName a name of the synthetic method that represents a lambda. + * @param capturedArguments a vararg containing {@link CapturedArgument} instances representing + * values that the given lambda has captured. + * @return an {@link Object} that represents an instance of the given functional interface {@code samType} + * and implements its single abstract method with the behavior of the given lambda. + */ + ${visibility by language}static Object buildStaticLambda( + Class samType, + Class declaringClass, + String lambdaName, + CapturedArgument... capturedArguments + ) throws Throwable { + // Create lookup for class where the lambda is declared in. + java.lang.invoke.MethodHandles.Lookup caller = getLookupIn(declaringClass); + + // Obtain the single abstract method of a functional interface whose instance we are building. + // For example, for `java.util.function.Predicate` it will be method `test`. + java.lang.reflect.Method singleAbstractMethod = getSingleAbstractMethod(samType); + String invokedName = singleAbstractMethod.getName(); + // Method type of single abstract method of the target functional interface. + java.lang.invoke.MethodType samMethodType = java.lang.invoke.MethodType.methodType(singleAbstractMethod.getReturnType(), singleAbstractMethod.getParameterTypes()); + + java.lang.reflect.Method lambdaMethod = getLambdaMethod(declaringClass, lambdaName); + lambdaMethod.setAccessible(true); + java.lang.invoke.MethodType lambdaMethodType = java.lang.invoke.MethodType.methodType(lambdaMethod.getReturnType(), lambdaMethod.getParameterTypes()); + java.lang.invoke.MethodHandle lambdaMethodHandle = caller.findStatic(declaringClass, lambdaName, lambdaMethodType); + + Class[] capturedArgumentTypes = getLambdaCapturedArgumentTypes(capturedArguments); + java.lang.invoke.MethodType invokedType = java.lang.invoke.MethodType.methodType(samType, capturedArgumentTypes); + java.lang.invoke.MethodType instantiatedMethodType = getInstantiatedMethodType(lambdaMethod, capturedArgumentTypes); + + // Create a CallSite for the given lambda. + java.lang.invoke.CallSite site = java.lang.invoke.LambdaMetafactory.metafactory( + caller, + invokedName, + invokedType, + samMethodType, + lambdaMethodHandle, + instantiatedMethodType); + + Object[] capturedValues = getLambdaCapturedArgumentValues(capturedArguments); + + // Get MethodHandle and pass captured values to it to obtain an object + // that represents the target functional interface instance. + java.lang.invoke.MethodHandle handle = site.getTarget(); + return handle.invokeWithArguments(capturedValues); + } + """.trimIndent() + CodegenLanguage.KOTLIN -> + """ + /** + * @param samType a class representing a functional interface. + * @param declaringClass a class where the lambda is declared. + * @param lambdaName a name of the synthetic method that represents a lambda. + * @param capturedArguments a vararg containing [CapturedArgument] instances representing + * values that the given lambda has captured. + * @return an [Any] that represents an instance of the given functional interface `samType` + * and implements its single abstract method with the behavior of the given lambda. + */ + ${visibility by language}fun buildStaticLambda( + samType: Class<*>, + declaringClass: Class<*>, + lambdaName: String, + vararg capturedArguments: CapturedArgument + ): Any { + // Create lookup for class where the lambda is declared in. + val caller = getLookupIn(declaringClass) + + // Obtain the single abstract method of a functional interface whose instance we are building. + // For example, for `java.util.function.Predicate` it will be method `test`. + val singleAbstractMethod = getSingleAbstractMethod(samType) + val invokedName = singleAbstractMethod.name + // Method type of single abstract method of the target functional interface. + val samMethodType = java.lang.invoke.MethodType.methodType(singleAbstractMethod.returnType, singleAbstractMethod.parameterTypes) + + val lambdaMethod = getLambdaMethod(declaringClass, lambdaName) + lambdaMethod.isAccessible = true + val lambdaMethodType = java.lang.invoke.MethodType.methodType(lambdaMethod.returnType, lambdaMethod.parameterTypes) + val lambdaMethodHandle = caller.findStatic(declaringClass, lambdaName, lambdaMethodType) + + val capturedArgumentTypes = getLambdaCapturedArgumentTypes(*capturedArguments) + val invokedType = java.lang.invoke.MethodType.methodType(samType, capturedArgumentTypes) + val instantiatedMethodType = getInstantiatedMethodType(lambdaMethod, capturedArgumentTypes) + + // Create a CallSite for the given lambda. + val site = java.lang.invoke.LambdaMetafactory.metafactory( + caller, + invokedName, + invokedType, + samMethodType, + lambdaMethodHandle, + instantiatedMethodType + ) + val capturedValues = getLambdaCapturedArgumentValues(*capturedArguments) + + // Get MethodHandle and pass captured values to it to obtain an object + // that represents the target functional interface instance. + val handle = site.target + return handle.invokeWithArguments(*capturedValues) + } + """.trimIndent() + } + +private fun buildLambda(visibility: Visibility, language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + /** + * @param samType a class representing a functional interface. + * @param declaringClass a class where the lambda is declared. + * @param lambdaName a name of the synthetic method that represents a lambda. + * @param capturedReceiver an object of {@code declaringClass} that is captured by the lambda. + * When the synthetic lambda method is not static, it means that the lambda captures an instance + * of the class it is declared in. + * @param capturedArguments a vararg containing {@link CapturedArgument} instances representing + * values that the given lambda has captured. + * @return an {@link Object} that represents an instance of the given functional interface {@code samType} + * and implements its single abstract method with the behavior of the given lambda. + */ + ${visibility by language}static Object buildLambda( + Class samType, + Class declaringClass, + String lambdaName, + Object capturedReceiver, + CapturedArgument... capturedArguments + ) throws Throwable { + // Create lookup for class where the lambda is declared in. + java.lang.invoke.MethodHandles.Lookup caller = getLookupIn(declaringClass); + + // Obtain the single abstract method of a functional interface whose instance we are building. + // For example, for `java.util.function.Predicate` it will be method `test`. + java.lang.reflect.Method singleAbstractMethod = getSingleAbstractMethod(samType); + String invokedName = singleAbstractMethod.getName(); + // Method type of single abstract method of the target functional interface. + java.lang.invoke.MethodType samMethodType = java.lang.invoke.MethodType.methodType(singleAbstractMethod.getReturnType(), singleAbstractMethod.getParameterTypes()); + + java.lang.reflect.Method lambdaMethod = getLambdaMethod(declaringClass, lambdaName); + lambdaMethod.setAccessible(true); + java.lang.invoke.MethodType lambdaMethodType = java.lang.invoke.MethodType.methodType(lambdaMethod.getReturnType(), lambdaMethod.getParameterTypes()); + java.lang.invoke.MethodHandle lambdaMethodHandle = caller.findVirtual(declaringClass, lambdaName, lambdaMethodType); + + Class[] capturedArgumentTypes = getLambdaCapturedArgumentTypes(capturedArguments); + java.lang.invoke.MethodType invokedType = java.lang.invoke.MethodType.methodType(samType, capturedReceiver.getClass(), capturedArgumentTypes); + java.lang.invoke.MethodType instantiatedMethodType = getInstantiatedMethodType(lambdaMethod, capturedArgumentTypes); + + // Create a CallSite for the given lambda. + java.lang.invoke.CallSite site = java.lang.invoke.LambdaMetafactory.metafactory( + caller, + invokedName, + invokedType, + samMethodType, + lambdaMethodHandle, + instantiatedMethodType); + + Object[] capturedArgumentValues = getLambdaCapturedArgumentValues(capturedArguments); + + // This array will contain the value of captured receiver + // (`this` instance of class where the lambda is declared) + // and the values of captured arguments. + Object[] capturedValues = new Object[capturedArguments.length + 1]; + + // Setting the captured receiver value. + capturedValues[0] = capturedReceiver; + + // Setting the captured argument values. + System.arraycopy(capturedArgumentValues, 0, capturedValues, 1, capturedArgumentValues.length); + + // Get MethodHandle and pass captured values to it to obtain an object + // that represents the target functional interface instance. + java.lang.invoke.MethodHandle handle = site.getTarget(); + return handle.invokeWithArguments(capturedValues); + } + """.trimIndent() + CodegenLanguage.KOTLIN -> + """ + /** + * @param samType a class representing a functional interface. + * @param declaringClass a class where the lambda is declared. + * @param lambdaName a name of the synthetic method that represents a lambda. + * @param capturedReceiver an object of `declaringClass` that is captured by the lambda. + * When the synthetic lambda method is not static, it means that the lambda captures an instance + * of the class it is declared in. + * @param capturedArguments a vararg containing [CapturedArgument] instances representing + * values that the given lambda has captured. + * @return an [Any] that represents an instance of the given functional interface `samType` + * and implements its single abstract method with the behavior of the given lambda. + */ + ${visibility by language}fun buildLambda( + samType: Class<*>, + declaringClass: Class<*>, + lambdaName: String, + capturedReceiver: Any, + vararg capturedArguments: CapturedArgument + ): Any { + // Create lookup for class where the lambda is declared in. + val caller = getLookupIn(declaringClass) + + // Obtain the single abstract method of a functional interface whose instance we are building. + // For example, for `java.util.function.Predicate` it will be method `test`. + val singleAbstractMethod = getSingleAbstractMethod(samType) + val invokedName = singleAbstractMethod.name + // Method type of single abstract method of the target functional interface. + val samMethodType = java.lang.invoke.MethodType.methodType(singleAbstractMethod.returnType, singleAbstractMethod.parameterTypes) + + val lambdaMethod = getLambdaMethod(declaringClass, lambdaName) + lambdaMethod.isAccessible = true + val lambdaMethodType = java.lang.invoke.MethodType.methodType(lambdaMethod.returnType, lambdaMethod.parameterTypes) + val lambdaMethodHandle = caller.findVirtual(declaringClass, lambdaName, lambdaMethodType) + + val capturedArgumentTypes = getLambdaCapturedArgumentTypes(*capturedArguments) + val invokedType = java.lang.invoke.MethodType.methodType(samType, capturedReceiver.javaClass, *capturedArgumentTypes) + val instantiatedMethodType = getInstantiatedMethodType(lambdaMethod, capturedArgumentTypes) + + // Create a CallSite for the given lambda. + val site = java.lang.invoke.LambdaMetafactory.metafactory( + caller, + invokedName, + invokedType, + samMethodType, + lambdaMethodHandle, + instantiatedMethodType + ) + val capturedValues = mutableListOf() + .apply { + add(capturedReceiver) + addAll(getLambdaCapturedArgumentValues(*capturedArguments)) + }.toTypedArray() + + + // Get MethodHandle and pass captured values to it to obtain an object + // that represents the target functional interface instance. + val handle = site.target + return handle.invokeWithArguments(*capturedValues) + } + """.trimIndent() + } + +private fun getLookupIn(language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + /** + * @param clazz a class to create lookup instance for. + * @return {@link java.lang.invoke.MethodHandles.Lookup} instance for the given {@code clazz}. + * It can be used, for example, to search methods of this {@code clazz}, even the {@code private} ones. + */ + private static java.lang.invoke.MethodHandles.Lookup getLookupIn(Class clazz) throws IllegalAccessException, NoSuchFieldException, java.lang.NoSuchMethodException, java.lang.reflect.InvocationTargetException { + java.lang.invoke.MethodHandles.Lookup lookup = java.lang.invoke.MethodHandles.lookup().in(clazz); + + // Allow lookup to access all members of declaringClass, including the private ones. + // For example, it is useful to access private synthetic methods representing lambdas. + + java.lang.reflect.Field allowedModes; + java.lang.reflect.Field allModesField; + ${getFieldRetrievingBlock(language, "java.lang.invoke.MethodHandles.Lookup", "allowedModes", "allowedModes")} + ${getFieldRetrievingBlock(language, "java.lang.invoke.MethodHandles.Lookup", "ALL_MODES", "allModesField")} + allModesField.setAccessible(true); + allowedModes.setAccessible(true); + allowedModes.setInt(lookup, (Integer) allModesField.get(null)); + + return lookup; + } + """.trimIndent() + CodegenLanguage.KOTLIN -> + """ + /** + * @param clazz a class to create lookup instance for. + * @return [java.lang.invoke.MethodHandles.Lookup] instance for the given [clazz]. + * It can be used, for example, to search methods of this [clazz], even the `private` ones. + */ + private fun getLookupIn(clazz: Class<*>): java.lang.invoke.MethodHandles.Lookup { + val lookup = java.lang.invoke.MethodHandles.lookup().`in`(clazz) + + // Allow lookup to access all members of declaringClass, including the private ones. + // For example, it is useful to access private synthetic methods representing lambdas. + val allowedModes: java.lang.reflect.Field + val allModesField: java.lang.reflect.Field + ${getFieldRetrievingBlock(language, "java.lang.invoke.MethodHandles.Lookup", "allowedModes", "allowedModes")} + ${getFieldRetrievingBlock(language, "java.lang.invoke.MethodHandles.Lookup", "ALL_MODES", "allModesField")} + allowedModes.isAccessible = true + allModesField.isAccessible = true + allowedModes.setInt(lookup, allModesField.get(null) as Int) + + return lookup + } + """.trimIndent() + } + +private fun getLambdaCapturedArgumentTypes(language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + /** + * @param capturedArguments values captured by some lambda. Note that this argument does not contain + * the possibly captured instance of the class where the lambda is declared. + * It contains all the other captured values. They are represented as arguments of the synthetic method + * that the lambda is compiled into. Hence, the name of the argument. + * @return types of the given {@code capturedArguments}. + * These types are required to build {@code invokedType}, which represents + * the target functional interface with info about captured values' types. + * See {@link java.lang.invoke.LambdaMetafactory#metafactory} method documentation for more details on what {@code invokedType} is. + */ + private static Class[] getLambdaCapturedArgumentTypes(CapturedArgument... capturedArguments) { + Class[] capturedArgumentTypes = new Class[capturedArguments.length]; + for (int i = 0; i < capturedArguments.length; i++) { + capturedArgumentTypes[i] = capturedArguments[i].type; + } + return capturedArgumentTypes; + } + """.trimIndent() + CodegenLanguage.KOTLIN -> + """ + /** + * @param capturedArguments values captured by some lambda. Note that this argument does not contain + * the possibly captured instance of the class where the lambda is declared. + * It contains all the other captured values. They are represented as arguments of the synthetic method + * that the lambda is compiled into. Hence, the name of the argument. + * @return types of the given `capturedArguments`. + * These types are required to build `invokedType`, which represents + * the target functional interface with info about captured values' types. + * See [java.lang.invoke.LambdaMetafactory.metafactory] method documentation for more details on what `invokedType` is. + */ + private fun getLambdaCapturedArgumentTypes(vararg capturedArguments: CapturedArgument): Array> { + return capturedArguments + .map { it.type } + .toTypedArray() + } + """.trimIndent() + } + +private fun getLambdaCapturedArgumentValues(language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + /** + * Obtain captured values to be used as captured arguments in the lambda call. + */ + private static Object[] getLambdaCapturedArgumentValues(CapturedArgument... capturedArguments) { + return java.util.Arrays.stream(capturedArguments) + .map(argument -> argument.value) + .toArray(); + } + """.trimIndent() + CodegenLanguage.KOTLIN -> + """ + /** + * Obtain captured values to be used as captured arguments in the lambda call. + */ + private fun getLambdaCapturedArgumentValues(vararg capturedArguments: CapturedArgument): Array { + return capturedArguments + .map { it.value } + .toTypedArray() + } + """.trimIndent() + } + +private fun getInstantiatedMethodType(language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + /** + * @param lambdaMethod {@link java.lang.reflect.Method} that represents a synthetic method for lambda. + * @param capturedArgumentTypes types of values captured by lambda. + * @return {@link java.lang.invoke.MethodType} that represents the value of argument {@code instantiatedMethodType} + * of method {@link java.lang.invoke.LambdaMetafactory#metafactory}. + */ + private static java.lang.invoke.MethodType getInstantiatedMethodType(java.lang.reflect.Method lambdaMethod, Class[] capturedArgumentTypes) { + // Types of arguments of synthetic method (representing lambda) without the types of captured values. + java.util.List> instantiatedMethodParamTypeList = java.util.Arrays.stream(lambdaMethod.getParameterTypes()) + .skip(capturedArgumentTypes.length) + .collect(java.util.stream.Collectors.toList()); + + // The same types, but stored in an array. + Class[] instantiatedMethodParamTypes = new Class[instantiatedMethodParamTypeList.size()]; + for (int i = 0; i < instantiatedMethodParamTypeList.size(); i++) { + instantiatedMethodParamTypes[i] = instantiatedMethodParamTypeList.get(i); + } + + return java.lang.invoke.MethodType.methodType(lambdaMethod.getReturnType(), instantiatedMethodParamTypes); + } + """.trimIndent() + CodegenLanguage.KOTLIN -> + """ + /** + * @param lambdaMethod [java.lang.reflect.Method] that represents a synthetic method for lambda. + * @param capturedArgumentTypes types of values captured by lambda. + * @return [java.lang.invoke.MethodType] that represents the value of argument `instantiatedMethodType` + * of method [java.lang.invoke.LambdaMetafactory.metafactory]. + */ + private fun getInstantiatedMethodType( + lambdaMethod: java.lang.reflect.Method, + capturedArgumentTypes: Array> + ): java.lang.invoke.MethodType { + // Types of arguments of synthetic method (representing lambda) without the types of captured values. + val instantiatedMethodParamTypes = lambdaMethod.parameterTypes + .drop(capturedArgumentTypes.size) + .toTypedArray() + + return java.lang.invoke.MethodType.methodType(lambdaMethod.returnType, instantiatedMethodParamTypes) + } + """.trimIndent() + } + +private fun getLambdaMethod(language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + /** + * @param declaringClass class where a lambda is declared. + * @param lambdaName name of synthetic method that represents a lambda. + * @return {@link java.lang.reflect.Method} instance for the synthetic method that represent a lambda. + */ + private static java.lang.reflect.Method getLambdaMethod(Class declaringClass, String lambdaName) { + return java.util.Arrays.stream(declaringClass.getDeclaredMethods()) + .filter(method -> method.getName().equals(lambdaName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No lambda method named " + lambdaName + " was found in class: " + declaringClass.getCanonicalName())); + } + """.trimIndent() + CodegenLanguage.KOTLIN -> + """ + /** + * @param declaringClass class where a lambda is declared. + * @param lambdaName name of synthetic method that represents a lambda. + * @return [java.lang.reflect.Method] instance for the synthetic method that represent a lambda. + */ + private fun getLambdaMethod(declaringClass: Class<*>, lambdaName: String): java.lang.reflect.Method { + return declaringClass.declaredMethods.firstOrNull { it.name == lambdaName } + ?: throw IllegalArgumentException("No lambda method named ${'$'}lambdaName was found in class: ${'$'}{declaringClass.canonicalName}") + } + """.trimIndent() + } + +private fun getSingleAbstractMethod(language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + private static java.lang.reflect.Method getSingleAbstractMethod(Class clazz) { + java.util.List abstractMethods = java.util.Arrays.stream(clazz.getMethods()) + .filter(method -> java.lang.reflect.Modifier.isAbstract(method.getModifiers())) + .collect(java.util.stream.Collectors.toList()); + + if (abstractMethods.isEmpty()) { + throw new IllegalArgumentException("No abstract methods found in class: " + clazz.getCanonicalName()); + } + if (abstractMethods.size() > 1) { + throw new IllegalArgumentException("More than one abstract method found in class: " + clazz.getCanonicalName()); + } + + return abstractMethods.get(0); + } + """.trimIndent() + CodegenLanguage.KOTLIN -> + """ + /** + * @param clazz functional interface + * @return a [java.lang.reflect.Method] for the single abstract method of the given functional interface `clazz`. + */ + private fun getSingleAbstractMethod(clazz: Class<*>): java.lang.reflect.Method { + val abstractMethods = clazz.methods.filter { java.lang.reflect.Modifier.isAbstract(it.modifiers) } + require(abstractMethods.isNotEmpty()) { "No abstract methods found in class: " + clazz.canonicalName } + require(abstractMethods.size <= 1) { "More than one abstract method found in class: " + clazz.canonicalName } + return abstractMethods[0] + } + """.trimIndent() + } + +private fun capturedArgumentClass(language: CodegenLanguage) = + when (language) { + CodegenLanguage.JAVA -> + """ + /** + * This class represents the {@code type} and {@code value} of a value captured by lambda. + * Captured values are represented as arguments of a synthetic method that lambda is compiled into, + * hence the name of the class. + */ + public static class CapturedArgument { + private Class type; + private Object value; + + public CapturedArgument(Class type, Object value) { + this.type = type; + this.value = value; + } + } + """.trimIndent() + CodegenLanguage.KOTLIN -> { + """ + /** + * This class represents the `type` and `value` of a value captured by lambda. + * Captured values are represented as arguments of a synthetic method that lambda is compiled into, + * hence the name of the class. + */ + data class CapturedArgument(val type: Class<*>, val value: Any?) + """.trimIndent() + } + } + +internal fun CgContextOwner.importUtilMethodDependencies(id: MethodId) { + // if util methods come from a separate UtUtils class and not from the test class, + // then we don't need to import any other methods, hence we return from method + val utilMethodProvider = utilMethodProvider as? TestClassUtilMethodProvider ?: return + for (classId in utilMethodProvider.regularImportsByUtilMethod(id, codegenLanguage)) { + importIfNeeded(classId) + } + for (methodId in utilMethodProvider.staticImportsByUtilMethod(id)) { + collectedImports += StaticImport(methodId.classId.canonicalName, methodId.name) + } +} + +private fun TestClassUtilMethodProvider.regularImportsByUtilMethod( + id: MethodId, + codegenLanguage: CodegenLanguage +): List { + return when (id) { + getUnsafeInstanceMethodId -> listOf(fieldClassId) + 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) + getFieldValueMethodId -> listOf(fieldClassId, Modifier::class.id) + getStaticFieldValueMethodId -> listOf(fieldClassId, Modifier::class.id) + getEnumConstantByNameMethodId -> listOf(fieldClassId) + deepEqualsMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf( + Objects::class.id, + Iterable::class.id, + Map::class.id, + List::class.id, + ArrayList::class.id, + Set::class.id, + HashSet::class.id, + fieldClassId, + Arrays::class.id + ) + CodegenLanguage.KOTLIN -> listOf(fieldClassId, Arrays::class.id) + } + arraysDeepEqualsMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf(java.lang.reflect.Array::class.id, Set::class.id) + CodegenLanguage.KOTLIN -> listOf(java.lang.reflect.Array::class.id) + } + iterablesDeepEqualsMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf(Iterable::class.id, Iterator::class.id, Set::class.id) + CodegenLanguage.KOTLIN -> emptyList() + } + streamsDeepEqualsMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf(java.util.stream.BaseStream::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 -> emptyList() + } + hasCustomEqualsMethodId -> emptyList() + getArrayLengthMethodId -> listOf(java.lang.reflect.Array::class.id) + consumeBaseStreamMethodId -> listOf(java.util.stream.BaseStream::class.id) + buildStaticLambdaMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf( + MethodHandles::class.id, Method::class.id, MethodType::class.id, + MethodHandle::class.id, CallSite::class.id, LambdaMetafactory::class.id + ) + CodegenLanguage.KOTLIN -> listOf(MethodType::class.id, LambdaMetafactory::class.id) + } + buildLambdaMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf( + MethodHandles::class.id, Method::class.id, MethodType::class.id, + MethodHandle::class.id, CallSite::class.id, LambdaMetafactory::class.id + ) + CodegenLanguage.KOTLIN -> listOf(MethodType::class.id, LambdaMetafactory::class.id) + } + getLookupInMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf(MethodHandles::class.id, Field::class.id, Modifier::class.id) + CodegenLanguage.KOTLIN -> listOf(MethodHandles::class.id, Modifier::class.id) + } + getLambdaCapturedArgumentTypesMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf(LambdaMetafactory::class.id) + CodegenLanguage.KOTLIN -> listOf(LambdaMetafactory::class.id) + } + getLambdaCapturedArgumentValuesMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf(Arrays::class.id) + CodegenLanguage.KOTLIN -> emptyList() + } + getInstantiatedMethodTypeMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf( + Method::class.id, MethodType::class.id, LambdaMetafactory::class.id, + java.util.List::class.id, Arrays::class.id, Collectors::class.id + ) + CodegenLanguage.KOTLIN -> listOf(Method::class.id, MethodType::class.id, LambdaMetafactory::class.id) + } + getLambdaMethodMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf(Method::class.id, Arrays::class.id) + CodegenLanguage.KOTLIN -> listOf(Method::class.id) + } + getSingleAbstractMethodMethodId -> listOf( + Method::class.id, java.util.List::class.id, Arrays::class.id, + Modifier::class.id, Collectors::class.id + ) + else -> error("Unknown util method for class $this: $id") + } +} + +// Note: for now always returns an empty list, because no util method +// requires static imports, but this may change in the future +@Suppress("unused", "unused_parameter") +private fun TestClassUtilMethodProvider.staticImportsByUtilMethod(id: MethodId): List = emptyList() \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/reports/TestsGenerationReport.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/reports/TestsGenerationReport.kt new file mode 100644 index 0000000000..7ca14aa3cd --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/reports/TestsGenerationReport.kt @@ -0,0 +1,124 @@ +package org.utbot.framework.codegen.reports + +import org.utbot.common.appendHtmlLine +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTestMethodType +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.util.kClass +import kotlin.reflect.KClass + +typealias MethodGeneratedTests = MutableMap> +typealias ErrorsCount = Map + +data class TestsGenerationReport( + val executables: MutableSet = mutableSetOf(), + var successfulExecutions: MethodGeneratedTests = mutableMapOf(), + var timeoutExecutions: MethodGeneratedTests = mutableMapOf(), + var failedExecutions: MethodGeneratedTests = mutableMapOf(), + var artificiallyFailedExecutions: MethodGeneratedTests = mutableMapOf(), + var crashExecutions: MethodGeneratedTests = mutableMapOf(), + var errors: MutableMap = mutableMapOf() +) { + val classUnderTest: KClass<*> + get() = executables.firstOrNull()?.classId?.kClass + ?: error("No executables found in test report") + + val initialWarnings: MutableList<() -> String> = mutableListOf() + val hasWarnings: Boolean + get() = initialWarnings.isNotEmpty() + + val detailedStatistics: String + get() = buildString { + appendHtmlLine("Class: ${classUnderTest.qualifiedName}") + val testMethodsStatistic = executables.map { it.countTestMethods() } + val errors = executables.map { it.countErrors() } + val overallErrors = errors.sum() + + appendHtmlLine("Successful test methods: ${testMethodsStatistic.sumOf { it.successful }}") + appendHtmlLine( + "Failing because of unexpected exception test methods: ${testMethodsStatistic.sumOf { it.failing }}" + ) + appendHtmlLine( + "Failing because of exception (according to custom settings) test methods: " + + "${testMethodsStatistic.sumOf { it.artificiallyFailing }}" + ) + appendHtmlLine( + "Failing because of exceeding timeout test methods: ${testMethodsStatistic.sumOf { it.timeout }}" + ) + appendHtmlLine( + "Failing because of possible JVM crash test methods: ${testMethodsStatistic.sumOf { it.crashes }}" + ) + appendHtmlLine("Not generated because of internal errors test methods: $overallErrors") + } + + fun addMethodErrors(testSet: CgMethodTestSet, errors: Map) { + this.executables += testSet.executableUnderTest + this.errors[testSet.executableUnderTest] = errors + } + + fun addTestsByType(testSet: CgMethodTestSet, testMethods: List) { + with(testSet.executableUnderTest) { + executables += this + + testMethods.forEach { + when (it.type) { + CgTestMethodType.SUCCESSFUL, CgTestMethodType.PASSED_EXCEPTION -> updateExecutions(it, successfulExecutions) + CgTestMethodType.FAILING -> updateExecutions(it, failedExecutions) + CgTestMethodType.ARTIFICIAL -> updateExecutions(it, artificiallyFailedExecutions) + CgTestMethodType.TIMEOUT -> updateExecutions(it, timeoutExecutions) + CgTestMethodType.CRASH -> updateExecutions(it, crashExecutions) + CgTestMethodType.PARAMETRIZED -> { + // Parametrized tests are not supported in the tests report yet + // TODO JIRA:1507 + } + } + } + } + } + + fun countTestMethods() = executables.map { it.countTestMethods() }.sumOf { it.count } + + fun toString(isShort: Boolean): String = buildString { + appendHtmlLine("Target: ${classUnderTest.qualifiedName}") + if (initialWarnings.isNotEmpty()) { + initialWarnings.forEach { appendHtmlLine(it()) } + appendHtmlLine() + } + + appendHtmlLine("Overall test methods: ${countTestMethods()}") + + if (!isShort) { + appendHtmlLine(detailedStatistics) + } + } + + override fun toString(): String = toString(false) + + private fun ExecutableId.countTestMethods(): TestMethodStatistic = TestMethodStatistic( + testMethodsNumber(successfulExecutions), + testMethodsNumber(failedExecutions), + testMethodsNumber(artificiallyFailedExecutions), + testMethodsNumber(timeoutExecutions), + testMethodsNumber(crashExecutions), + ) + + private fun ExecutableId.countErrors(): Int = errors.getOrDefault(this, emptyMap()).values.sum() + + private fun ExecutableId.testMethodsNumber(executables: MethodGeneratedTests): Int = + executables.getOrDefault(this, emptySet()).size + + private fun ExecutableId.updateExecutions(it: CgTestMethod, executions: MethodGeneratedTests) { + executions.getOrPut(this) { mutableSetOf() } += it + } + + private data class TestMethodStatistic( + val successful: Int, + val failing: Int, + val artificiallyFailing: Int, + val timeout: Int, + val crashes: Int, + ) { + val count: Int = successful + failing + artificiallyFailing + timeout + crashes + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/CgNameGenerator.kt similarity index 92% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/CgNameGenerator.kt index 50254afc03..09608ccc98 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/CgNameGenerator.kt @@ -1,9 +1,9 @@ -package org.utbot.framework.codegen.model.constructor.name +package org.utbot.framework.codegen.services -import org.utbot.framework.codegen.isLanguageKeyword -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.infiniteInts +import org.utbot.framework.codegen.services.language.isLanguageKeyword +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.tree.infiniteInts import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.ConstructorId @@ -14,7 +14,7 @@ import org.utbot.framework.plugin.api.util.isArray /** * Interface for method and variable name generators */ -internal interface CgNameGenerator { +interface CgNameGenerator { /** * Generate a variable name given a [base] name. * @param isMock denotes whether a variable represents a mock object or not @@ -67,7 +67,7 @@ internal interface CgNameGenerator { * Class that generates names for methods and variables * To avoid name collisions it uses existing names information from CgContext */ -internal class CgNameGeneratorImpl(private val context: CgContext) +class CgNameGeneratorImpl(val context: CgContext) : CgNameGenerator, CgContextOwner by context { override fun variableName(base: String, isMock: Boolean, isStatic: Boolean): String { @@ -78,7 +78,7 @@ internal class CgNameGeneratorImpl(private val context: CgContext) } return when { baseName in existingVariableNames -> nextIndexedVarName(baseName) - isLanguageKeyword(baseName, codegenLanguage) -> createNameFromKeyword(baseName) + isLanguageKeyword(baseName, context.cgLanguageAssistant) -> createNameFromKeyword(baseName) else -> baseName }.also { existingVariableNames = existingVariableNames.add(it) 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/services/access/CgCallableAccessManager.kt similarity index 81% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/access/CgCallableAccessManager.kt index 2c73f18281..176c83977d 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/services/access/CgCallableAccessManager.kt @@ -1,44 +1,44 @@ -package org.utbot.framework.codegen.model.constructor.tree +package org.utbot.framework.codegen.services.access 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.getMethodId -import org.utbot.framework.codegen.model.constructor.builtin.getTargetException -import org.utbot.framework.codegen.model.constructor.builtin.invoke -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.context.CgContext -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner -import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManagerImpl.FieldAccessorSuitability.* -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getStatementConstructorBy -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getVariableConstructorBy -import org.utbot.framework.codegen.model.constructor.util.getAmbiguousOverloadsOf -import org.utbot.framework.codegen.model.constructor.util.importIfNeeded -import org.utbot.framework.codegen.model.constructor.util.isUtil -import org.utbot.framework.codegen.model.constructor.util.typeCast -import org.utbot.framework.codegen.model.tree.CgAllocateArray -import org.utbot.framework.codegen.model.tree.CgAssignment -import org.utbot.framework.codegen.model.tree.CgConstructorCall -import org.utbot.framework.codegen.model.tree.CgExecutableCall -import org.utbot.framework.codegen.model.tree.CgExpression -import org.utbot.framework.codegen.model.tree.CgFieldAccess -import org.utbot.framework.codegen.model.tree.CgMethodCall -import org.utbot.framework.codegen.model.tree.CgSpread -import org.utbot.framework.codegen.model.tree.CgStatement -import org.utbot.framework.codegen.model.tree.CgStaticFieldAccess -import org.utbot.framework.codegen.model.tree.CgThisInstance -import org.utbot.framework.codegen.model.tree.CgValue -import org.utbot.framework.codegen.model.tree.CgVariable -import org.utbot.framework.codegen.model.util.at -import org.utbot.framework.codegen.model.util.isAccessibleFrom -import org.utbot.framework.codegen.model.util.canBeReadFrom -import org.utbot.framework.codegen.model.util.nullLiteral -import org.utbot.framework.codegen.model.util.resolve -import org.utbot.framework.plugin.api.BuiltinConstructorId +import org.utbot.framework.codegen.domain.builtin.any +import org.utbot.framework.codegen.domain.builtin.anyOfClass +import org.utbot.framework.codegen.domain.builtin.getMethodId +import org.utbot.framework.codegen.domain.builtin.getTargetException +import org.utbot.framework.codegen.domain.builtin.invoke +import org.utbot.framework.codegen.domain.builtin.newInstance +import org.utbot.framework.codegen.domain.builtin.genericObjectClassId +import org.utbot.framework.codegen.domain.builtin.setAccessible +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgAllocateArray +import org.utbot.framework.codegen.domain.models.CgAssignment +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgFieldAccess +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgSpread +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgStaticFieldAccess +import org.utbot.framework.codegen.domain.models.CgThisInstance +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.services.access.CgCallableAccessManagerImpl.FieldAccessorSuitability.* +import org.utbot.framework.codegen.tree.CgComponents.getStatementConstructorBy +import org.utbot.framework.codegen.tree.CgComponents.getVariableConstructorBy +import org.utbot.framework.codegen.tree.downcastIfNeeded +import org.utbot.framework.codegen.tree.getAmbiguousOverloadsOf +import org.utbot.framework.codegen.tree.importIfNeeded +import org.utbot.framework.codegen.tree.isUtil +import org.utbot.framework.codegen.tree.typeCast +import org.utbot.framework.codegen.util.at +import org.utbot.framework.codegen.util.canBeReadFrom +import org.utbot.framework.codegen.util.isAccessibleFrom +import org.utbot.framework.codegen.util.nullLiteral +import org.utbot.framework.codegen.util.resolve import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.BuiltinConstructorId import org.utbot.framework.plugin.api.BuiltinMethodId import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage @@ -46,11 +46,13 @@ import org.utbot.framework.plugin.api.ConstructorId 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.UtExplicitlyThrownException -import org.utbot.framework.plugin.api.util.isStatic import org.utbot.framework.plugin.api.util.exceptions +import org.utbot.framework.plugin.api.util.extensionReceiverParameterIndex +import org.utbot.framework.plugin.api.util.humanReadableName import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.isAbstract import org.utbot.framework.plugin.api.util.isArray +import org.utbot.framework.plugin.api.util.isStatic import org.utbot.framework.plugin.api.util.isSubtypeOf import org.utbot.framework.plugin.api.util.jClass import org.utbot.framework.plugin.api.util.method @@ -83,7 +85,7 @@ interface CgCallableAccessManager { operator fun ClassId.get(fieldId: FieldId): CgStaticFieldAccess } -internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableAccessManager, +class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableAccessManager, CgContextOwner by context { private val statementConstructor by lazy { getStatementConstructorBy(context) } @@ -110,7 +112,11 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA override operator fun CgIncompleteMethodCall.invoke(vararg args: Any?): CgMethodCall { val resolvedArgs = args.resolve() val methodCall = if (method.canBeCalledWith(caller, resolvedArgs)) { - CgMethodCall(caller, method, resolvedArgs.guardedForDirectCallOf(method)) + CgMethodCall( + caller = caller?.let { downcastIfNeeded(method.classId, caller) }, + executableId = method, + arguments = resolvedArgs.guardedForDirectCallOf(method) + ).takeCallerFromArgumentsIfNeeded() } else { method.callWithReflection(caller, resolvedArgs) } @@ -156,16 +162,6 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA addExceptionIfNeeded(Throwable::class.id) } - 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 - } - methodId.method.exceptionTypes.forEach { addExceptionIfNeeded(it.id) } } @@ -187,13 +183,49 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA private infix fun CgExpression.canBeReceiverOf(executable: MethodId): Boolean = when { - // method of the current test class can be called on its 'this' instance + // this executable can be called on its 'this' instance currentTestClass == executable.classId && this isThisInstanceOf currentTestClass -> true - // method of a class can be called on an object of this class or any of its subtypes + + // this executable can be called on an object of this class or any of its subtypes this.type isSubtypeOf executable.classId -> true - else -> false + + // this executable can be called on builtin type + this.type is BuiltinClassId && this.type in builtinCallersWithoutReflection -> true + + // receiver can be downcasted before call + else -> executable.isAccessibleFrom(testClassPackageName) + } + + // For some builtin types we need to clarify + // that it is allowed to call an executable without reflection. + // + // This approach is used, for example, to render the constructions with stubs + // like `doNothing().when(entityManagerMock).persist(any())`. + private val builtinCallersWithoutReflection = setOf(genericObjectClassId) + + /** + * For Kotlin extension functions, real caller is one of the arguments in JVM method (and declaration class is omitted), + * thus we should move it from arguments to caller + * + * For example, if we have `Int.f(a: Int)` declared in `Main.kt`, the JVM method signature will be `MainKt.f(Int, Int)` + * and in Kotlin we should render this not like `MainKt.f(a, b)` but like `a.f(b)` + */ + private fun CgMethodCall.takeCallerFromArgumentsIfNeeded(): CgMethodCall { + if (codegenLanguage == CodegenLanguage.KOTLIN) { + // TODO: reflection calls for util and some of mockito methods produce exceptions => here we suppose that + // methods for BuiltinClasses are not extensions by default (which should be true as long as we suppose them to be java methods) + if (executableId.classId !is BuiltinClassId) { + executableId.extensionReceiverParameterIndex?.let { receiverIndex -> + require(caller == null) { "${executableId.humanReadableName} is an extension function but it already has a non-static caller provided" } + val args = arguments.toMutableList() + return CgMethodCall(args.removeAt(receiverIndex), executableId, args, typeParameters) + } + } } + return this + } + private infix fun CgExpression.canBeArgOf(type: ClassId): Boolean { // TODO: SAT-1210 support generics so that we wouldn't need to check specific cases such as this one if (this is CgExecutableCall && (executableId == any || executableId == anyOfClass)) { @@ -297,26 +329,19 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA return caller canBeReceiverOf this } - private fun FieldId.accessSuitability(accessor: CgExpression?): FieldAccessorSuitability { + private fun FieldId.accessSuitability(accessor: CgExpression): FieldAccessorSuitability { // Check field accessibility. - if (!canBeReadFrom(context)) { + if (!canBeReadFrom(context, accessor.type)) { return ReflectionOnly } - // If field is static, then it may not have an accessor. - if (this.isStatic && accessor == null) { - return Suitable - } - - if (this.isStatic && accessor != null && codegenLanguage == CodegenLanguage.KOTLIN) { + if (this.isStatic && codegenLanguage == CodegenLanguage.KOTLIN) { error("In Kotlin, unlike Java, static fields cannot be accessed by an object: $this") } // if field is declared in the current test class if (this.declaringClass == currentTestClass) { return when { - // field of the current class can be accessed without explicit accessor - accessor == null -> Suitable // field of the current class can be accessed with `this` reference accessor isThisInstanceOf currentTestClass -> Suitable // field of the current class can be accessed by the instance of this class @@ -326,10 +351,6 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA } } - requireNotNull(accessor) { - "Field access must have a non-null accessor, unless it is the field of the current test class or a static field: $this" - } - if (this.declaringClass == accessor.type) { // if the field was declared in class `T`, and the accessor is __exactly__ of this type (not a subtype), // then we can safely access the field @@ -468,6 +489,11 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA isAccessibleFrom(testClassPackageName) && !classId.isAbstract && args canBeArgsOf this private fun List.guardedForDirectCallOf(executable: ExecutableId): List { + if (executable is BuiltinMethodId) { + // We assume that we do not have ambiguous overloads for builtin methods + return this + } + val ambiguousOverloads = executable.classId .getAmbiguousOverloadsOf(executable) .filterNot { it == executable } @@ -597,7 +623,13 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA getStaticFieldValueMethodId, setStaticFieldMethodId -> setOf(java.lang.IllegalAccessException::class.id, java.lang.NoSuchFieldException::class.id) getFieldValueMethodId, - setFieldMethodId -> setOf(java.lang.ClassNotFoundException::class.id, java.lang.IllegalAccessException::class.id, java.lang.NoSuchFieldException::class.id) + setFieldMethodId -> setOf( + java.lang.ClassNotFoundException::class.id, + java.lang.IllegalAccessException::class.id, + java.lang.NoSuchFieldException::class.id, + java.lang.reflect.InvocationTargetException::class.id, + java.lang.NoSuchMethodException::class.id + ) createInstanceMethodId -> setOf(Exception::class.id) getUnsafeInstanceMethodId -> setOf(ClassNotFoundException::class.id, NoSuchFieldException::class.id, IllegalAccessException::class.id) createArrayMethodId -> setOf(ClassNotFoundException::class.id) @@ -608,6 +640,7 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA mapsDeepEqualsMethodId, hasCustomEqualsMethodId, getArrayLengthMethodId, + consumeBaseStreamMethodId, getLambdaCapturedArgumentTypesMethodId, getLambdaCapturedArgumentValuesMethodId, getInstantiatedMethodTypeMethodId, @@ -615,7 +648,12 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA getSingleAbstractMethodMethodId -> emptySet() buildStaticLambdaMethodId, buildLambdaMethodId -> setOf(Throwable::class.id) - getLookupInMethodId ->setOf(IllegalAccessException::class.id, NoSuchFieldException::class.id) + getLookupInMethodId -> setOf( + IllegalAccessException::class.id, + NoSuchFieldException::class.id, + java.lang.NoSuchMethodException::class.id, + java.lang.reflect.InvocationTargetException::class.id + ) else -> error("Unknown util method ${this@findExceptionTypes}") } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/access/CgFieldStateManager.kt similarity index 81% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/access/CgFieldStateManager.kt index 2bb300557b..33fea7ed75 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/access/CgFieldStateManager.kt @@ -1,26 +1,26 @@ -package org.utbot.framework.codegen.model.constructor.tree +package org.utbot.framework.codegen.services.access -import org.utbot.framework.codegen.model.constructor.builtin.forName -import org.utbot.framework.codegen.model.constructor.builtin.getArrayElement -import org.utbot.framework.codegen.model.constructor.context.CgContext -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getCallableAccessManagerBy -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getStatementConstructorBy -import org.utbot.framework.codegen.model.constructor.util.CgFieldState -import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor -import org.utbot.framework.codegen.model.constructor.util.FieldStateCache -import org.utbot.framework.codegen.model.constructor.util.classCgClassId -import org.utbot.framework.codegen.model.constructor.util.getFieldVariableName -import org.utbot.framework.codegen.model.constructor.util.getStaticFieldVariableName -import org.utbot.framework.codegen.model.constructor.util.needExpectedDeclaration -import org.utbot.framework.codegen.model.tree.CgExpression -import org.utbot.framework.codegen.model.tree.CgGetJavaClass -import org.utbot.framework.codegen.model.tree.CgValue -import org.utbot.framework.codegen.model.tree.CgVariable -import org.utbot.framework.codegen.model.util.at -import org.utbot.framework.codegen.model.util.isAccessibleFrom -import org.utbot.framework.codegen.model.util.canBeReadFrom -import org.utbot.framework.codegen.model.util.stringLiteral +import org.utbot.framework.codegen.domain.builtin.forName +import org.utbot.framework.codegen.domain.builtin.getArrayElement +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgGetJavaClass +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.tree.CgComponents.getCallableAccessManagerBy +import org.utbot.framework.codegen.tree.CgComponents.getStatementConstructorBy +import org.utbot.framework.codegen.tree.CgFieldState +import org.utbot.framework.codegen.tree.CgStatementConstructor +import org.utbot.framework.codegen.tree.FieldStateCache +import org.utbot.framework.codegen.tree.classCgClassId +import org.utbot.framework.codegen.tree.getFieldVariableName +import org.utbot.framework.codegen.tree.getStaticFieldVariableName +import org.utbot.framework.codegen.tree.needExpectedDeclaration +import org.utbot.framework.codegen.util.at +import org.utbot.framework.codegen.util.isAccessibleFrom +import org.utbot.framework.codegen.util.canBeReadFrom +import org.utbot.framework.codegen.util.stringLiteral import org.utbot.framework.fields.ArrayElementAccess import org.utbot.framework.fields.FieldAccess import org.utbot.framework.fields.FieldPath @@ -28,23 +28,19 @@ import org.utbot.framework.fields.ModifiedFields import org.utbot.framework.fields.StateModificationInfo import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.UtSymbolicExecution -import org.utbot.framework.plugin.api.util.hasField -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.isArray -import org.utbot.framework.plugin.api.util.isRefType -import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.* import org.utbot.framework.util.hasThisInstance import org.utbot.fuzzer.UtFuzzedExecution import java.lang.reflect.Array -internal interface CgFieldStateManager { +interface CgFieldStateManager { fun rememberInitialEnvironmentState(info: StateModificationInfo) fun rememberFinalEnvironmentState(info: StateModificationInfo) } internal class CgFieldStateManagerImpl(val context: CgContext) : CgContextOwner by context, - CgFieldStateManager, + CgFieldStateManager, CgCallableAccessManager by getCallableAccessManagerBy(context), CgStatementConstructor by getStatementConstructorBy(context) { @@ -141,7 +137,7 @@ internal class CgFieldStateManagerImpl(val context: CgContext) emptyLineIfNeeded() val fields = when (state) { FieldState.INITIAL -> modifiedFields - .filter { it.path.elements.isNotEmpty() && it.path.fieldType.isRefType } + .filter { it.path.elements.isNotEmpty() && !it.path.fieldType.isPrimitive } .filter { needExpectedDeclaration(it.after) } FieldState.FINAL -> modifiedFields } @@ -184,7 +180,7 @@ internal class CgFieldStateManagerImpl(val context: CgContext) for ((index, fieldPathElement) in path.withIndex()) { when (fieldPathElement) { is FieldAccess -> { - if (!fieldPathElement.field.canBeReadFrom(context)) { + if (!fieldPathElement.field.canBeReadFrom(context, curType)) { lastAccessibleIndex = index - 1 break } @@ -229,7 +225,9 @@ internal class CgFieldStateManagerImpl(val context: CgContext) if (index > path.lastIndex) return@generateSequence null val passedPath = FieldPath(path.subList(0, index + 1)) val name = if (index == path.lastIndex) customName else getFieldVariableName(prev, passedPath) - val expression = when (val newElement = path[index++]) { + + val newElement = path[index++] + val expression = when (newElement) { is FieldAccess -> { val fieldId = newElement.field utilsClassId[getFieldValue](prev, fieldId.declaringClass.name, fieldId.name) @@ -238,7 +236,7 @@ internal class CgFieldStateManagerImpl(val context: CgContext) Array::class.id[getArrayElement](prev, newElement.index) } } - newVar(objectClassId, name) { expression } + newVar(newElement.type, name) { expression } }.last() } @@ -248,7 +246,7 @@ internal class CgFieldStateManagerImpl(val context: CgContext) private fun variableForStaticFieldState(owner: ClassId, fieldPath: FieldPath, customName: String?): CgVariable { val firstField = (fieldPath.elements.first() as FieldAccess).field - val firstAccessor = if (owner.isAccessibleFrom(testClassPackageName) && firstField.canBeReadFrom(context)) { + val firstAccessor = if (owner.isAccessibleFrom(testClassPackageName) && firstField.canBeReadFrom(context, owner)) { owner[firstField] } else { // TODO: there is a function getClassOf() for these purposes, but it is not accessible from here for now diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/MockFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/MockFrameworkManager.kt new file mode 100644 index 0000000000..f6e75a5e58 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/MockFrameworkManager.kt @@ -0,0 +1,494 @@ +package org.utbot.framework.codegen.services.framework + +import org.utbot.framework.codegen.domain.MockitoStaticMocking +import org.utbot.framework.codegen.domain.NoStaticMocking +import org.utbot.framework.codegen.domain.builtin.any +import org.utbot.framework.codegen.domain.builtin.anyBoolean +import org.utbot.framework.codegen.domain.builtin.anyByte +import org.utbot.framework.codegen.domain.builtin.anyChar +import org.utbot.framework.codegen.domain.builtin.anyDouble +import org.utbot.framework.codegen.domain.builtin.anyFloat +import org.utbot.framework.codegen.domain.builtin.anyInt +import org.utbot.framework.codegen.domain.builtin.anyLong +import org.utbot.framework.codegen.domain.builtin.anyOfClass +import org.utbot.framework.codegen.domain.builtin.anyShort +import org.utbot.framework.codegen.domain.builtin.argumentMatchersClassId +import org.utbot.framework.codegen.domain.builtin.doNothingMethodId +import org.utbot.framework.codegen.domain.builtin.mockMethodId +import org.utbot.framework.codegen.domain.builtin.mockedConstructionContextClassId +import org.utbot.framework.codegen.domain.builtin.mockitoClassId +import org.utbot.framework.codegen.domain.builtin.thenReturnMethodId +import org.utbot.framework.codegen.domain.builtin.whenMethodId +import org.utbot.framework.codegen.domain.builtin.whenStubberMethodId +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgAnonymousFunction +import org.utbot.framework.codegen.domain.models.CgAssignment +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgRunnable +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgStatementExecutableCall +import org.utbot.framework.codegen.domain.models.CgStaticRunnable +import org.utbot.framework.codegen.domain.models.CgSwitchCase +import org.utbot.framework.codegen.domain.models.CgSwitchCaseLabel +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.services.CgNameGenerator +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.services.access.CgCallableAccessManagerImpl +import org.utbot.framework.codegen.tree.CgComponents.getNameGeneratorBy +import org.utbot.framework.codegen.tree.CgComponents.getVariableConstructorBy +import org.utbot.framework.codegen.tree.CgStatementConstructor +import org.utbot.framework.codegen.tree.CgStatementConstructorImpl +import org.utbot.framework.codegen.tree.CgVariableConstructor +import org.utbot.framework.codegen.tree.hasAmbiguousOverloadsOf +import org.utbot.framework.codegen.tree.importIfNeeded +import org.utbot.framework.codegen.util.isAccessibleFrom +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.TypeParameters +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation +import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation +import org.utbot.framework.plugin.api.util.atomicIntegerClassId +import org.utbot.framework.plugin.api.util.atomicIntegerGet +import org.utbot.framework.plugin.api.util.atomicIntegerGetAndIncrement +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.voidClassId +import java.util.* + +abstract class CgVariableConstructorComponent(val context: CgContext) : + CgContextOwner by context, + CgCallableAccessManager by CgCallableAccessManagerImpl(context), + CgStatementConstructor by CgStatementConstructorImpl(context) { + + val nameGenerator: CgNameGenerator = getNameGeneratorBy(context) + val variableConstructor: CgVariableConstructor by lazy { getVariableConstructorBy(context) } + + fun mockitoArgumentMatchersFor(executable: ExecutableId): Array = + executable.parameters.map { + val matcher = it.mockitoAnyMatcher(executable.classId.hasAmbiguousOverloadsOf(executable)) + if (matcher != anyOfClass) argumentMatchersClassId[matcher]() else matchByClass(it) + }.toTypedArray() + + /** + * Clears all resources required for [currentExecution]. + */ + open fun clearExecutionResources() { + // do nothing by default + } + + // TODO: implement other similar methods like thenThrow, etc. + fun CgMethodCall.thenReturn(returnType: ClassId, vararg args: CgValue) { + val castedArgs = args + // guard args to reuse typecast creation logic + .map { if (it.type == returnType) it else guardExpression(returnType, it).expression } + .toTypedArray() + + +this[thenReturnMethodId](*castedArgs) + } + + fun ClassId.mockitoAnyMatcher(withExplicitClass: Boolean): MethodId = + when (this) { + byteClassId -> anyByte + charClassId -> anyChar + shortClassId -> anyShort + intClassId -> anyInt + longClassId -> anyLong + floatClassId -> anyFloat + doubleClassId -> anyDouble + booleanClassId -> anyBoolean + // we cannot match by string here + // because anyString accepts only non-nullable strings but argument could be null + else -> if (withExplicitClass) anyOfClass else any + } + + private fun matchByClass(id: ClassId): CgMethodCall = + argumentMatchersClassId[anyOfClass](getClassOf(id)) +} + +class MockFrameworkManager(context: CgContext) : CgVariableConstructorComponent(context) { + + private val objectMocker = MockitoMocker(context) + private val staticMocker = when (context.staticsMocking) { + is NoStaticMocking -> null + is MockitoStaticMocking -> MockitoStaticMocker(context, objectMocker) + else -> null + } + + /** + * Precondition: in the given [model] flag [UtCompositeModel.isMock] must be true. + * @return a variable representing a created mock object. + */ + fun createMockFor(model: UtCompositeModel, baseName: String): CgVariable = withMockFramework { + require(model.isMock) { "Mock model is expected in MockObjectConstructor" } + + objectMocker.createMock(model, baseName) + } + + fun createMockForVariable(model: UtCompositeModel, variable: CgVariable) = + withMockFramework { + require(model.isMock) { "Mock model $model is expected in MockObjectConstructor" } + objectMocker.mockForVariable(model, variable) + } + + fun mockNewInstance(mock: UtNewInstanceInstrumentation) { + staticMocker?.mockNewInstance(mock) + } + + fun mockStaticMethodsOfClass(classId: ClassId, methodMocks: List) { + staticMocker?.mockStaticMethodsOfClass(classId, methodMocks) + } + + override fun clearExecutionResources() { + staticMocker?.clearExecutionResources() + } + + internal fun getAndClearMethodResources(): List? = + if (staticMocker is MockitoStaticMocker) staticMocker.copyAndClearMockResources() else null +} + +private abstract class ObjectMocker( + context: CgContext +) : CgVariableConstructorComponent(context) { + abstract fun createMock(model: UtCompositeModel, baseName: String): CgVariable + + abstract fun mock(clazz: CgExpression): CgMethodCall + + abstract fun `when`(call: CgExecutableCall): CgMethodCall +} + +private abstract class StaticMocker( + context: CgContext +) : CgVariableConstructorComponent(context) { + abstract fun mockNewInstance(mock: UtNewInstanceInstrumentation) + abstract fun mockStaticMethodsOfClass(classId: ClassId, methodMocks: List) +} + +private class MockitoMocker(context: CgContext) : ObjectMocker(context) { + + override fun createMock(model: UtCompositeModel, baseName: String): CgVariable { + val modelClass = getClassOf(model.classId) + val mockObject = + newVar(model.classId, model = model, baseName = baseName, isMock = true) { mock(modelClass) } + + mockForVariable(model, mockObject) + + return mockObject + } + + fun mockForVariable(model: UtCompositeModel, mockObject: CgVariable) { + for ((executable, values) in model.mocks) { + val matchers = mockitoArgumentMatchersFor(executable) + + if (executable.returnType == voidClassId) { + when (executable) { + // All constructors are considered like void methods, but it is proposed to be changed to test constructors better. + is ConstructorId -> continue + // Sometimes void methods are called explicitly, e.g. annotated with @Mock fields in Spring test classes. + // We would like to mark that this field is used and must not be removed from test class. + // Without `doNothing` call Intellij Idea suggests removing this field as unused. + is MethodId -> { + +mockitoClassId[doNothingMethodId]()[whenStubberMethodId](mockObject)[executable](*matchers) + } + else -> error("Only MethodId and ConstructorId was expected to appear in simple mocker but got $executable") + } + } else { + when (executable) { + is MethodId -> { + if (executable.parameters.any { !it.isAccessibleFrom(testClassPackageName) }) { + error("Cannot mock method $executable with not accessible parameters" ) + } + + if (!executable.isAccessibleFrom(testClassPackageName)) { + error("Cannot mock method $executable as it is not accessible from package $testClassPackageName") + } + + val results = values + .map { value -> + // Sometimes we need mocks returning itself, e.g. for StringBuilder.append method + if (value != model) variableConstructor.getOrCreateVariable(value) else mockObject + } + .toTypedArray() + `when`(mockObject[executable](*matchers)).thenReturn(executable.returnType, *results) + } + else -> error("Only MethodId was expected to appear in simple mocker but got $executable") + } + } + } + } + + override fun mock(clazz: CgExpression): CgMethodCall = + mockitoClassId[mockMethodId](clazz) + + override fun `when`(call: CgExecutableCall): CgMethodCall = + mockitoClassId[whenMethodId](call) +} + +private class MockitoStaticMocker(context: CgContext, private val mocker: ObjectMocker) : StaticMocker(context) { + private val resources = mutableListOf() + private val mockedStaticForMethods = mutableMapOf() + private val mockedStaticConstructions = mutableSetOf() + + override fun mockNewInstance(mock: UtNewInstanceInstrumentation) { + val classId = mock.classId + if (classId in mockedStaticConstructions) return + + val mockClassCounter = CgDeclaration( + atomicIntegerClassId, + nameGenerator.variableName(MOCK_CLASS_COUNTER_NAME), + CgConstructorCall(ConstructorId(atomicIntegerClassId, emptyList()), emptyList()) + ) + + val mocksExecutablesAnswers = mock + .instances + .filterIsInstance() + .filter { it.isMock } + .map { + // If there are no expected answers for the particular executable + // (this executable is never invoked during the execution, for example), + // we do not need to consider this executable at all. + it.mocks.filterTo(mutableMapOf()) { executableAnswers -> executableAnswers.value.isNotEmpty() } + } + + val modelClass = getClassOf(classId) + + val mockConstructionInitializer = mockConstruction( + modelClass, + classId, + mocksExecutablesAnswers, + mockClassCounter.variable + ) + + if (mockConstructionInitializer.isMockClassCounterRequired) { + // We should insert the counter declaration only if we use this counter, for better readability. + +mockClassCounter + } + + // TODO this behavior diverges with expected by the symbolic engine, + // see https://github.com/UnitTestBot/UTBotJava/issues/1953 for more details + val mockedConstructionDeclaration = CgDeclaration( + MockitoStaticMocking.mockedConstructionClassId, + nameGenerator.variableName(MOCKED_CONSTRUCTION_NAME), + mockConstructionInitializer.mockConstructionCall + ) + + importIfNeeded(MockitoStaticMocking.mockedConstructionClassId) + importIfNeeded(classId) + + resources += mockedConstructionDeclaration + +CgAssignment(mockedConstructionDeclaration.variable, mockConstructionInitializer.mockConstructionCall) + mockedStaticConstructions += classId + } + + override fun mockStaticMethodsOfClass(classId: ClassId, methodMocks: List) { + for ((methodId, values) in methodMocks) { + if (methodId.parameters.any { !it.isAccessibleFrom(testClassPackageName) }) { + error("Cannot mock static method $methodId with not accessible parameters" ) + } + + val matchers = mockitoArgumentMatchersFor(methodId) + val mockedStaticDeclaration = getOrCreateMockStatic(classId) + val mockedStaticVariable = mockedStaticDeclaration.variable + val methodRunnable = if (matchers.isEmpty()) { + CgStaticRunnable(type = methodId.returnType, classId, methodId) + } else { + CgAnonymousFunction( + type = methodId.returnType, + parameters = emptyList(), + listOf( + CgStatementExecutableCall( + CgMethodCall( + caller = null, + methodId, + matchers.toList() + ) + ) + ) + ) + } + // void method + if (methodId.returnType == voidClassId) { + // we do not generate additional code for void methods because they do nothing by default + continue + } + + if (!methodId.isAccessibleFrom(testClassPackageName)) { + error("Cannot mock static method $methodId as it is not accessible from package $testClassPackageName") + } + + val results = values.map { variableConstructor.getOrCreateVariable(it) }.toTypedArray() + `when`(mockedStaticVariable, methodRunnable).thenReturn(methodId.returnType, *results) + } + } + + override fun clearExecutionResources() { + resources.clear() + mockedStaticForMethods.clear() + mockedStaticConstructions.clear() + } + + private fun getOrCreateMockStatic(classId: ClassId): CgDeclaration = + mockedStaticForMethods.getOrPut(classId) { + val modelClass = getClassOf(classId) + val classMockStaticCall = mockStatic(modelClass) + val mockedStaticVariableName = nameGenerator.variableName(MOCKED_STATIC_NAME) + CgDeclaration( + MockitoStaticMocking.mockedStaticClassId, + mockedStaticVariableName, + classMockStaticCall + ).also { + resources += it + +CgAssignment(it.variable, classMockStaticCall) + + importIfNeeded(MockitoStaticMocking.mockedStaticClassId) + importIfNeeded(classId) + } + } + + private fun mockConstruction( + clazz: CgExpression, + classId: ClassId, + mocksWhenAnswers: List>>, + mockClassCounter: CgVariable + ): MockConstructionBlock { + val mockParameter = variableConstructor.declareParameter( + classId, + nameGenerator.variableName( + classId, + isMock = true + ) + ) + + val mockAnswerStatements = mutableMapOf>() + + for ((index, mockWhenAnswers) in mocksWhenAnswers.withIndex()) { + val statements = mutableListOf() + for ((executable, values) in mockWhenAnswers) { + // For now, all constructors are considered like void methods, but it is proposed to be changed + // for better constructors testing. + if (executable.returnType == voidClassId) continue + + require(values.isNotEmpty()) { + "Expected at least one mocked answer for $executable but got 0" + } + + when (executable) { + is MethodId -> { + val matchers = mockitoArgumentMatchersFor(executable) + val results = values.map { variableConstructor.getOrCreateVariable(it) }.toTypedArray() + statements += CgStatementExecutableCall( + mocker.`when`(mockParameter[executable](*matchers))[thenReturnMethodId](*results) + ) + } + is ConstructorId -> error("Expected MethodId but got ConstructorId $executable") + } + } + + mockAnswerStatements[index] = statements + } + + val answerValues = mockAnswerStatements.values.let { + val uniqueMockingStatements = it.distinct() + + // If we have only one unique mocking statement, we do not need switch-case with all statements - we can + // use only this unique statement. + if (uniqueMockingStatements.size == 1) { + uniqueMockingStatements + } else { + it + } + } + // If we have no more than one branch or all branches are empty, + // it means we do not need this switch and mock counter itself at all. + val atMostOneBranchOrAllEmpty = answerValues.size <= 1 || answerValues.all { statements -> statements.isEmpty() } + val mockConstructionBody = if (atMostOneBranchOrAllEmpty) { + answerValues.singleOrNull() ?: emptyList() + } else { + val caseLabels = mockAnswerStatements.map { (index, statements) -> + CgSwitchCaseLabel(CgLiteral(intClassId, index), statements) + } + val switchCase = CgSwitchCase(mockClassCounter[atomicIntegerGet](), caseLabels) + + listOf(switchCase, CgStatementExecutableCall(mockClassCounter[atomicIntegerGetAndIncrement]())) + } + + val contextParameter = variableConstructor.declareParameter( + mockedConstructionContextClassId, + nameGenerator.variableName("context") + ) + + val answersBlock = CgAnonymousFunction( + voidClassId, + listOf(mockParameter, contextParameter).map { CgParameterDeclaration(it, isVararg = false) }, + mockConstructionBody + ) + + importIfNeeded(mockedConstructionContextClassId) + importIfNeeded(classId) + + return MockConstructionBlock( + mockitoClassId[MockitoStaticMocking.mockConstructionMethodId](clazz, answersBlock), + !atMostOneBranchOrAllEmpty + ) + } + + /** + * Represents a body for invocation of the [MockitoStaticMocking.mockConstructionMethodId] method and information + * whether we need to use a counter for different mocking invocations + * (i.e., on each mocking we expect possibly different results). + */ + private data class MockConstructionBlock( + val mockConstructionCall: CgMethodCall, + val isMockClassCounterRequired: Boolean + ) + + private fun mockStatic(clazz: CgExpression): CgMethodCall = + mockitoClassId[MockitoStaticMocking.mockStaticMethodId](clazz) + + private fun `when`( + mockedStatic: CgVariable, + runnable: CgExpression, + ): CgMethodCall { + val typeParams = when (runnable) { + is CgRunnable, is CgAnonymousFunction -> listOf(runnable.type) + else -> error("Unsupported runnable type: $runnable") + } + return CgMethodCall( + mockedStatic, + MockitoStaticMocking.mockedStaticWhen(nullable = mockedStatic.type.isNullable), + listOf(runnable), + TypeParameters(typeParams), + ) + } + + + fun copyAndClearMockResources(): List? { + val copiedResources = resources.toList() + clearExecutionResources() + + return copiedResources.ifEmpty { null } + } + + companion object { + private const val MOCKED_CONSTRUCTION_NAME = "mockedConstruction" + private const val MOCKED_STATIC_NAME = "mockedStatic" + private const val MOCK_CLASS_COUNTER_NAME = "mockClassCounter" + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/TestFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/TestFrameworkManager.kt new file mode 100644 index 0000000000..a36ee470cf --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/TestFrameworkManager.kt @@ -0,0 +1,563 @@ +package org.utbot.framework.codegen.services.framework + +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.TestNg +import org.utbot.framework.codegen.domain.context.TestClassContext +import org.utbot.framework.codegen.domain.builtin.forName +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.* +import org.utbot.framework.codegen.domain.models.AnnotationTarget.* +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.tree.CgComponents.getCallableAccessManagerBy +import org.utbot.framework.codegen.tree.CgComponents.getStatementConstructorBy +import org.utbot.framework.codegen.tree.addToListMethodId +import org.utbot.framework.codegen.tree.argumentsClassId +import org.utbot.framework.codegen.tree.argumentsMethodId +import org.utbot.framework.codegen.tree.classCgClassId +import org.utbot.framework.codegen.tree.importIfNeeded +import org.utbot.framework.codegen.tree.setArgumentsArrayElement +import org.utbot.framework.codegen.util.isAccessibleFrom +import org.utbot.framework.codegen.util.resolve +import org.utbot.framework.codegen.util.stringLiteral +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.util.booleanArrayClassId +import org.utbot.framework.plugin.api.util.byteArrayClassId +import org.utbot.framework.plugin.api.util.charArrayClassId +import org.utbot.framework.plugin.api.util.doubleArrayClassId +import org.utbot.framework.plugin.api.util.floatArrayClassId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.intArrayClassId +import org.utbot.framework.plugin.api.util.longArrayClassId +import org.utbot.framework.plugin.api.util.shortArrayClassId +import org.utbot.framework.plugin.api.util.stringClassId +import java.util.concurrent.TimeUnit + +@Suppress("MemberVisibilityCanBePrivate") +abstract class TestFrameworkManager(val context: CgContext) + : CgContextOwner by context, + CgCallableAccessManager by getCallableAccessManagerBy(context) { + + val assertions = context.testFramework.assertionsClass + + val assertSame = context.testFramework.assertSame + + val assertEquals = context.testFramework.assertEquals + val assertFloatEquals = context.testFramework.assertFloatEquals + val assertDoubleEquals = context.testFramework.assertDoubleEquals + + val assertNull = context.testFramework.assertNull + val assertNotNull = context.testFramework.assertNotNull + val assertTrue = context.testFramework.assertTrue + val assertFalse = context.testFramework.assertFalse + + val fail = context.testFramework.fail + val kotlinFail = context.testFramework.kotlinFail + + val assertArrayEquals = context.testFramework.assertArrayEquals + val assertBooleanArrayEquals = context.testFramework.assertBooleanArrayEquals + val assertByteArrayEquals = context.testFramework.assertByteArrayEquals + val assertCharArrayEquals = context.testFramework.assertCharArrayEquals + val assertShortArrayEquals = context.testFramework.assertShortArrayEquals + val assertIntArrayEquals = context.testFramework.assertIntArrayEquals + val assertLongArrayEquals = context.testFramework.assertLongArrayEquals + val assertFloatArrayEquals = context.testFramework.assertFloatArrayEquals + val assertDoubleArrayEquals = context.testFramework.assertDoubleArrayEquals + + // Points to the class, into which data provider methods in parametrized tests should be put (current or outermost). + // It is needed, because data provider methods are static and thus may not be put into inner classes, e.g. in JUnit5 + // all data providers should be placed in the outermost class. + protected abstract val dataProviderMethodsHolder: TestClassContext + + protected val statementConstructor = getStatementConstructorBy(context) + + abstract fun addAnnotationForNestedClasses() + + /** + * Determines whether appearance of expected exception in test method breaks current test execution or not. + */ + abstract val isExpectedExceptionExecutionBreaking: Boolean + + protected open val timeoutArgumentName: String = "timeout" + + open fun assertEquals(expected: CgValue, actual: CgValue) { + +assertions[assertEquals](expected, actual) + } + + open fun assertSame(expected: CgValue, actual: CgValue) { + +assertions[assertSame](expected, actual) + } + + open fun assertFloatEquals(expected: CgExpression, actual: CgExpression, delta: Any) { + +assertions[assertFloatEquals](expected, actual, delta) + } + + open fun assertDoubleEquals(expected: CgExpression, actual: CgExpression, delta: Any) { + +assertions[assertDoubleEquals](expected, actual, delta) + } + + open fun getArrayEqualsAssertion(arrayType: ClassId, expected: CgExpression, actual: CgExpression): CgMethodCall = + when (arrayType) { + booleanArrayClassId -> assertions[assertBooleanArrayEquals](expected, actual) + byteArrayClassId -> assertions[assertByteArrayEquals](expected, actual) + charArrayClassId -> assertions[assertCharArrayEquals](expected, actual) + shortArrayClassId -> assertions[assertShortArrayEquals](expected, actual) + intArrayClassId -> assertions[assertIntArrayEquals](expected, actual) + longArrayClassId -> assertions[assertLongArrayEquals](expected, actual) + floatArrayClassId -> assertions[assertFloatArrayEquals](expected, actual) + doubleArrayClassId -> assertions[assertDoubleArrayEquals](expected, actual) + else -> assertions[assertArrayEquals](expected, actual) + } + + fun assertArrayEquals(arrayType: ClassId, expected: CgExpression, actual: CgExpression) { + +getArrayEqualsAssertion(arrayType, expected, actual) + } + + open fun getDeepEqualsAssertion(expected: CgExpression, actual: CgExpression): CgMethodCall { + requiredUtilMethods += setOf( + utilMethodProvider.deepEqualsMethodId, + utilMethodProvider.arraysDeepEqualsMethodId, + utilMethodProvider.iterablesDeepEqualsMethodId, + utilMethodProvider.streamsDeepEqualsMethodId, + utilMethodProvider.mapsDeepEqualsMethodId, + utilMethodProvider.hasCustomEqualsMethodId + ) + // TODO we cannot use common assertEquals because of using custom deepEquals + // For this reason we have to use assertTrue here + // Unfortunately, if test with assertTrue fails, it gives non informative message false != true + // Thus, we need to provide custom message to assertTrue showing compared objects correctly + // SAT-1345 + return assertions[assertTrue](utilsClassId[deepEquals](expected, actual)) + } + + @Suppress("unused") + fun assertDeepEquals(expected: CgExpression, actual: CgExpression) { + +getDeepEqualsAssertion(expected, actual) + } + + open fun getFloatArrayEqualsAssertion(expected: CgExpression, actual: CgExpression, delta: Any): CgMethodCall = + assertions[assertFloatArrayEquals](expected, actual, delta) + + fun assertFloatArrayEquals(expected: CgExpression, actual: CgExpression, delta: Any) { + +getFloatArrayEqualsAssertion(expected, actual, delta) + } + + open fun getDoubleArrayEqualsAssertion(expected: CgExpression, actual: CgExpression, delta: Any): CgMethodCall = + assertions[assertDoubleArrayEquals](expected, actual, delta) + + fun assertDoubleArrayEquals(expected: CgExpression, actual: CgExpression, delta: Any) { + +getDoubleArrayEqualsAssertion(expected, actual, delta) + } + + fun assertNull(actual: CgExpression) { + +assertions[assertNull](actual) + } + + fun assertBoolean(expected: Boolean, actual: CgExpression) { + if (expected) { + +assertions[assertTrue](actual) + } else { + +assertions[assertFalse](actual) + } + } + + fun assertBoolean(actual: CgExpression) = assertBoolean(expected = true, actual) + + fun fail(actual: CgExpression) { + // failure assertion may be implemented in different packages in Java and Kotlin + // more details at https://stackoverflow.com/questions/52967039/junit-5-assertions-fail-can-not-infer-type-in-kotlin + when (codegenLanguage) { + CodegenLanguage.JAVA -> +assertions[fail](actual) + CodegenLanguage.KOTLIN -> +assertions[kotlinFail](actual) + } + } + + // Exception expectation differs between test frameworks + // JUnit4 requires to add a specific argument to the test method annotation + // JUnit5 requires using method assertThrows() + // TestNg allows both approaches, we use similar to JUnit5 + abstract fun expectException(exception: ClassId, block: () -> Unit) + + /** + * Creates annotations for data provider method in parameterized tests + */ + abstract fun addDataProviderAnnotations(dataProviderMethodName: String) + + /** + * Creates declaration of argList collection in parameterized tests. + */ + abstract fun createArgList(length: Int): CgVariable + + abstract fun addParameterizedTestAnnotations(dataProviderMethodName: String?) + + abstract fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) + + /** + * Most frameworks don't have special timeout assertion, so only tested + * method call is generated, while timeout is set using + * [timeout argument][timeoutArgumentName] of the test annotation + * + * @see setTestExecutionTimeout + */ + open fun expectTimeout(timeoutMs: Long, block: () -> Unit): Unit = block() + + open fun setTestExecutionTimeout(timeoutMs: Long) { + val timeout = CgNamedAnnotationArgument( + name = timeoutArgumentName, + value = timeoutMs.resolve() + ) + val testAnnotation = collectedMethodAnnotations.singleOrNull { it.classId == testFramework.testAnnotationId } + + if (testAnnotation is CgMultipleArgsAnnotation) { + testAnnotation.arguments += timeout + } else { + statementConstructor.addAnnotation( + classId = testFramework.testAnnotationId, + namedArguments = listOf(timeout), + target = Method, + ) + } + } + + + /** + * Add a short test's description depending on the test framework type: + */ + abstract fun addTestDescription(description: String) + + abstract fun disableTestMethod(reason: String) + + /** + * Adds @DisplayName annotation. + * + * Should be used only with JUnit 5. + * @see issue-576 on GitHub + */ + open fun addDisplayName(name: String) { + statementConstructor.addAnnotation(Junit5.displayNameClassId, stringLiteral(name), Method) + } + + protected fun ClassId.toExceptionClass(): CgExpression = + if (isAccessibleFrom(testClassPackageName)) { + CgGetJavaClass(this) + } else { + statementConstructor.newVar(classCgClassId) { Class::class.id[forName](name) } + } + + fun addDataProvider(dataProvider: CgMethod) { + dataProviderMethodsHolder.cgDataProviderMethods += dataProvider + } +} + +internal class TestNgManager(context: CgContext) : TestFrameworkManager(context) { + override val dataProviderMethodsHolder: TestClassContext + get() = currentTestClassContext + + override fun addAnnotationForNestedClasses() { } + + override val isExpectedExceptionExecutionBreaking: Boolean = false + + override val timeoutArgumentName: String = "timeOut" + + private val assertThrows: BuiltinMethodId + get() { + require(testFramework is TestNg) { "According to settings, TestNg was expected, but got: $testFramework" } + + return testFramework.assertThrows + } + + override fun assertEquals(expected: CgValue, actual: CgValue) = super.assertEquals(actual, expected) + + override fun assertFloatEquals(expected: CgExpression, actual: CgExpression, delta: Any) = + super.assertFloatEquals(actual, expected, delta) + + override fun assertDoubleEquals(expected: CgExpression, actual: CgExpression, delta: Any) = + super.assertDoubleEquals(actual, expected, delta) + + override fun getArrayEqualsAssertion(arrayType: ClassId, expected: CgExpression, actual: CgExpression): CgMethodCall = + super.getArrayEqualsAssertion(arrayType, actual, expected) + + override fun getDeepEqualsAssertion(expected: CgExpression, actual: CgExpression): CgMethodCall = + super.getDeepEqualsAssertion(actual, expected) + + override fun getFloatArrayEqualsAssertion(expected: CgExpression, actual: CgExpression, delta: Any): CgMethodCall = + super.getFloatArrayEqualsAssertion(actual, expected, delta) + + override fun getDoubleArrayEqualsAssertion(expected: CgExpression, actual: CgExpression, delta: Any): CgMethodCall = + super.getDoubleArrayEqualsAssertion(actual, expected, delta) + + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is TestNg) { "According to settings, TestNg was expected, but got: $testFramework" } + val lambda = statementConstructor.lambda(testFramework.throwingRunnableClassId) { + runWithoutCollectingExceptions(block) + } + +assertions[assertThrows](exception.toExceptionClass(), lambda) + } + + override fun addDataProviderAnnotations(dataProviderMethodName: String) { + val nameArgument = CgNamedAnnotationArgument("name", stringLiteral(dataProviderMethodName)) + statementConstructor.addAnnotation( + classId = testFramework.methodSourceAnnotationId, + namedArguments = listOf(nameArgument), + target = Method, + ) + } + + override fun createArgList(length: Int) = + statementConstructor.newVar(testFramework.argListClassId, "argList") { + CgAllocateArray(testFramework.argListClassId, Array::class.java.id, length) + } + + override fun addParameterizedTestAnnotations(dataProviderMethodName: String?) { + val dataProviderArgument = + CgNamedAnnotationArgument("dataProvider", CgLiteral(stringClassId, dataProviderMethodName)) + statementConstructor.addAnnotation( + classId = testFramework.parameterizedTestAnnotationId, + namedArguments = listOf(dataProviderArgument), + target = Method, + ) + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) = + setArgumentsArrayElement(argsVariable, executionIndex, argsArray, statementConstructor) + + /** + * Supplements TestNG @Test annotation with a description. + * It looks like @Test(description="...") + * + * @see issue-576 on GitHub + */ + private fun addDescriptionAnnotation(description: String) { + val testAnnotation = + collectedMethodAnnotations.singleOrNull { it.classId == testFramework.testAnnotationId } + + val descriptionArgument = CgNamedAnnotationArgument("description", stringLiteral(description)) + if (testAnnotation is CgMultipleArgsAnnotation) { + testAnnotation.arguments += descriptionArgument + } else { + statementConstructor.addAnnotation( + classId = testFramework.testAnnotationId, + namedArguments = listOf(descriptionArgument), + target = Method, + ) + } + } + + override fun addTestDescription(description: String) = addDescriptionAnnotation(description) + + override fun disableTestMethod(reason: String) { + require(testFramework is TestNg) { "According to settings, TestNg was expected, but got: $testFramework" } + + val disabledAnnotationArgument = CgNamedAnnotationArgument( + name = "enabled", + value = false.resolve() + ) + + val descriptionArgumentName = "description" + val descriptionTestAnnotationArgument = CgNamedAnnotationArgument( + name = descriptionArgumentName, + value = reason.resolve() + ) + + val testAnnotation = collectedMethodAnnotations.singleOrNull { it.classId == testFramework.testAnnotationId } + if (testAnnotation is CgMultipleArgsAnnotation) { + testAnnotation.arguments += disabledAnnotationArgument + + val alreadyExistingDescriptionAnnotationArgument = testAnnotation.arguments.singleOrNull { + it.name == descriptionArgumentName + } + + // append new description to existing one + if (alreadyExistingDescriptionAnnotationArgument != null) { + val gluedDescription = with(alreadyExistingDescriptionAnnotationArgument) { + require(value is CgLiteral && value.value is String) { + "Expected description to be String literal but got ${value.type}" + } + + listOf(value.value, reason).joinToString("; ").resolve() + } + + testAnnotation.arguments.map { + if (it.name != descriptionArgumentName) return@map it + + CgNamedAnnotationArgument( + name = descriptionArgumentName, + value = gluedDescription + ) + } + } else { + testAnnotation.arguments += descriptionTestAnnotationArgument + } + } else { + statementConstructor.addAnnotation( + classId = testFramework.testAnnotationId, + namedArguments =listOf(disabledAnnotationArgument, descriptionTestAnnotationArgument), + target = Method, + ) + } + } +} + +internal class Junit4Manager(context: CgContext) : TestFrameworkManager(context) { + private val parametrizedTestsNotSupportedError: Nothing + get() = error("Parametrized tests are not supported for JUnit4") + + override val dataProviderMethodsHolder: TestClassContext + get() = parametrizedTestsNotSupportedError + + override fun addAnnotationForNestedClasses() { } + + override val isExpectedExceptionExecutionBreaking: Boolean = true + + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Junit4) { "According to settings, JUnit4 was expected, but got: $testFramework" } + + require(exception.isAccessibleFrom(testClassPackageName)) { + "Exception $exception is not accessible from package $testClassPackageName" + } + + val expected = CgNamedAnnotationArgument( + name = "expected", + value = createGetClassExpression(exception, codegenLanguage) + ) + val testAnnotation = collectedMethodAnnotations.singleOrNull { it.classId == testFramework.testAnnotationId } + if (testAnnotation is CgMultipleArgsAnnotation) { + // TODO: think about argument priorities, currently we always add on the last position + testAnnotation.arguments += expected + } else { + // TODO: what is we have `CgSingleArgAnnotation` without argument name and would like to add one more? + statementConstructor.addAnnotation( + classId = testFramework.testAnnotationId, + namedArguments = listOf(expected), + target = Method, + ) + } + block() + } + + override fun addDataProviderAnnotations(dataProviderMethodName: String) = + parametrizedTestsNotSupportedError + + override fun createArgList(length: Int) = + parametrizedTestsNotSupportedError + + override fun addParameterizedTestAnnotations(dataProviderMethodName: String?) = + parametrizedTestsNotSupportedError + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) = + parametrizedTestsNotSupportedError + + override fun addTestDescription(description: String) = Unit + + override fun disableTestMethod(reason: String) { + require(testFramework is Junit4) { "According to settings, JUnit4 was expected, but got: $testFramework" } + + val reasonArgument = CgNamedAnnotationArgument(name = "value", value = reason.resolve()) + statementConstructor.addAnnotation( + classId = testFramework.ignoreAnnotationClassId, + namedArguments = listOf(reasonArgument), + target = Method, + ) + } +} + +internal class Junit5Manager(context: CgContext) : TestFrameworkManager(context) { + override val dataProviderMethodsHolder: TestClassContext + get() = outerMostTestClassContext + + override fun addAnnotationForNestedClasses() { + require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" } + statementConstructor.addAnnotation(testFramework.nestedTestClassAnnotationId, Class) + } + + override val isExpectedExceptionExecutionBreaking: Boolean = false + + private val assertThrows: BuiltinMethodId + get() { + require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" } + return testFramework.assertThrows + } + + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" } + val lambda = statementConstructor.lambda(testFramework.executableClassId) { + runWithoutCollectingExceptions(block) + } + +assertions[assertThrows](exception.toExceptionClass(), lambda) + } + + override fun addDataProviderAnnotations(dataProviderMethodName: String) { } + + override fun createArgList(length: Int) = + statementConstructor.newVar(testFramework.argListClassId, "argList") { + val constructor = ConstructorId(testFramework.argListClassId, emptyList()) + constructor.invoke() + } + + override fun addParameterizedTestAnnotations(dataProviderMethodName: String?) { + statementConstructor.addAnnotation(testFramework.parameterizedTestAnnotationId, Method) + statementConstructor.addAnnotation( + testFramework.methodSourceAnnotationId, + "${outerMostTestClass.canonicalName}#$dataProviderMethodName", + Method, + ) + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + +argsVariable[addToListMethodId]( + argumentsClassId[argumentsMethodId](argsArray) + ) + } + + + override fun expectTimeout(timeoutMs : Long, block: () -> Unit) { + require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" } + val lambda = statementConstructor.lambda(testFramework.executableClassId) { block() } + importIfNeeded(testFramework.durationClassId) + val duration = CgMethodCall(null, testFramework.ofMillis, listOf(timeoutMs.resolve())) + +assertions[testFramework.assertTimeoutPreemptively](duration, lambda) + } + + override fun addDisplayName(name: String) { + require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" } + statementConstructor.addAnnotation(testFramework.displayNameClassId, name, Method) + } + + override fun setTestExecutionTimeout(timeoutMs: Long) { + require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" } + + importIfNeeded(testFramework.timeunitClassId) + + statementConstructor.addAnnotation( + classId = Junit5.timeoutClassId, + namedArguments = listOf( + CgNamedAnnotationArgument( + name = "value", + value = timeoutMs.resolve(), + ), + CgNamedAnnotationArgument( + name = "unit", + value = CgEnumConstantAccess(testFramework.timeunitClassId, TimeUnit.MILLISECONDS.name), + ), + ), + target = Method, + ) + } + + override fun addTestDescription(description: String) = addDisplayName(description) + + override fun disableTestMethod(reason: String) { + require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" } + + val reasonArgument = CgNamedAnnotationArgument(name = "value", value = reason.resolve()) + statementConstructor.addAnnotation( + classId = testFramework.disabledAnnotationClassId, + namedArguments = listOf(reasonArgument), + target = Method, + ) + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/CgLanguageAssistant.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/CgLanguageAssistant.kt new file mode 100644 index 0000000000..df92c641f3 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/CgLanguageAssistant.kt @@ -0,0 +1,76 @@ +package org.utbot.framework.codegen.services.language + +import org.utbot.framework.codegen.domain.context.TestClassContext +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.renderer.CgPrinter +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.codegen.renderer.CgRendererContext +import org.utbot.framework.codegen.services.CgNameGenerator +import org.utbot.framework.codegen.services.CgNameGeneratorImpl +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.services.access.CgCallableAccessManagerImpl +import org.utbot.framework.codegen.services.access.CgFieldStateManager +import org.utbot.framework.codegen.services.access.CgFieldStateManagerImpl +import org.utbot.framework.codegen.tree.CgCustomAssertConstructor +import org.utbot.framework.codegen.tree.CgMethodConstructor +import org.utbot.framework.codegen.tree.CgSimpleCustomAssertConstructor +import org.utbot.framework.codegen.tree.CgStatementConstructor +import org.utbot.framework.codegen.tree.CgStatementConstructorImpl +import org.utbot.framework.codegen.tree.CgVariableConstructor +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage + +interface CgLanguageAssistant { + + companion object { + fun getByCodegenLanguage(language: CodegenLanguage) = when (language) { + CodegenLanguage.JAVA -> JavaCgLanguageAssistant + CodegenLanguage.KOTLIN -> KotlinCgLanguageAssistant + else -> throw UnsupportedOperationException() + } + } + + val outerMostTestClassContent: TestClassContext? + + val extension: String + + val languageKeywords: Set + + fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair + + fun getNameGeneratorBy(context: CgContext): CgNameGenerator + fun getCallableAccessManagerBy(context: CgContext): CgCallableAccessManager + fun getStatementConstructorBy(context: CgContext): CgStatementConstructor + + fun getVariableConstructorBy(context: CgContext): CgVariableConstructor + + fun getMethodConstructorBy(context: CgContext): CgMethodConstructor + + fun getCustomAssertConstructorBy(context: CgContext): CgCustomAssertConstructor + + fun getCgFieldStateManager(context: CgContext): CgFieldStateManager + + fun getLanguageTestFrameworkManager(): LanguageTestFrameworkManager + fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer +} + +abstract class AbstractCgLanguageAssistant : CgLanguageAssistant { + override val outerMostTestClassContent: TestClassContext? get() = null + + override fun getNameGeneratorBy(context: CgContext): CgNameGenerator = CgNameGeneratorImpl(context) + override fun getCallableAccessManagerBy(context: CgContext): CgCallableAccessManager = + CgCallableAccessManagerImpl(context) + override fun getStatementConstructorBy(context: CgContext): CgStatementConstructor = CgStatementConstructorImpl(context) + + override fun getVariableConstructorBy(context: CgContext): CgVariableConstructor = CgVariableConstructor(context) + + override fun getMethodConstructorBy(context: CgContext): CgMethodConstructor = CgMethodConstructor(context) + override fun getCgFieldStateManager(context: CgContext): CgFieldStateManager = CgFieldStateManagerImpl(context) + + override fun getCustomAssertConstructorBy(context: CgContext): CgCustomAssertConstructor = + CgSimpleCustomAssertConstructor(context) +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/JavaCgLanguageAssistant.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/JavaCgLanguageAssistant.kt new file mode 100644 index 0000000000..dcdce897cd --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/JavaCgLanguageAssistant.kt @@ -0,0 +1,36 @@ +package org.utbot.framework.codegen.services.language + +import org.utbot.framework.codegen.renderer.CgPrinter +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.codegen.renderer.CgJavaRenderer +import org.utbot.framework.codegen.renderer.CgRendererContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.JVMTestFrameworkManager +import org.utbot.framework.plugin.api.utils.ClassNameUtils.generateTestClassName + +object JavaCgLanguageAssistant : AbstractCgLanguageAssistant() { + + override val extension: String + get() = ".java" + + override val languageKeywords: Set = setOf( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", + "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", + "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", + "throw", "throws", "transient", "try", "void", "volatile", "while", "null", "false", "true" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + return generateTestClassName(testClassCustomName, testClassPackageName, classUnderTest) + } + + override fun getLanguageTestFrameworkManager() = JVMTestFrameworkManager() + + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = + CgJavaRenderer(context, printer) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/KeywordsUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/KeywordsUtil.kt new file mode 100644 index 0000000000..10dc13e7b6 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/KeywordsUtil.kt @@ -0,0 +1,4 @@ +package org.utbot.framework.codegen.services.language + +fun isLanguageKeyword(word: String, codegenLanguageAssistant: CgLanguageAssistant): Boolean = + word in codegenLanguageAssistant.languageKeywords diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/KotlinCgLanguageAssistant.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/KotlinCgLanguageAssistant.kt new file mode 100644 index 0000000000..98a444e841 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/KotlinCgLanguageAssistant.kt @@ -0,0 +1,47 @@ +package org.utbot.framework.codegen.services.language + +import org.utbot.framework.codegen.renderer.CgPrinter +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.codegen.renderer.CgKotlinRenderer +import org.utbot.framework.codegen.renderer.CgRendererContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.JVMTestFrameworkManager +import org.utbot.framework.plugin.api.utils.ClassNameUtils.generateTestClassName + +object KotlinCgLanguageAssistant : AbstractCgLanguageAssistant() { + + override val extension: String + get() = ".kt" + + override val languageKeywords: Set = setOf( + "as", "as?", "break", "class", "continue", "do", "else", "false", "for", "fun", "if", "in", "!in", "interface", + "is", "!is", "null", "object", "package", "return", "super", "this", "throw", "true", "try", "typealias", + "typeof", "val", "var", "when", "while" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + return generateTestClassName(testClassCustomName, testClassPackageName, classUnderTest) + } + + override fun getLanguageTestFrameworkManager() = JVMTestFrameworkManager() + + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = + CgKotlinRenderer(context, printer) + + @Suppress("unused") + private val kotlinSoftKeywords = setOf( + "by", "catch", "constructor", "delegate", "dynamic", "field", "file", "finally", "get", "import", "init", + "param", "property", "receiver", "set", "setparam", "value", "where" + ) + + @Suppress("unused") + private val kotlinModifierKeywords = setOf( + "actual", "abstract", "annotation", "companion", "const", "crossinline", "data", "enum", "expect", "external", + "final", "infix", "inline", "inner", "internal", "lateinit", "noinline", "open", "operator", "out", "override", + "private", "protected", "public", "reified", "sealed", "suspend", "tailrec", "vararg" + ) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/LanguageTestFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/LanguageTestFrameworkManager.kt new file mode 100644 index 0000000000..beb6de6a41 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/language/LanguageTestFrameworkManager.kt @@ -0,0 +1,12 @@ +package org.utbot.framework.codegen.services.language + +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.services.framework.TestFrameworkManager + +abstract class LanguageTestFrameworkManager { + + open val testFrameworks: List = emptyList() + abstract fun managerByFramework(context: CgContext): TestFrameworkManager + abstract val defaultTestFramework: TestFramework +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/Builders.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/Builders.kt new file mode 100644 index 0000000000..89838ade7d --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/Builders.kt @@ -0,0 +1,235 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.Import +import org.utbot.framework.codegen.domain.models.CgAnnotation +import org.utbot.framework.codegen.domain.models.CgAssignment +import org.utbot.framework.codegen.domain.models.CgClass +import org.utbot.framework.codegen.domain.models.CgClassBody +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgDoWhileLoop +import org.utbot.framework.codegen.domain.models.CgDocumentationComment +import org.utbot.framework.codegen.domain.models.CgElement +import org.utbot.framework.codegen.domain.models.CgErrorTestMethod +import org.utbot.framework.codegen.domain.models.CgExceptionHandler +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgForEachLoop +import org.utbot.framework.codegen.domain.models.CgForLoop +import org.utbot.framework.codegen.domain.models.CgLoop +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodsCluster +import org.utbot.framework.codegen.domain.models.CgNestedClassesRegion +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.domain.models.CgReferenceExpression +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgStaticsRegion +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTestMethodType +import org.utbot.framework.codegen.domain.models.CgTryCatch +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.domain.models.CgWhileLoop +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.voidClassId + +interface CgBuilder { + fun build(): T +} + +// Code entities + +class CgClassFileBuilder : CgBuilder { + val imports: MutableList = mutableListOf() + lateinit var declaredClass: CgClass + + override fun build() = CgClassFile(imports, declaredClass) +} + +fun buildClassFile(init: CgClassFileBuilder.() -> Unit) = CgClassFileBuilder().apply(init).build() + +class CgClassBuilder : CgBuilder { + lateinit var id: ClassId + var documentation: CgDocumentationComment? = null + val annotations: MutableList = mutableListOf() + var superclass: ClassId? = null + val interfaces: MutableList = mutableListOf() + var isStatic: Boolean = false + var isNested: Boolean = false + lateinit var body: CgClassBody + + override fun build() = CgClass(id, documentation, annotations, superclass, interfaces, body, isStatic, isNested) +} + +fun buildClass(init: CgClassBuilder.() -> Unit) = CgClassBuilder().apply(init).build() + +class CgClassBodyBuilder(val classId: ClassId) : CgBuilder { + val methodRegions: MutableList = mutableListOf() + val staticDeclarationRegions: MutableList = mutableListOf() + val nestedClassRegions: MutableList> = mutableListOf() + val fields: MutableSet = mutableSetOf() + + override fun build() = CgClassBody(classId, methodRegions, staticDeclarationRegions, nestedClassRegions, fields) +} + +fun buildClassBody(classId: ClassId, init: CgClassBodyBuilder.() -> Unit) = CgClassBodyBuilder(classId).apply(init).build() +// Methods + +interface CgMethodBuilder : CgBuilder { + val name: String + val returnType: ClassId + val parameters: List + val statements: List + val exceptions: Set + val annotations: List + val documentation: CgDocumentationComment +} + +class CgTestMethodBuilder : CgMethodBuilder { + override lateinit var name: String + override val returnType: ClassId = voidClassId + override lateinit var parameters: List + override lateinit var statements: List + override val exceptions: MutableSet = mutableSetOf() + override val annotations: MutableList = mutableListOf() + lateinit var methodType: CgTestMethodType + override var documentation: CgDocumentationComment = CgDocumentationComment(emptyList()) + + override fun build() = CgTestMethod( + name, + returnType, + parameters, + statements, + exceptions, + annotations, + type = methodType, + documentation = documentation, + ) +} + +fun buildTestMethod(init: CgTestMethodBuilder.() -> Unit) = CgTestMethodBuilder().apply(init).build() + +class CgErrorTestMethodBuilder : CgMethodBuilder { + override lateinit var name: String + override val returnType: ClassId = voidClassId + override val parameters: List = emptyList() + override lateinit var statements: List + override val exceptions: Set = emptySet() + override val annotations: List = emptyList() + override var documentation: CgDocumentationComment = CgDocumentationComment(emptyList()) + + override fun build() = CgErrorTestMethod(name, statements, documentation) +} + +fun buildErrorTestMethod(init: CgErrorTestMethodBuilder.() -> Unit) = CgErrorTestMethodBuilder().apply(init).build() + +class CgParameterizedTestDataProviderBuilder : CgMethodBuilder { + override lateinit var name: String + + override lateinit var returnType: ClassId + override val parameters: List = mutableListOf() + override lateinit var statements: List + override val annotations: MutableList = mutableListOf() + override val exceptions: MutableSet = mutableSetOf() + override var documentation: CgDocumentationComment = CgDocumentationComment(emptyList()) + + override fun build() = CgParameterizedTestDataProviderMethod(name, statements, returnType, annotations, exceptions) +} + +fun buildParameterizedTestDataProviderMethod( + init: CgParameterizedTestDataProviderBuilder.() -> Unit +) = CgParameterizedTestDataProviderBuilder().apply(init).build() + +// Variable declaration + +class CgDeclarationBuilder : CgBuilder { + lateinit var variableType: ClassId + lateinit var variableName: String + var initializer: CgExpression? = null + var isMutable: Boolean = false + + override fun build() = CgDeclaration(variableType, variableName, initializer, isMutable) +} + +fun buildDeclaration(init: CgDeclarationBuilder.() -> Unit) = CgDeclarationBuilder().apply(init).build() + +// Variable assignment +class CgAssignmentBuilder : CgBuilder { + lateinit var lValue: CgExpression + lateinit var rValue: CgExpression + + override fun build() = CgAssignment(lValue, rValue) +} + +fun buildAssignment(init: CgAssignmentBuilder.() -> Unit) = CgAssignmentBuilder().apply(init).build() + +class CgTryCatchBuilder : CgBuilder { + lateinit var statements: List + private val handlers: MutableList = mutableListOf() + var finally: List? = null + var resources: List? = null + + override fun build(): CgTryCatch = CgTryCatch(statements, handlers, finally, resources) +} + +fun buildTryCatch(init: CgTryCatchBuilder.() -> Unit): CgTryCatch = CgTryCatchBuilder().apply(init).build() + +// Loops +interface CgLoopBuilder : CgBuilder { + val condition: CgExpression + val statements: List +} + +class CgForLoopBuilder : CgLoopBuilder { + lateinit var initialization: CgDeclaration + override lateinit var condition: CgExpression + lateinit var update: CgStatement + override lateinit var statements: List + + override fun build() = CgForLoop(initialization, condition, update, statements) +} + +fun buildForLoop(init: CgForLoopBuilder.() -> Unit) = CgForLoopBuilder().apply(init).build() + +class CgWhileLoopBuilder : CgLoopBuilder { + override lateinit var condition: CgExpression + override val statements: MutableList = mutableListOf() + + override fun build() = CgWhileLoop(condition, statements) +} + +fun buildWhileLoop(init: CgWhileLoopBuilder.() -> Unit) = CgWhileLoopBuilder().apply(init).build() + +class CgDoWhileLoopBuilder : CgLoopBuilder { + override lateinit var condition: CgExpression + override val statements: MutableList = mutableListOf() + + override fun build() = CgDoWhileLoop(condition, statements) +} + +fun buildDoWhileLoop(init: CgDoWhileLoopBuilder.() -> Unit) = CgDoWhileLoopBuilder().apply(init).build() + +class CgForEachLoopBuilder : CgLoopBuilder { + override lateinit var condition: CgExpression + override lateinit var statements: List + lateinit var iterable: CgReferenceExpression + + override fun build(): CgForEachLoop = CgForEachLoop(condition, statements, iterable) +} + +fun buildCgForEachLoop(init: CgForEachLoopBuilder.() -> Unit): CgForEachLoop = + CgForEachLoopBuilder().apply(init).build() + +class CgExceptionHandlerBuilder { + lateinit var exception: CgVariable + lateinit var statements: List + + fun build(): CgExceptionHandler { + return CgExceptionHandler(exception, statements) + } +} + +fun buildExceptionHandler(init: CgExceptionHandlerBuilder.() -> Unit): CgExceptionHandler { + return CgExceptionHandlerBuilder().apply(init).build() +} + diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractTestClassConstructor.kt new file mode 100644 index 0000000000..11ec2c751d --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractTestClassConstructor.kt @@ -0,0 +1,225 @@ +package org.utbot.framework.codegen.tree + +import mu.KotlinLogging +import org.utbot.framework.UtSettings +import org.utbot.framework.codegen.domain.builtin.TestClassUtilMethodProvider +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgAuxiliaryClass +import org.utbot.framework.codegen.domain.models.CgClass +import org.utbot.framework.codegen.domain.models.CgClassBody +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.CgRegion +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTestMethodCluster +import org.utbot.framework.codegen.domain.models.CgTripleSlashMultilineComment +import org.utbot.framework.codegen.domain.models.CgUtilEntity +import org.utbot.framework.codegen.domain.models.CgUtilMethod +import org.utbot.framework.codegen.domain.models.TestClassModel +import org.utbot.framework.codegen.renderer.importUtilMethodDependencies +import org.utbot.framework.codegen.reports.TestsGenerationReport +import org.utbot.framework.codegen.services.CgNameGenerator +import org.utbot.framework.codegen.services.framework.TestFrameworkManager +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.MethodId +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.UtMethodTestSet +import org.utbot.framework.plugin.api.util.description +import org.utbot.framework.util.calculateSize + +abstract class CgAbstractTestClassConstructor(val context: CgContext): + CgContextOwner by context, + CgStatementConstructor by CgComponents.getStatementConstructorBy(context) { + + companion object { + private val logger = KotlinLogging.logger {} + } + + init { + CgComponents.clearContextRelatedStorage() + } + + val testsGenerationReport = TestsGenerationReport() + + protected val methodConstructor: CgMethodConstructor = CgComponents.getMethodConstructorBy(context) + protected val nameGenerator: CgNameGenerator = CgComponents.getNameGeneratorBy(context) + protected val testFrameworkManager: TestFrameworkManager = CgComponents.getTestFrameworkManagerBy(context) + + /** + * Constructs a file with the test class corresponding to [TestClassModel]. + */ + open fun construct(testClassModel: T): CgClassFile { + return buildClassFile { + this.declaredClass = withTestClassScope { constructTestClass(testClassModel) } + imports += context.collectedImports + } + } + + /** + * Constructs [CgClass] corresponding to [TestClassModel]. + */ + open fun constructTestClass(testClassModel: T): CgClass { + return buildClass { + id = currentTestClass + + if (currentTestClass != outerMostTestClass) { + isNested = true + isStatic = testFramework.nestedClassesShouldBeStatic + testFrameworkManager.addAnnotationForNestedClasses() + } + + body = constructTestClassBody(testClassModel) + + // It is important that annotations, superclass and interfaces assignment is run after + // all methods are generated so that all necessary info is already present in the context + with (currentTestClassContext) { + annotations += collectedTestClassAnnotations + superclass = testClassSuperclass + interfaces += collectedTestClassInterfaces + } + } + } + + abstract fun constructTestClassBody(testClassModel: T): CgClassBody + + abstract fun constructTestSet(testSet: CgMethodTestSet): List>? + + protected fun createTest( + testSet: CgMethodTestSet, + regions: MutableList> + ) { + val (_, _, clustersInfo) = testSet + + // `stateAfter` is not accounted here, because usually most of it is not rendered + val executionToSizeCache = testSet.executions.associateWith { execution -> + execution.stateBefore.calculateSize() + + ((execution.result as? UtExecutionSuccess)?.model?.calculateSize() ?: 1) + } + + for ((clusterSummary, executionIndices) in clustersInfo) { + val currentTestCaseTestMethods = mutableListOf() + emptyLineIfNeeded() + val (checkedRange, needLimitExceedingComments) = if (executionIndices.last - executionIndices.first >= UtSettings.maxTestsPerMethodInRegion) { + IntRange(executionIndices.first, executionIndices.first + (UtSettings.maxTestsPerMethodInRegion - 1).coerceAtLeast(0)) to true + } else { + executionIndices to false + } + + testSet.executions + .withIndex() + .toList() + .slice(checkedRange) + .sortedWith( + // NPE tests are rendered last, because oftentimes they are meaningless in a sense + // that they pass `null` somewhere where `null` is never passed in production + compareBy> { (_, execution) -> + if ((execution.result as? UtExecutionFailure)?.exception is NullPointerException) 1 else -1 + } + // we place "smaller" tests earlier, since they are easier to read + .thenComparingInt { (_, execution) -> executionToSizeCache.getValue(execution) } + ).forEach { (i, execution) -> + withExecutionIdScope(i) { + currentTestCaseTestMethods += methodConstructor.createTestMethod(testSet, execution) + } + } + + val comments = listOf("Actual number of generated tests (${executionIndices.last - executionIndices.first}) exceeds per-method limit (${UtSettings.maxTestsPerMethodInRegion})", + "The limit can be configured in '{HOME_DIR}/.utbot/settings.properties' with 'maxTestsPerMethod' property") + + val clusterHeader = clusterSummary?.header + var clusterContent = clusterSummary?.content + ?.split('\n') + ?.let { CgTripleSlashMultilineComment(if (needLimitExceedingComments) {it.toMutableList() + comments} else {it}) } + if (clusterContent == null && needLimitExceedingComments) { + clusterContent = CgTripleSlashMultilineComment(comments) + } + regions += CgTestMethodCluster(clusterHeader, clusterContent, currentTestCaseTestMethods) + + testsGenerationReport.addTestsByType(testSet, currentTestCaseTestMethods) + } + } + + protected fun processFailure(testSet: CgMethodTestSet, failure: Throwable) { + logger.warn(failure) { "Code generation error" } + codeGenerationErrors + .getOrPut(testSet) { mutableMapOf() } + .merge(failure.description, 1, Int::plus) + } + + /** + * This method collects a list of util entities (methods and classes) needed by the class. + * By the end of the test method generation [requiredUtilMethods] may not contain all the needed. + * That's because some util methods may not be directly used in tests, but they may be used from other util methods. + * We define such method dependencies in [MethodId.methodDependencies]. + * + * Once all dependencies are collected, required methods are added back to [requiredUtilMethods], + * because during the work of this method they are being removed from this list, so we have to put them back in. + * + * Also, some util methods may use some classes that also need to be generated. + * That is why we collect information about required classes using [MethodId.classDependencies]. + * + * @return a list of [CgUtilEntity] representing required util methods and classes (including their own dependencies). + */ + protected fun collectUtilEntities(): List { + val utilMethods = mutableListOf() + // Some util methods depend on other util methods or some auxiliary classes. + // Using this loop we make sure that all the util method dependencies are taken into account. + val requiredClasses = mutableSetOf() + while (requiredUtilMethods.isNotEmpty()) { + val method = requiredUtilMethods.first() + requiredUtilMethods.remove(method) + if (method.name !in existingMethodNames) { + utilMethods += CgUtilMethod(method) + // we only need imports from util methods if these util methods are declared in the test class + if (utilMethodProvider is TestClassUtilMethodProvider) { + importUtilMethodDependencies(method) + } + existingMethodNames += method.name + requiredUtilMethods += method.methodDependencies() + requiredClasses += method.classDependencies() + } + } + // Collect all util methods back into requiredUtilMethods. + // Now there will also be util methods that weren't present in requiredUtilMethods at first, + // but were needed for the present util methods to work. + requiredUtilMethods += utilMethods.map { method -> method.id } + + val auxiliaryClasses = requiredClasses.map { CgAuxiliaryClass(it) } + + return utilMethods + auxiliaryClasses + } + + /** + * Engine errors + codegen errors for a given [UtMethodTestSet] + */ + protected val CgMethodTestSet.allErrors: Map + get() = errors + codeGenerationErrors.getOrDefault(this, mapOf()) + + /** + * If @receiver is an util method, then returns a list of util method ids that @receiver depends on + * Otherwise, an empty list is returned + */ + private fun MethodId.methodDependencies(): List = when (this) { + createInstance -> listOf(getUnsafeInstance) + deepEquals -> listOf(arraysDeepEquals, iterablesDeepEquals, streamsDeepEquals, mapsDeepEquals, hasCustomEquals) + arraysDeepEquals, iterablesDeepEquals, streamsDeepEquals, mapsDeepEquals -> listOf(deepEquals) + buildLambda, buildStaticLambda -> listOf( + getLookupIn, getSingleAbstractMethod, getLambdaMethod, + getLambdaCapturedArgumentTypes, getInstantiatedMethodType, getLambdaCapturedArgumentValues + ) + else -> emptyList() + } + + /** + * If @receiver is an util method, then returns a list of auxiliary class ids that @receiver depends on. + * Otherwise, an empty list is returned. + */ + private fun MethodId.classDependencies(): List = when (this) { + buildLambda, buildStaticLambda -> listOf(capturedArgumentClass) + else -> emptyList() + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgComponents.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgComponents.kt new file mode 100644 index 0000000000..5b9876f1ef --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgComponents.kt @@ -0,0 +1,71 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.services.CgNameGenerator +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.services.framework.MockFrameworkManager +import org.utbot.framework.codegen.services.framework.TestFrameworkManager +import java.util.* + +object CgComponents { + + /** + * Clears all stored data for current [CgContext]. + * As far as context is created per class under test, + * no related data is required after it's processing. + */ + fun clearContextRelatedStorage() { + nameGenerators.clear() + statementConstructors.clear() + callableAccessManagers.clear() + testFrameworkManagers.clear() + mockFrameworkManagers.clear() + variableConstructors.clear() + methodConstructors.clear() + customAssertConstructors.clear() + } + + private val nameGenerators: IdentityHashMap = IdentityHashMap() + + private val callableAccessManagers: IdentityHashMap = IdentityHashMap() + private val testFrameworkManagers: IdentityHashMap = IdentityHashMap() + private val mockFrameworkManagers: IdentityHashMap = IdentityHashMap() + + private val statementConstructors: IdentityHashMap = IdentityHashMap() + private val variableConstructors: IdentityHashMap = IdentityHashMap() + private val methodConstructors: IdentityHashMap = IdentityHashMap() + private val customAssertConstructors: IdentityHashMap = IdentityHashMap() + + fun getNameGeneratorBy(context: CgContext): CgNameGenerator = nameGenerators.getOrPut(context) { + context.cgLanguageAssistant.getNameGeneratorBy(context) + } + + fun getCallableAccessManagerBy(context: CgContext): CgCallableAccessManager = callableAccessManagers.getOrPut(context) { + context.cgLanguageAssistant.getCallableAccessManagerBy(context) + } + + fun getStatementConstructorBy(context: CgContext): CgStatementConstructor = statementConstructors.getOrPut(context) { + context.cgLanguageAssistant.getStatementConstructorBy(context) + } + + fun getTestFrameworkManagerBy(context: CgContext): TestFrameworkManager = + testFrameworkManagers.getOrDefault( + context, + context.cgLanguageAssistant.getLanguageTestFrameworkManager().managerByFramework(context) + ) + + fun getMockFrameworkManagerBy(context: CgContext): MockFrameworkManager = + mockFrameworkManagers.getOrPut(context) { MockFrameworkManager(context) } + + fun getVariableConstructorBy(context: CgContext): CgVariableConstructor = variableConstructors.getOrPut(context) { + context.cgLanguageAssistant.getVariableConstructorBy(context) + } + + fun getMethodConstructorBy(context: CgContext): CgMethodConstructor = methodConstructors.getOrPut(context) { + context.cgLanguageAssistant.getMethodConstructorBy(context) + } + + fun getCustomAssertConstructorBy(context: CgContext): CgCustomAssertConstructor = customAssertConstructors.getOrPut(context) { + context.cgLanguageAssistant.getCustomAssertConstructorBy(context) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgCustomAssertConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgCustomAssertConstructor.kt new file mode 100644 index 0000000000..86dc612905 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgCustomAssertConstructor.kt @@ -0,0 +1,19 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.plugin.api.UtCustomModel + +interface CgCustomAssertConstructor { + val context: CgContext + fun tryConstructCustomAssert(expected: UtCustomModel, actual: CgVariable): Boolean +} + +class CgSimpleCustomAssertConstructor( + override val context: CgContext +) : CgCustomAssertConstructor, + CgContextOwner by context { + override fun tryConstructCustomAssert(expected: UtCustomModel, actual: CgVariable): Boolean = + false +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt new file mode 100644 index 0000000000..fb1c7acb62 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt @@ -0,0 +1,2064 @@ +package org.utbot.framework.codegen.tree + +import mu.KotlinLogging +import org.utbot.common.WorkaroundReason +import org.utbot.common.isStatic +import org.utbot.common.workaround +import org.utbot.framework.UtSettings +import org.utbot.framework.assemble.assemble +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour.PASS +import org.utbot.framework.codegen.domain.builtin.closeMethodIdOrNull +import org.utbot.framework.codegen.domain.builtin.forName +import org.utbot.framework.codegen.domain.builtin.getClass +import org.utbot.framework.codegen.domain.builtin.getTargetException +import org.utbot.framework.codegen.domain.builtin.invoke +import org.utbot.framework.codegen.domain.builtin.newInstance +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.AnnotationTarget +import org.utbot.framework.codegen.domain.models.CgAllocateArray +import org.utbot.framework.codegen.domain.models.CgArrayElementAccess +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgDocumentationComment +import org.utbot.framework.codegen.domain.models.CgEqualTo +import org.utbot.framework.codegen.domain.models.CgErrorTestMethod +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgFieldAccess +import org.utbot.framework.codegen.domain.models.CgGetJavaClass +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.CgMultilineComment +import org.utbot.framework.codegen.domain.models.CgNotNullAssertion +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgParameterKind +import org.utbot.framework.codegen.domain.models.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.domain.models.CgRegion +import org.utbot.framework.codegen.domain.models.CgSimpleRegion +import org.utbot.framework.codegen.domain.models.CgSingleLineComment +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgStaticFieldAccess +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTestMethodType +import org.utbot.framework.codegen.domain.models.CgTestMethodType.ARTIFICIAL +import org.utbot.framework.codegen.domain.models.CgTestMethodType.CRASH +import org.utbot.framework.codegen.domain.models.CgTestMethodType.FAILING +import org.utbot.framework.codegen.domain.models.CgTestMethodType.PARAMETRIZED +import org.utbot.framework.codegen.domain.models.CgTestMethodType.PASSED_EXCEPTION +import org.utbot.framework.codegen.domain.models.CgTestMethodType.SUCCESSFUL +import org.utbot.framework.codegen.domain.models.CgTestMethodType.TIMEOUT +import org.utbot.framework.codegen.domain.models.CgTryCatch +import org.utbot.framework.codegen.domain.models.CgTypeCast +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.domain.models.convertDocToCg +import org.utbot.framework.codegen.domain.models.toStatement +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.services.access.CgFieldStateManagerImpl +import org.utbot.framework.codegen.services.framework.TestFrameworkManager +import org.utbot.framework.codegen.tree.CgComponents.getCallableAccessManagerBy +import org.utbot.framework.codegen.tree.CgComponents.getCustomAssertConstructorBy +import org.utbot.framework.codegen.tree.CgComponents.getMockFrameworkManagerBy +import org.utbot.framework.codegen.tree.CgComponents.getNameGeneratorBy +import org.utbot.framework.codegen.tree.CgComponents.getStatementConstructorBy +import org.utbot.framework.codegen.tree.CgComponents.getTestFrameworkManagerBy +import org.utbot.framework.codegen.tree.CgComponents.getVariableConstructorBy +import org.utbot.framework.codegen.util.canBeReadFrom +import org.utbot.framework.codegen.util.canBeReadViaGetterFrom +import org.utbot.framework.codegen.util.canBeSetFrom +import org.utbot.framework.codegen.util.equalTo +import org.utbot.framework.codegen.util.escapeControlChars +import org.utbot.framework.codegen.util.getter +import org.utbot.framework.codegen.util.inc +import org.utbot.framework.codegen.util.length +import org.utbot.framework.codegen.util.lessThan +import org.utbot.framework.codegen.util.nullLiteral +import org.utbot.framework.codegen.util.resolve +import org.utbot.framework.fields.ExecutionStateAnalyzer +import org.utbot.framework.fields.FieldPath +import org.utbot.framework.plugin.api.ArtificialError +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.CgClassId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.InstrumentedProcessDeathException +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.framework.plugin.api.TypeParameters +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtClassRefModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtConcreteExecutionFailure +import org.utbot.framework.plugin.api.UtCustomModel +import org.utbot.framework.plugin.api.UtDirectSetFieldModel +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.UtExecutionResult +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtExecutionWithInstrumentation +import org.utbot.framework.plugin.api.UtExplicitlyThrownException +import org.utbot.framework.plugin.api.UtLambdaModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtModelWithCompositeOrigin +import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtOverflowFailure +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.UtReferenceModel +import org.utbot.framework.plugin.api.UtSandboxFailure +import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation +import org.utbot.framework.plugin.api.UtStreamConsumingFailure +import org.utbot.framework.plugin.api.UtTaintAnalysisFailure +import org.utbot.framework.plugin.api.UtTimeoutException +import org.utbot.framework.plugin.api.UtVoidModel +import org.utbot.framework.plugin.api.isMockModel +import org.utbot.framework.plugin.api.isNotNull +import org.utbot.framework.plugin.api.isNull +import org.utbot.framework.plugin.api.onFailure +import org.utbot.framework.plugin.api.onSuccess +import org.utbot.framework.plugin.api.util.IndentUtil.TAB +import org.utbot.framework.plugin.api.util.allSuperTypes +import org.utbot.framework.plugin.api.util.baseStreamClassId +import org.utbot.framework.plugin.api.util.doubleArrayClassId +import org.utbot.framework.plugin.api.util.doubleClassId +import org.utbot.framework.plugin.api.util.doubleStreamClassId +import org.utbot.framework.plugin.api.util.doubleStreamToArrayMethodId +import org.utbot.framework.plugin.api.util.doubleWrapperClassId +import org.utbot.framework.plugin.api.util.executable +import org.utbot.framework.plugin.api.util.floatArrayClassId +import org.utbot.framework.plugin.api.util.floatClassId +import org.utbot.framework.plugin.api.util.floatWrapperClassId +import org.utbot.framework.plugin.api.util.hasField +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.intStreamClassId +import org.utbot.framework.plugin.api.util.intStreamToArrayMethodId +import org.utbot.framework.plugin.api.util.isArray +import org.utbot.framework.plugin.api.util.isInaccessibleViaReflection +import org.utbot.framework.plugin.api.util.isInnerClassEnclosingClassReference +import org.utbot.framework.plugin.api.util.isIterableOrMap +import org.utbot.framework.plugin.api.util.isPrimitive +import org.utbot.framework.plugin.api.util.isPrimitiveArray +import org.utbot.framework.plugin.api.util.isPrimitiveWrapper +import org.utbot.framework.plugin.api.util.isRefType +import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.framework.plugin.api.util.isSubtypeOf +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.jField +import org.utbot.framework.plugin.api.util.kClass +import org.utbot.framework.plugin.api.util.longStreamClassId +import org.utbot.framework.plugin.api.util.longStreamToArrayMethodId +import org.utbot.framework.plugin.api.util.objectArrayClassId +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.streamClassId +import org.utbot.framework.plugin.api.util.streamToArrayMethodId +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.framework.plugin.api.util.wrapIfPrimitive +import org.utbot.framework.util.isUnit +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.ParameterizedType +import java.security.AccessControlException + +private const val DEEP_EQUALS_MAX_DEPTH = 5 // TODO move it to plugin settings? + +open class CgMethodConstructor(val context: CgContext) : CgContextOwner by context, + CgCallableAccessManager by getCallableAccessManagerBy(context), + CgStatementConstructor by getStatementConstructorBy(context) { + + companion object { + private val logger = KotlinLogging.logger {} + } + + protected val nameGenerator = getNameGeneratorBy(context) + protected val testFrameworkManager = getTestFrameworkManagerBy(context) + + protected val variableConstructor = getVariableConstructorBy(context) + private val customAssertConstructor = getCustomAssertConstructorBy(context) + private val mockFrameworkManager = getMockFrameworkManagerBy(context) + + private val floatDelta: Float = 1e-6f + private val doubleDelta = 1e-6 + + // a model for execution result (it is lateinit because execution can fail, + // and we need it only on assertions generation stage + lateinit var resultModel: UtModel + + lateinit var methodType: CgTestMethodType + + private val fieldsOfExecutionResults = mutableMapOf, MutableList>() + + /** + * Contains whether [UtStreamConsumingFailure] is in [CgMethodTestSet] for parametrized tests. + * See [WorkaroundReason.CONSUME_DIRTY_STREAMS]. + */ + private var containsStreamConsumingFailureForParametrizedTests: Boolean = false + + protected fun setupInstrumentation() { + val instrumentation = when (val execution = currentExecution) { + is UtExecutionWithInstrumentation -> execution.instrumentation + else -> return + } + if (instrumentation.isEmpty()) return + + if (generateWarningsForStaticMocking && forceStaticMocking == ForceStaticMocking.DO_NOT_FORCE) { + // warn user about possible flaky tests + multilineComment(forceStaticMocking.warningMessage) + return + } + + instrumentation + .filterIsInstance() + .forEach { mockFrameworkManager.mockNewInstance(it) } + instrumentation + .filterIsInstance() + .groupBy { it.methodId.classId } + .forEach { (classId, methodMocks) -> mockFrameworkManager.mockStaticMethodsOfClass(classId, methodMocks) } + + if (generateWarningsForStaticMocking && forceStaticMocking == ForceStaticMocking.FORCE) { + // warn user about forced using static mocks + multilineComment(forceStaticMocking.warningMessage) + } + } + + /** + * Create variables for initial values of the static fields and store them in [prevStaticFieldValues] + * in order to use these variables at the end of the test to restore the initial static fields state. + * + * Note: + * Later in the test method we also cache the 'before' and 'after' states of fields (including static fields). + * This cache is stored in [statesCache]. + * + * However, it is _not_ the same cache as [prevStaticFieldValues]. + * The [statesCache] should not be confused with [prevStaticFieldValues] cache. + * + * The difference is that [prevStaticFieldValues] contains the static field states before we made _any_ changes. + * On the other hand, [statesCache] contains 'before' and 'after' states where the 'before' state is + * the state that we _specifically_ set up in order to cover a certain branch. + * + * Thus, this method only caches an actual initial static fields state in order to recover it + * at the end of the test, and it has nothing to do with the 'before' and 'after' caches. + */ + protected fun rememberInitialStaticFields(statics: Map) { + val accessibleStaticFields = statics.accessibleFields() + for ((field, _) in accessibleStaticFields) { + val declaringClass = field.declaringClass + val fieldAccessible = field.canBeReadFrom(context, declaringClass) + + // prevValue is nullable if not accessible because of getStaticFieldValue(..) : Any? + val prevValue = newVar( + CgClassId(field.type, isNullable = !fieldAccessible), + "prev${field.name.capitalize()}" + ) { + if (fieldAccessible) { + declaringClass[field] + } else { + val declaringClassVar = newVar(classCgClassId) { + Class::class.id[forName](declaringClass.name) + } + utilsClassId[getStaticFieldValue](declaringClassVar, field.name) + } + } + // remember the previous value of a static field to recover it at the end of the test + prevStaticFieldValues[field] = prevValue + } + } + + protected fun substituteStaticFields(statics: Map, isParametrized: Boolean = false) { + val accessibleStaticFields = statics.accessibleFields() + for ((field, model) in accessibleStaticFields) { + val declaringClass = field.declaringClass + val fieldAccessible = field.canBeSetFrom(context, declaringClass) + + val fieldValue = if (isParametrized) { + currentMethodParameters[CgParameterKind.Statics(model)] + } else { + variableConstructor.getOrCreateVariable(model, field.name) + } + + if (fieldAccessible) { + declaringClass[field] `=` fieldValue + } else { + val declaringClassVar = newVar(classCgClassId) { + Class::class.id[forName](declaringClass.name) + } + +utilsClassId[setStaticField](declaringClassVar, field.name, fieldValue) + } + } + } + + protected fun recoverStaticFields() { + for ((field, prevValue) in prevStaticFieldValues.accessibleFields()) { + if (field.canBeSetFrom(context, field.declaringClass)) { + field.declaringClass[field] `=` prevValue + } else { + val declaringClass = getClassOf(field.declaringClass) + +utilsClassId[setStaticField](declaringClass, field.name, prevValue) + } + } + } + + private fun Map.accessibleFields(): Map = filterKeys { !it.isInaccessibleViaReflection } + + /** + * Generates result assertions for unit tests. + */ + protected open fun generateResultAssertions() { + when (val executable = currentExecutableToCall) { + is ConstructorId -> generateConstructorCall(executable, currentExecution!!) + is BuiltinMethodId -> error("Unexpected BuiltinMethodId $executable while generating result assertions") + is MethodId -> { + emptyLineIfNeeded() + val currentExecution = currentExecution!! + val executionResult = currentExecution.result + + // build assertions + executionResult + .onSuccess { resultModel -> + methodType = SUCCESSFUL + + // TODO possible engine bug - void method return type and result model not UtVoidModel + if (resultModel.isUnit() || executable.returnType == voidClassId) { + +thisInstance[executable](*methodArguments.toTypedArray()) + } else { + this.resultModel = resultModel + assertEquality(resultModel, actual, emptyLineIfNeeded = true) + } + } + .onFailure { exception -> processExecutionFailure(exception, executionResult) } + } + else -> {} // TODO: check this specific case + } + } + + private fun processExecutionFailure(exceptionFromAnalysis: Throwable, executionResult: UtExecutionResult) { + val (methodInvocationBlock, expectedException) = constructExceptionProducingBlock(exceptionFromAnalysis, executionResult) + + when (methodType) { + SUCCESSFUL -> error("Unexpected successful without exception method type for execution with exception $expectedException") + PASSED_EXCEPTION -> { + // TODO consider rendering message in a comment + // expectedException.message?.let { +comment(it.escapeControlChars()) } + testFrameworkManager.expectException(expectedException::class.id) { + methodInvocationBlock() + } + } + TIMEOUT -> { + writeWarningAboutTimeoutExceeding() + testFrameworkManager.expectTimeout(hangingTestsTimeout.timeoutMs) { + methodInvocationBlock() + } + } + CRASH -> when (expectedException) { + is InstrumentedProcessDeathException -> { + writeWarningAboutCrash() + methodInvocationBlock() + } + is AccessControlException -> { + // exception from sandbox + writeWarningAboutFailureTest(expectedException) + } + else -> error("Unexpected crash suite for failing execution with $expectedException exception") + } + ARTIFICIAL -> { + methodInvocationBlock() + + val failureMessage = prepareArtificialFailureMessage(executionResult) + testFrameworkManager.fail(failureMessage) + } + FAILING -> { + writeWarningAboutFailureTest(expectedException) + methodInvocationBlock() + } + PARAMETRIZED -> error("Unexpected $PARAMETRIZED method type for failing execution with $expectedException exception") + } + } + + // TODO: ISSUE-1546 introduce some kind of language-specific string interpolation in Codegen + // and render arguments that cause overflow (but it requires engine enhancements) + private fun prepareArtificialFailureMessage(executionResult: UtExecutionResult): CgLiteral { + when (executionResult) { + is UtOverflowFailure -> { + val failureMessage = "Overflow detected in \'${currentExecutableToCall!!.name}\' call" + return CgLiteral(stringClassId, failureMessage) + } + is UtTaintAnalysisFailure -> { + return CgLiteral(stringClassId, executionResult.exception.message) + } + else -> error("$executionResult is not supported in artificial errors") + } + } + + private fun constructExceptionProducingBlock( + exceptionFromAnalysis: Throwable, + executionResult: UtExecutionResult + ): Pair<() -> Unit, Throwable> { + if (executionResult is UtStreamConsumingFailure) { + return constructStreamConsumingBlock() to executionResult.rootCauseException + } + + return { + with(currentExecutableToCall) { + when (this) { + is MethodId -> thisInstance[this](*methodArguments.toTypedArray()).intercepted() + is ConstructorId -> this(*methodArguments.toTypedArray()).intercepted() + else -> {} // TODO: check this specific case + } + } + } to exceptionFromAnalysis + } + + private fun constructStreamConsumingBlock(): () -> Unit { + val executable = currentExecutableToCall + + require((executable is MethodId) && (executable.returnType isSubtypeOf baseStreamClassId)) { + "Unexpected non-stream returning executable $executable" + } + + val allSuperTypesOfReturn = executable.returnType.allSuperTypes().toSet() + + val streamConsumingMethodId = when { + // The order is important since all streams implement BaseStream + intStreamClassId in allSuperTypesOfReturn -> intStreamToArrayMethodId + longStreamClassId in allSuperTypesOfReturn -> longStreamToArrayMethodId + doubleStreamClassId in allSuperTypesOfReturn -> doubleStreamToArrayMethodId + streamClassId in allSuperTypesOfReturn -> streamToArrayMethodId + else -> { + // BaseStream, use util method to consume it + return { +utilsClassId[consumeBaseStream](actual) } + } + } + + return { +actual[streamConsumingMethodId]() } + } + + protected open fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean { + if (exception is AccessControlException) return false + // tests with timeout or crash should be processed differently + if (exception is TimeoutException || exception is InstrumentedProcessDeathException) return false + if (exception is ArtificialError) return false + if (UtSettings.treatAssertAsErrorSuite && exception is AssertionError) return false + + val exceptionRequiresAssert = exception !is RuntimeException || runtimeExceptionTestsBehaviour == PASS + val exceptionIsExplicit = execution.result is UtExplicitlyThrownException + return exceptionRequiresAssert || exceptionIsExplicit + } + + protected fun shouldTestPassWithTimeoutException(execution: UtExecution, exception: Throwable): Boolean { + return execution.result is UtTimeoutException || exception is TimeoutException + } + + protected fun writeWarningAboutTimeoutExceeding() { + +CgMultilineComment( + listOf( + "This execution may take longer than the ${hangingTestsTimeout.timeoutMs} ms timeout", + " and therefore fail due to exceeding the timeout." + ) + ) + } + + protected fun writeWarningAboutFailureTest(exception: Throwable) { + require(currentExecutableToCall is ExecutableId) + val executableName = "${currentExecutableToCall!!.classId.name}.${currentExecutableToCall!!.name}" + + val warningLine = "This test fails because method [$executableName] produces [$exception]" + .lines() + .map { it.escapeControlChars() } + .toMutableList() + + +CgMultilineComment(warningLine + collectNeededStackTraceLines( + exception, + executableToStartCollectingFrom = currentExecutableToCall!! + )) + } + + protected open fun collectNeededStackTraceLines( + exception: Throwable, + executableToStartCollectingFrom: ExecutableId + ): List { + val executableName = "${executableToStartCollectingFrom.classId.name}.${executableToStartCollectingFrom.name}" + + val neededStackTraceLines = mutableListOf() + var executableCallFound = false + exception.stackTrace.reversed().forEach { stackTraceElement -> + val line = stackTraceElement.toString() + if (line.startsWith(executableName)) { + executableCallFound = true + } + if (executableCallFound) { + neededStackTraceLines += TAB + line + } + } + if (!executableCallFound) + logger.warn(exception) { "Failed to find executable call in stack trace" } + + return neededStackTraceLines.reversed() + } + + protected fun writeWarningAboutCrash() { + +CgSingleLineComment("This invocation possibly crashes JVM") + } + + /** + * Generates result assertions in parameterized tests for successful executions + * and just runs the method if all executions are unsuccessful. + */ + private fun generateAssertionsForParameterizedTest() { + emptyLineIfNeeded() + + when (val executable = currentExecutableToCall) { + is ConstructorId -> generateConstructorCall(executable, currentExecution!!) + is MethodId -> { + val executionResult = currentExecution!!.result + + executionResult + .onSuccess { resultModel -> + if (resultModel.isUnit()) { + +thisInstance[executable](*methodArguments.toTypedArray()) + } else { + //"generic" expected variable is represented with a wrapper if + //actual result is primitive to support cases with exceptions. + this.resultModel = if (resultModel is UtPrimitiveModel) assemble(resultModel) else resultModel + + val expectedVariable = currentMethodParameters[CgParameterKind.ExpectedResult]!! + val expectedExpression = CgNotNullAssertion(expectedVariable) + + assertEquality(expectedExpression, actual) + } + } + .onFailure { + workaround(WorkaroundReason.CONSUME_DIRTY_STREAMS) { + if (containsStreamConsumingFailureForParametrizedTests) { + constructStreamConsumingBlock().invoke() + } else { + thisInstance[executable](*methodArguments.toTypedArray()).intercepted() + } + } + } + } + else -> {} // TODO: check this specific case + } + } + + /** + * Generates assertions for field states. + * + * Note: not supported in parameterized tests. + */ + protected fun generateFieldStateAssertions() { + val thisInstanceCache = statesCache.thisInstance + for (path in thisInstanceCache.paths) { + assertStatesByPath(thisInstanceCache, path) + } + for (argumentCache in statesCache.arguments) { + for (path in argumentCache.paths) { + assertStatesByPath(argumentCache, path) + } + } + for ((_, staticFieldCache) in statesCache.classesWithStaticFields) { + for (path in staticFieldCache.paths) { + assertStatesByPath(staticFieldCache, path) + } + } + } + + /** + * If the given field is _not_ of reference type, then the variable for its 'before' state + * is not created, because we only need its final state to make an assertion. + * For reference type fields, in turn, we make an assertion assertFalse(before == after). + */ + private fun assertStatesByPath(cache: FieldStateCache, path: FieldPath) { + emptyLineIfNeeded() + val beforeVariable = cache.before[path]?.variable + val (afterVariable, afterModel) = cache.after[path]!! + + // TODO: remove the following after the issue fix + // We do not generate some assertions for Enums due to [https://github.com/UnitTestBot/UTBotJava/issues/1704]. + val beforeModel = cache.before[path]?.model + if (beforeModel !is UtEnumConstantModel && afterModel is UtEnumConstantModel) { + return + } + + if (afterModel !is UtReferenceModel) { + assertEquality( + expected = afterModel, + actual = afterVariable, + expectedVariableName = "expected" + afterVariable.name.capitalize() + ) + } else { + if (beforeVariable != null) + testFrameworkManager.assertBoolean(false, beforeVariable equalTo afterVariable) + // TODO: fail here + } + } + + private fun assertDeepEquals( + expectedModel: UtModel, + expected: CgVariable?, + actual: CgVariable, + depth: Int, + visitedModels: MutableSet, + expectedModelField: FieldId? = null, + ) { + val modelWithField = ModelWithField(expectedModel, expectedModelField) + if (modelWithField in visitedModels) return + + @Suppress("NAME_SHADOWING") + var expected = expected + if (expected == null) { + require(!needExpectedDeclaration(expectedModel)) + expected = actual + } + + visitedModels += modelWithField + + with(testFrameworkManager) { + if (expectedModel.isMockModel()) { + currentBlock += assertions[assertSame](expected, actual).toStatement() + return + } + + if (depth >= DEEP_EQUALS_MAX_DEPTH) { + currentBlock += CgSingleLineComment("Current deep equals depth exceeds max depth $DEEP_EQUALS_MAX_DEPTH") + currentBlock += getDeepEqualsAssertion(expected, actual).toStatement() + return + } + + when (expectedModel) { + is UtPrimitiveModel -> { + currentBlock += when { + (expected.type == floatClassId || expected.type == floatWrapperClassId) -> + assertions[assertFloatEquals]( // cast have to be not safe here because of signature + typeCast(floatClassId, expected, isSafetyCast = false), + typeCast(floatClassId, actual, isSafetyCast = false), + floatDelta + ) + (expected.type == doubleClassId || expected.type == doubleWrapperClassId) -> + assertions[assertDoubleEquals]( // cast have to be not safe here because of signature + typeCast(doubleClassId, expected, isSafetyCast = false), + typeCast(doubleClassId, actual, isSafetyCast = false), + doubleDelta + ) + expectedModel.value is Boolean -> { + when (parametrizedTestSource) { + 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 + else -> { + require(expected.type.isPrimitive || expected.type == stringClassId) { + "Expected primitive or String but got ${expected.type}" + } + assertions[assertEquals](expected, actual) + } + }.toStatement() + } + is UtEnumConstantModel -> { + currentBlock += assertions[assertEquals]( + expected, + actual + ).toStatement() + } + is UtClassRefModel -> { + // TODO this stuff is needed because Kotlin has javaclass property instead of Java getClass method + // probably it is better to change getClass method behaviour in the future + val actualObject: CgVariable = when (codegenLanguage) { + CodegenLanguage.KOTLIN -> newVar( + baseType = objectClassId, + baseName = nameGenerator.variableName("actualObject"), + init = { CgTypeCast(objectClassId, actual) } + ) + else -> actual + } + + currentBlock += assertions[assertEquals]( + CgGetJavaClass(expected.type), + actualObject[getClass]() + ).toStatement() + } + is UtNullModel -> { + currentBlock += assertions[assertNull](actual).toStatement() + } + is UtArrayModel -> { + val arrayInfo = expectedModel.collectArrayInfo() + val nestedElementClassId = arrayInfo.nestedElementClassId + ?: error("Expected element class id from array ${arrayInfo.classId} but null found") + + if (!arrayInfo.isPrimitiveArray) { + // array of objects, have to use deep equals + + // We can't use for loop here because array model can contain different models + // and there is no a general approach to process it in loop + // For example, actual can be Object[3] and + // actual[0] instance of Point[][] + // actual[1] instance of int[][][] + // actual[2] instance of Object[] + + addArraysLengthAssertion(expected, actual) + currentBlock += getDeepEqualsAssertion(expected, actual).toStatement() + return + } + + // It does not work for Double and Float because JUnit does not have equals overloading with wrappers + if (nestedElementClassId == floatClassId || nestedElementClassId == doubleClassId) { + floatingPointArraysDeepEquals(arrayInfo, expected, actual) + return + } + + // common primitive array, can use default array equals + addArraysLengthAssertion(expected, actual) + currentBlock += getArrayEqualsAssertion( + expectedModel.classId, + typeCast(expectedModel.classId, expected, isSafetyCast = true), + typeCast(expectedModel.classId, actual, isSafetyCast = true) + ).toStatement() + } + is UtAssembleModel -> { + if (expectedModel.classId.isPrimitiveWrapper) { + currentBlock += assertions[assertEquals](expected, actual).toStatement() + return + } + + // UtCompositeModel deep equals is much more easier and human friendly + expectedModel.origin?.let { + assertDeepEquals(it, expected, actual, depth, visitedModels) + return + } + + // special case for strings as they are constructed from UtAssembleModel but can be compared with equals + if (expectedModel.classId == stringClassId) { + currentBlock += assertions[assertEquals]( + expected, + actual + ).toStatement() + return + } + + // We cannot implement deep equals for not field set model + // because if modification was made by direct field access, we can compare modifications by field access too + // (like in modification expected.value = 5 we can assert equality expected.value and actual.value), + // but in other cases we don't know what fields do we need to compare + // (like if modification was List add() method invocation) + + // We can add some heuristics to process standard assemble models like List, Set and Map. + // So, there is a space for improvements + if (expectedModel.modificationsChain.isEmpty() || expectedModel.modificationsChain.any { it !is UtDirectSetFieldModel }) { + currentBlock += getDeepEqualsAssertion(expected, actual).toStatement() + return + } + + for (modificationStep in expectedModel.modificationsChain) { + modificationStep as UtDirectSetFieldModel + val fieldId = modificationStep.fieldId + val fieldModel = modificationStep.fieldModel + + // we should not process enclosing class + // (actually, we do not do it properly anyway) + if (fieldId.isInnerClassEnclosingClassReference) continue + + traverseFieldRecursively( + fieldId, + fieldModel, + expected, + actual, + depth, + visitedModels + ) + } + } + is UtCompositeModel -> assertDeepEqualsForComposite( + expected = expected, + actual = actual, + expectedModel = expectedModel, + depth = depth, + visitedModels = visitedModels + ) + is UtCustomModel -> assertDeepEqualsForComposite( + expected = expected, + actual = actual, + expectedModel = expectedModel.origin + ?: error("Can't generate equals assertion for custom expected model without origin [$expectedModel]"), + depth = depth, + visitedModels = visitedModels + ) + is UtLambdaModel -> Unit // we do not check equality of lambdas + is UtVoidModel -> { + // Unit result is considered in generateResultAssertions method + error("Unexpected UtVoidModel in deep equals") + } + else -> {} + } + } + } + + private fun TestFrameworkManager.assertDeepEqualsForComposite( + expected: CgVariable, + actual: CgVariable, + expectedModel: UtCompositeModel, + depth: Int, + visitedModels: MutableSet + ) { + // Basically, to compare two iterables or maps, we need to iterate over them and compare each entry. + // But it leads to a lot of trash code in each test method, and it is more clear to use + // outer deep equals here + if (expected.isIterableOrMap()) { + currentBlock += CgSingleLineComment( + "${expected.type.canonicalName} is iterable or Map, use outer deep equals to iterate over" + ) + currentBlock += getDeepEqualsAssertion(expected, actual).toStatement() + + return + } + + // We can use overridden equals if we have one, but not for mocks. + if (expected.hasNotParametrizedCustomEquals() && !expectedModel.isMock) { + // We rely on already existing equals + currentBlock += CgSingleLineComment("${expected.type.canonicalName} has overridden equals method") + currentBlock += assertions[assertEquals](expected, actual).toStatement() + + return + } + + for ((fieldId, fieldModel) in expectedModel.fields) { + // we should not process enclosing class + // (actually, we do not do it properly anyway) + if (fieldId.isInnerClassEnclosingClassReference) continue + + traverseFieldRecursively( + fieldId, + fieldModel, + expected, + actual, + depth, + visitedModels + ) + } + } + + private fun TestFrameworkManager.addArraysLengthAssertion( + expected: CgVariable, + actual: CgVariable, + ): CgDeclaration { + val cgGetLengthDeclaration = CgDeclaration( + intClassId, + nameGenerator.variableName("${expected.name}Size"), + expected.length(this@CgMethodConstructor) + ) + currentBlock += cgGetLengthDeclaration + currentBlock += assertions[assertEquals]( + cgGetLengthDeclaration.variable, + actual.length(this@CgMethodConstructor) + ).toStatement() + + return cgGetLengthDeclaration + } + + /** + * Generate deep equals for float and double any-dimensional arrays (DOES NOT includes wrappers) + */ + private fun TestFrameworkManager.floatingPointArraysDeepEquals( + expectedArrayInfo: ClassIdArrayInfo, + expected: CgVariable, + actual: CgVariable, + ) { + val cgGetLengthDeclaration = addArraysLengthAssertion(expected, actual) + + val nestedElementClassId = expectedArrayInfo.nestedElementClassId + ?: error("Expected from floating point array ${expectedArrayInfo.classId} to contain elements but null found") + require(nestedElementClassId == floatClassId || nestedElementClassId == doubleClassId) { + "Expected float or double ClassId but `$nestedElementClassId` found" + } + + if (expectedArrayInfo.isSingleDimensionalArray) { + // we can use array equals for all single dimensional arrays + currentBlock += when (nestedElementClassId) { + floatClassId -> getFloatArrayEqualsAssertion( + typeCast(floatArrayClassId, expected, isSafetyCast = true), + typeCast(floatArrayClassId, actual, isSafetyCast = true), + floatDelta + ) + else -> getDoubleArrayEqualsAssertion( + typeCast(doubleArrayClassId, expected, isSafetyCast = true), + typeCast(doubleArrayClassId, actual, isSafetyCast = true), + doubleDelta + ) + }.toStatement() + } else { + // we can't use array equals for multidimensional double and float arrays + // so we need to go deeper to single-dimensional array + forLoop { + val (i, init) = variableConstructor.loopInitialization(intClassId, "i", initializer = 0) + initialization = init + condition = i lessThan cgGetLengthDeclaration.variable.resolve() + update = i.inc() + + statements = block { + val expectedNestedElement = newVar( + baseType = expected.type.elementClassId!!, + baseName = nameGenerator.variableName("${expected.name}NestedElement"), + init = { CgArrayElementAccess(expected, i) } + ) + + val actualNestedElement = newVar( + baseType = actual.type.elementClassId!!, + baseName = nameGenerator.variableName("${actual.name}NestedElement"), + init = { CgArrayElementAccess(actual, i) } + ) + + emptyLine() + + ifStatement( + CgEqualTo(expectedNestedElement, nullLiteral()), + trueBranch = { +assertions[assertNull](actualNestedElement).toStatement() }, + falseBranch = { + floatingPointArraysDeepEquals( + expectedArrayInfo.getNested(), + expectedNestedElement, + actualNestedElement, + ) + } + ) + } + } + } + } + + private fun CgVariable.isIterableOrMap(): Boolean = type.isIterableOrMap + + /** + * Some classes have overridden equals method, but it doesn't work properly. + * For example, List has overridden equals method but it relies on T equals. + * So, if T doesn't override equals, assertEquals with List fails. + * Therefore, all standard collections and map can fail. + * We overapproximate this assumption for all parametrized classes because we can't be sure that + * overridden equals doesn't rely on type parameters equals. + */ + private fun CgVariable.hasNotParametrizedCustomEquals(): Boolean { + if (type.jClass.overridesEquals()) { + // type parameters is list of class type parameters - empty if class is not generic + val typeParameters = type.kClass.typeParameters + + return typeParameters.isEmpty() + } + + return false + } + + private fun traverseFieldRecursively( + fieldId: FieldId, + fieldModel: UtModel, + expected: CgVariable, + actual: CgVariable, + 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 + val modelWithField = ModelWithField(fieldModel, fieldId) + if (modelWithField in visitedModels) { + currentBlock += testFrameworkManager.getDeepEqualsAssertion(expected, actual).toStatement() + return + } + + when (parametrizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> { + traverseField(fieldId, fieldModel, expected, actual, depth, visitedModels) + } + + ParametrizedTestSource.PARAMETRIZE -> { + traverseFieldForParametrizedTest(fieldId, fieldModel, expected, actual, depth, visitedModels) + } + } + } + + private fun traverseField( + fieldId: FieldId, + fieldModel: UtModel, + expected: CgVariable, + actual: CgVariable, + depth: Int, + visitedModels: MutableSet + ) { + // fieldModel is not visited and will be marked in assertDeepEquals call + val fieldName = fieldId.name + var expectedVariable: CgVariable? = null + + if (needExpectedDeclaration(fieldModel)) { + val expectedFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, expected, fieldName) + + currentBlock += expectedFieldDeclaration + expectedVariable = expectedFieldDeclaration.variable + } + + val actualFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, actual, fieldName) + currentBlock += actualFieldDeclaration + + assertDeepEquals( + fieldModel, + expectedVariable, + actualFieldDeclaration.variable, + depth + 1, + visitedModels, + fieldId, + ) + emptyLineIfNeeded() + } + + private fun traverseFieldForParametrizedTest( + fieldId: FieldId, + fieldModel: UtModel, + expected: CgVariable, + actual: CgVariable, + depth: Int, + visitedModels: MutableSet + ) { + val fieldResultModels = fieldsOfExecutionResults[fieldId to depth] + val nullResultModelInExecutions = fieldResultModels?.find { it.isNull() } + val notNullResultModelInExecutions = fieldResultModels?.find { it.isNotNull() } + + val hasNullResultModel = nullResultModelInExecutions != null + val hasNotNullResultModel = notNullResultModelInExecutions != null + + val needToSubstituteFieldModel = fieldModel is UtNullModel && hasNotNullResultModel + + val fieldModelForAssert = if (needToSubstituteFieldModel) notNullResultModelInExecutions!! else fieldModel + + // fieldModel is not visited and will be marked in assertDeepEquals call + val fieldName = fieldId.name + var expectedVariable: CgVariable? = null + + val needExpectedDeclaration = needExpectedDeclaration(fieldModelForAssert) + if (needExpectedDeclaration) { + val expectedFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, expected, fieldName) + + currentBlock += expectedFieldDeclaration + expectedVariable = expectedFieldDeclaration.variable + } + + val actualFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, actual, fieldName) + currentBlock += actualFieldDeclaration + + if (needExpectedDeclaration && hasNullResultModel) { + ifStatement( + CgEqualTo(expectedVariable!!, nullLiteral()), + trueBranch = { +testFrameworkManager.assertions[testFramework.assertNull](actualFieldDeclaration.variable).toStatement() }, + falseBranch = { + assertDeepEquals( + fieldModelForAssert, + expectedVariable, + actualFieldDeclaration.variable, + depth + 1, + visitedModels, + fieldId, + ) + } + ) + } else { + assertDeepEquals( + fieldModelForAssert, + expectedVariable, + actualFieldDeclaration.variable, + depth + 1, + visitedModels, + fieldId, + ) + } + emptyLineIfNeeded() + } + + private fun collectExecutionsResultFields() { + for (model in successfulExecutionsModels) { + when (model) { + is UtCompositeModel -> collectExecutionsResultFieldsRecursively(model, 0) + + is UtModelWithCompositeOrigin -> model.origin?.let { + collectExecutionsResultFieldsRecursively(it, 0) + } + + // Lambdas do not have fields. They have captured values, but we do not consider them here. + is UtLambdaModel, + is UtNullModel, + is UtPrimitiveModel, + is UtArrayModel, + is UtClassRefModel, + is UtEnumConstantModel, + is UtVoidModel -> { + // only [UtCompositeModel] and [UtAssembleModel] have fields to traverse + } + else -> {} + } + } + } + + private fun collectExecutionsResultFieldsRecursively(model: UtCompositeModel, depth: Int) { + for ((fieldId, fieldModel) in model.fields) { + collectExecutionsResultFieldsRecursively(fieldId, fieldModel, depth) + } + } + + private fun collectExecutionsResultFieldsRecursively( + fieldId: FieldId, + fieldModel: UtModel, + depth: Int, + ) { + if (depth >= DEEP_EQUALS_MAX_DEPTH) { + return + } + + val fieldKey = fieldId to depth + fieldsOfExecutionResults.getOrPut(fieldKey) { mutableListOf() } += fieldModel + + when (fieldModel) { + is UtCompositeModel -> collectExecutionsResultFieldsRecursively(fieldModel, depth + 1) + + is UtModelWithCompositeOrigin -> fieldModel.origin?.let { + collectExecutionsResultFieldsRecursively(it, depth + 1) + } + + // Lambdas do not have fields. They have captured values, but we do not consider them here. + is UtLambdaModel, + is UtNullModel, + is UtPrimitiveModel, + is UtArrayModel, + is UtClassRefModel, + is UtEnumConstantModel, + is UtVoidModel -> { + // only [UtCompositeModel] and [UtAssembleModel] have fields to traverse + } + else -> {} + } + } + + @Suppress("UNUSED_ANONYMOUS_PARAMETER") + private fun createDeclarationForFieldFromVariable( + fieldId: FieldId, + variable: CgVariable, + fieldName: String + ): CgDeclaration { + val expectedFieldDeclaration = createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType = fieldId.type, + baseName = "${variable.name}${fieldName.capitalize()}", + init = { fieldId.getAccessExpression(variable) } + ).either( + { declaration -> declaration }, + { unexpectedExistingVariable -> + error( + "Unexpected existing variable for field $fieldName with type ${fieldId.type} " + + "from expected variable ${variable.name} with type ${variable.type}" + ) + } + ) + return expectedFieldDeclaration + } + + private fun FieldId.getAccessExpression(variable: CgVariable): CgExpression = + // Can directly access field only if it is declared in variable class (or in its ancestors) + // and is accessible from current package + if (variable.type.hasField(this) && canBeReadFrom(context, variable.type)) { + if (jField.isStatic) CgStaticFieldAccess(this) else CgFieldAccess(variable, this) + } else if (context.codegenLanguage == CodegenLanguage.JAVA && + !jField.isStatic && canBeReadViaGetterFrom(context) + ) { + variable[getter]() + } else { + utilsClassId[getFieldValue](variable, this.declaringClass.name, this.name) + } + + /** + * Stores array information about ClassId. + * @property classId ClassId itself. + * @property nestedElementClassId the most nested element ClassId if it is array and null otherwise. + * @property dimensions 0 for non-arrays and number of dimensions in case of arrays. + */ + private data class ClassIdArrayInfo(val classId: ClassId, val nestedElementClassId: ClassId?, val dimensions: Int) { + val isArray get(): Boolean = dimensions > 0 + + val isPrimitiveArray get(): Boolean = isArray && nestedElementClassId!!.isPrimitive + + val isSingleDimensionalArray get(): Boolean = dimensions == 1 + + fun getNested(): ClassIdArrayInfo { + require(dimensions > 0) { "Trying to get nested array from not array type $classId" } + + return copy(dimensions = dimensions - 1) + } + } + + private val UtArrayModel.isArray: Boolean + get() = this.classId.isArray + + private fun UtArrayModel.collectArrayInfo(): ClassIdArrayInfo { + if (!isArray) return ClassIdArrayInfo( + classId = classId, + nestedElementClassId = null, + dimensions = 0 + ) + + val nestedElementClassIdList = generateSequence(classId.elementClassId) { it.elementClassId }.toList() + val dimensions = nestedElementClassIdList.size + val nestedElementClassId = nestedElementClassIdList.last() + + return ClassIdArrayInfo(classId, nestedElementClassId, dimensions) + } + + fun assertEquality( + expected: UtModel, + actual: CgVariable, + expectedVariableName: String = "expected", + emptyLineIfNeeded: Boolean = false, + ) { + val successfullyConstructedCustomAssert = expected is UtCustomModel && + customAssertConstructor.tryConstructCustomAssert(expected, actual) + + if (!successfullyConstructedCustomAssert) { + val expectedVariable = variableConstructor.getOrCreateVariable(expected, expectedVariableName) + if (emptyLineIfNeeded) emptyLineIfNeeded() + assertEquality(expectedVariable, actual) + } + } + + open fun assertEquality(expected: CgValue, actual: CgVariable) { + when { + expected.type.isArray -> { + // TODO: How to compare arrays of Float and Double wrappers? + // TODO: For example, JUnit5 does not have an assertEquals() overload for these wrappers. + // TODO: So for now we compare arrays of these wrappers as arrays of Objects, but that is probably wrong. + when (expected.type.elementClassId!!) { + floatClassId -> testFrameworkManager.assertFloatArrayEquals( + typeCast(floatArrayClassId, expected, isSafetyCast = true), + typeCast(floatArrayClassId, actual, isSafetyCast = true), + floatDelta + ) + doubleClassId -> testFrameworkManager.assertDoubleArrayEquals( + typeCast(doubleArrayClassId, expected, isSafetyCast = true), + typeCast(doubleArrayClassId, actual, isSafetyCast = true), + doubleDelta + ) + else -> { + val targetType = when { + expected.type.isPrimitiveArray -> expected.type + actual.type.isPrimitiveArray -> actual.type + else -> objectArrayClassId + } + if (targetType.isPrimitiveArray) { + // we can use simple arrayEquals for primitive arrays + testFrameworkManager.assertArrayEquals( + targetType, + typeCast(targetType, expected, isSafetyCast = true), + typeCast(targetType, actual, isSafetyCast = true) + ) + } else { + // array of objects, have to use deep equals + + when (expected) { + is CgLiteral -> testFrameworkManager.assertEquals(expected, actual) + is CgNotNullAssertion -> generateForNotNullAssertion(expected, actual) + else -> { + require(resultModel is UtArrayModel) { + "Result model have to be UtArrayModel to generate arrays assertion " + + "but `${resultModel::class}` found" + } + generateDeepEqualsOrNullAssertion(expected, actual) + } + } + } + } + } + } + else -> when { + (expected.type == floatClassId || expected.type == floatWrapperClassId) -> { + testFrameworkManager.assertFloatEquals( + typeCast(floatClassId, expected, isSafetyCast = true), + typeCast(floatClassId, actual, isSafetyCast = true), + floatDelta + ) + } + (expected.type == doubleClassId || expected.type == doubleWrapperClassId) -> { + testFrameworkManager.assertDoubleEquals( + typeCast(doubleClassId, expected, isSafetyCast = true), + typeCast(doubleClassId, actual, isSafetyCast = true), + doubleDelta + ) + } + expected == nullLiteral() -> testFrameworkManager.assertNull(actual) + expected is CgLiteral && expected.value is Boolean -> { + when (parametrizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> + testFrameworkManager.assertBoolean(expected.value, actual) + ParametrizedTestSource.PARAMETRIZE -> + testFrameworkManager.assertEquals(expected, actual) + } + } + else -> { + when (expected) { + is CgLiteral -> testFrameworkManager.assertEquals(expected, actual) + is CgNotNullAssertion -> generateForNotNullAssertion(expected, actual) + else -> generateDeepEqualsOrNullAssertion(expected, actual) + } + } + } + } + } + + private fun generateForNotNullAssertion(expected: CgNotNullAssertion, actual: CgVariable) { + require(expected.expression is CgVariable) { + "Only CgVariable wrapped in CgNotNullAssertion is supported in deepEquals" + } + generateDeepEqualsOrNullAssertion(expected.expression, actual) + } + + private fun generateConstructorCall(currentExecutableId: ConstructorId, currentExecution: UtExecution) { + // we cannot generate any assertions for constructor testing + // but we need to generate a constructor call + val constructorCall = currentExecutableId as ConstructorId + val executionResult = currentExecution.result + + executionResult + .onSuccess { + methodType = SUCCESSFUL + + actual = newVar(constructorCall.classId, "actual") { + constructorCall(*methodArguments.toTypedArray()) + } + } + .onFailure { exception -> processExecutionFailure(exception, executionResult) } + } + + // Class is required to verify, if current model has already been analyzed in deepEquals. + // Using model without related field (if it is present) in comparison is incorrect, + // for example, for [UtNullModel] as they are equal to each other.. + private data class ModelWithField( + val fieldModel: UtModel, + val relatedField: FieldId?, + ) + + /** + * 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, + ) { + when (parametrizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> generateDeepEqualsAssertion(expected, actual) + ParametrizedTestSource.PARAMETRIZE -> { + collectExecutionsResultFields() + + when { + actual.type.isPrimitive -> generateDeepEqualsAssertion(expected, actual) + else -> ifStatement( + CgEqualTo(expected, nullLiteral()), + trueBranch = { + workaround(WorkaroundReason.CONSUME_DIRTY_STREAMS) { + if (containsStreamConsumingFailureForParametrizedTests) { + constructStreamConsumingBlock().invoke() + } else { + +testFrameworkManager.assertions[testFramework.assertNull](actual).toStatement() + } + } + }, + falseBranch = { + +testFrameworkManager.assertions[testFrameworkManager.assertNotNull](actual).toStatement() + generateDeepEqualsAssertion(expected, actual) + } + ) + } + } + } + } + + private fun generateDeepEqualsAssertion( + expected: CgValue, + actual: CgVariable, + ) { + require(expected is CgVariable) { + "Expected value have to be Literal or Variable but `${expected::class}` found" + } + + assertDeepEquals( + resultModel, + expected, + actual, + depth = 0, + visitedModels = hashSetOf() + ) + } + + protected fun recordActualResult() { + val executionResult = currentExecution!!.result + + executionResult.onSuccess { resultModel -> + when (val executable = currentExecutableToCall) { + is ConstructorId -> { + // there is nothing to generate for constructors + return + } + is BuiltinMethodId -> error("Unexpected BuiltinMethodId $executable while generating actual result") + is MethodId -> { + // TODO possible engine bug - void method return type and result model not UtVoidModel + if (resultModel.isUnit() || executable.returnType == voidClassId) return + + emptyLineIfNeeded() + + actual = newVar( + CgClassId(resultModel.classId, isNullable = resultModel is UtNullModel), + "actual" + ) { + thisInstance[executable](*methodArguments.toTypedArray()) + } + } + else -> {} // TODO: check this specific case + } + }.onFailure { + processActualInvocationFailure(executionResult) + } + } + + private fun processActualInvocationFailure(executionResult: UtExecutionResult) { + when (executionResult) { + is UtStreamConsumingFailure -> processStreamConsumingException(executionResult.rootCauseException) + else -> {} // Do nothing for now + } + } + + private fun processStreamConsumingException(innerException: Throwable) { + val executable = currentExecutableToCall + + require((executable is MethodId) && (executable.returnType isSubtypeOf baseStreamClassId)) { + "Unexpected exception $innerException during stream consuming in non-stream returning executable $executable" + } + + emptyLineIfNeeded() + + actual = newVar( + CgClassId(executable.returnType, isNullable = false), + "actual" + ) { + thisInstance[executable](*methodArguments.toTypedArray()) + } + } + + open fun createTestMethod(testSet: CgMethodTestSet, execution: UtExecution): CgTestMethod = + withTestMethodScope(execution) { + val testMethodName = nameGenerator.testMethodNameFor(testSet.executableUnderTest, execution.testMethodName) + if (execution.testMethodName == null) { + execution.testMethodName = testMethodName + } + // TODO: remove this line when SAT-1273 is completed + execution.displayName = execution.displayName?.let { "${testSet.executableUnderTest.name}: $it" } + testMethod(testMethodName, execution.displayName) { + //Enum constants are static, but there is no need to store and recover value for them + val statics = currentExecution!!.stateBefore + .statics + .filterNot { it.value is UtEnumConstantModel } + .filterNot { it.value.classId.outerClass?.isEnum == true } + + rememberInitialStaticFields(statics) + val stateAnalyzer = ExecutionStateAnalyzer(execution) + val modificationInfo = stateAnalyzer.findModifiedFields() + val fieldStateManager = CgFieldStateManagerImpl(context) + // TODO: move such methods to another class and leave only 2 public methods: remember initial and final states + val mainBody = { + substituteStaticFields(statics) + setupInstrumentation() + // build this instance + thisInstance = execution.stateBefore.thisInstance?.let { + variableConstructor.getOrCreateVariable(it) + } + // build arguments + for ((index, param) in execution.stateBefore.parameters.withIndex()) { + val name = paramNames[execution.executableToCall ?: testSet.executableUnderTest]?.get(index) + methodArguments += variableConstructor.getOrCreateVariable(param, name) + } + + if (requiresFieldStateAssertions()) { + // we should generate field assertions only for successful tests + // that does not break the current test execution after invocation of method under test + fieldStateManager.rememberInitialEnvironmentState(modificationInfo) + } + + recordActualResult() + generateResultAssertions() + + if (requiresFieldStateAssertions()) { + // we should generate field assertions only for successful tests + // that does not break the current test execution after invocation of method under test + fieldStateManager.rememberFinalEnvironmentState(modificationInfo) + generateFieldStateAssertions() + } + } + + if (statics.isNotEmpty()) { + +tryBlock { + mainBody() + }.finally { + recoverStaticFields() + } + } else { + mainBody() + } + + mockFrameworkManager.getAndClearMethodResources()?.let { resources -> + val closeFinallyBlock = resources.map { + val variable = it.variable + variable.type.closeMethodIdOrNull?.let { closeMethod -> + CgMethodCall(variable, closeMethod, arguments = emptyList()).toStatement() + } ?: error("Resource $variable was expected to be auto closeable but it is not") + } + + val tryWithMocksFinallyClosing = CgTryCatch(currentBlock, handlers = emptyList(), closeFinallyBlock) + currentBlock = currentBlock.clear() + resources.forEach { + // First argument for mocked resource declaration initializer is a target type. + // Pass this argument as a type parameter for the mocked resource + + // TODO this type parameter (required for Kotlin test) is unused until the proper implementation + // of generics in code generation https://github.com/UnitTestBot/UTBotJava/issues/88 + @Suppress("UNUSED_VARIABLE") + val typeParameter = when (val firstArg = (it.initializer as CgMethodCall).arguments.first()) { + is CgGetJavaClass -> firstArg.classId + is CgVariable -> firstArg.type + else -> error("Unexpected mocked resource declaration argument $firstArg") + } + + +CgDeclaration( + it.variableType, + it.variableName, + initializer = nullLiteral(), + isMutable = true, + ) + } + +tryWithMocksFinallyClosing + } + } + } + + private fun requiresFieldStateAssertions(): Boolean = + !methodType.isThrowing || + (methodType == PASSED_EXCEPTION && !testFrameworkManager.isExpectedExceptionExecutionBreaking) + + private val expectedResultVarName = "expectedResult" + private val expectedErrorVarName = "expectedError" + + /** + * Returns `null` if parameterized test method can't be created for [testSet] + * (for example, when there are multiple distinct [CgMethodTestSet.executablesToCall]). + */ + fun createParameterizedTestMethod(testSet: CgMethodTestSet, dataProviderMethodName: String): CgTestMethod? { + //TODO: orientation on generic execution may be misleading, but what is the alternative? + //may be a heuristic to select a model with minimal number of internal nulls should be used + val genericExecution = chooseGenericExecution(testSet.executions) + + workaround(WorkaroundReason.CONSUME_DIRTY_STREAMS) { + containsStreamConsumingFailureForParametrizedTests = testSet.executions.any { + it.result is UtStreamConsumingFailure + } + } + + val statics = genericExecution.stateBefore.statics + + return withTestMethodScope(genericExecution) { + val testName = nameGenerator.parameterizedTestMethodName(dataProviderMethodName) + withNameScope { + val testParameterDeclarations = + createParameterDeclarations(testSet, genericExecution) ?: return@withNameScope null + + methodType = PARAMETRIZED + testMethod( + testName, + displayName = null, + testParameterDeclarations, + parameterized = true, + dataProviderMethodName + ) { + rememberInitialStaticFields(statics) + substituteStaticFields(statics, isParametrized = true) + + // build this instance + thisInstance = genericExecution.stateBefore.thisInstance?.let { + currentMethodParameters[CgParameterKind.ThisInstance] + } + + // build arguments for method under test and parameterized test + for (index in genericExecution.stateBefore.parameters.indices) { + methodArguments += currentMethodParameters[CgParameterKind.Argument(index)]!! + } + + if (containsFailureExecution(testSet) || statics.isNotEmpty()) { + var currentTryBlock = tryBlock { + recordActualResult() + generateAssertionsForParameterizedTest() + } + + if (containsFailureExecution(testSet)) { + val expectedErrorVariable = currentMethodParameters[CgParameterKind.ExpectedException] + ?: error("Test set $testSet contains failure execution, but test method signature has no error parameter") + currentTryBlock = + if (containsReflectiveCall) { + currentTryBlock.catch(InvocationTargetException::class.java.id) { exception -> + testFrameworkManager.assertBoolean( + expectedErrorVariable.isInstance(exception[getTargetException]()) + ) + } + } else { + currentTryBlock.catch(Throwable::class.java.id) { throwable -> + testFrameworkManager.assertBoolean( + expectedErrorVariable.isInstance(throwable) + ) + } + } + } + + if (statics.isNotEmpty()) { + currentTryBlock = currentTryBlock.finally { + recoverStaticFields() + } + } + +currentTryBlock + } else { + recordActualResult() + generateAssertionsForParameterizedTest() + } + } + } + } + } + + private fun chooseGenericExecution(executions: List): UtExecution { + return executions + .firstOrNull { it.result is UtExecutionSuccess && (it.result as UtExecutionSuccess).model !is UtNullModel } + ?: executions + .firstOrNull { it.result is UtExecutionSuccess } ?: executions.first() + } + + /** + * Returns `null` if parameter declarations can't be created for [testSet] + * (for example, when there are multiple distinct [CgMethodTestSet.executablesToCall]). + */ + private fun createParameterDeclarations( + testSet: CgMethodTestSet, + genericExecution: UtExecution, + ): List? { + val executableToCall = testSet.executablesToCall.singleOrNull() ?: return null + val executableUnderTestParameters = executableToCall.executable.parameters + + return mutableListOf().apply { + // this instance + genericExecution.stateBefore.thisInstance?.let { + val type = wrapTypeIfRequired(it.classId) + val thisInstance = CgParameterDeclaration( + parameter = declareParameter( + type = type, + name = nameGenerator.variableName(type) + ), + isReferenceType = true + ) + this += thisInstance + currentMethodParameters[CgParameterKind.ThisInstance] = thisInstance.parameter + } + + // arguments + for (index in genericExecution.stateBefore.parameters.indices) { + val argumentName = paramNames[executableToCall]?.get(index) + val paramType = executableUnderTestParameters[index].parameterizedType + + val argumentType = when { + paramType is Class<*> && paramType.isArray -> paramType.id + paramType is ParameterizedType -> paramType.id + else -> ClassId(paramType.typeName) + } + + val argument = CgParameterDeclaration( + parameter = declareParameter( + type = argumentType, + name = nameGenerator.variableName(argumentType, argumentName), + ), + isReferenceType = argumentType.isRefType + ) + this += argument + currentMethodParameters[CgParameterKind.Argument(index)] = argument.parameter + } + + val statics = genericExecution.stateBefore.statics + if (statics.isNotEmpty()) { + for ((fieldId, model) in statics) { + val staticType = wrapTypeIfRequired(model.classId) + val static = CgParameterDeclaration( + parameter = declareParameter( + type = staticType, + name = nameGenerator.variableName(fieldId.name, isStatic = true) + ), + isReferenceType = staticType.isRefType + ) + this += static + currentMethodParameters[CgParameterKind.Statics(model)] = static.parameter + } + } + + val expectedResultClassId = wrapTypeIfRequired(testSet.getCommonResultTypeOrNull() ?: return null) + if (expectedResultClassId != voidClassId) { + val wrappedType = wrapIfPrimitive(expectedResultClassId) + //We are required to wrap the type of expected result if it is primitive + //to support nulls for throwing exceptions executions. + val expectedResult = CgParameterDeclaration( + parameter = declareParameter( + type = wrappedType, + name = nameGenerator.variableName(expectedResultVarName) + ), + isReferenceType = wrappedType.isRefType + ) + this += expectedResult + currentMethodParameters[CgParameterKind.ExpectedResult] = expectedResult.parameter + } + + val containsFailureExecution = containsFailureExecution(testSet) + if (containsFailureExecution) { + val classClassId = Class::class.id + val expectedException = CgParameterDeclaration( + parameter = declareParameter( + type = BuiltinClassId( + simpleName = classClassId.simpleName, + canonicalName = classClassId.canonicalName, + packageName = classClassId.packageName, + typeParameters = TypeParameters(listOf(Throwable::class.java.id)) + ), + name = nameGenerator.variableName(expectedErrorVarName) + ), + // exceptions are always reference type + isReferenceType = true, + ) + this += expectedException + currentMethodParameters[CgParameterKind.ExpectedException] = expectedException.parameter + } + } + } + + /** + * Constructs data provider method for parameterized tests. + * + * The body of this method is constructed manually, statement by statement. + * Standard logic for generating each test case parameter code is used. + */ + fun createParameterizedTestDataProvider( + testSet: CgMethodTestSet, + dataProviderMethodName: String + ): CgParameterizedTestDataProviderMethod { + return withDataProviderScope { + dataProviderMethod(dataProviderMethodName) { + val argListLength = testSet.executions.size + val argListVariable = testFrameworkManager.createArgList(argListLength) + + emptyLine() + + for ((execIndex, execution) in testSet.executions.withIndex()) { + // create a block for current test case + innerBlock { + val arguments = createExecutionArguments(testSet, execution) + createArgumentsCallRepresentation(execIndex, argListVariable, arguments) + } + } + + emptyLineIfNeeded() + + returnStatement { argListVariable } + } + } + } + + private fun createExecutionArguments(testSet: CgMethodTestSet, execution: UtExecution): List { + val arguments = mutableListOf() + + execution.stateBefore.thisInstance?.let { + arguments += variableConstructor.getOrCreateVariable(it) + } + + for ((paramIndex, paramModel) in execution.stateBefore.parameters.withIndex()) { + val argumentName = paramNames[execution.executableToCall ?: testSet.executableUnderTest]?.get(paramIndex) + arguments += variableConstructor.getOrCreateVariable(paramModel, argumentName) + } + + val statics = execution.stateBefore.statics + for ((field, model) in statics) { + arguments += variableConstructor.getOrCreateVariable(model, field.name) + } + + val method = currentExecutableToCall!! + val needsReturnValue = method.returnType != voidClassId + val containsFailureExecution = containsFailureExecution(testSet) + execution.result + .onSuccess { + if (needsReturnValue) { + arguments += variableConstructor.getOrCreateVariable(it) + } + if (containsFailureExecution) { + arguments += nullLiteral() + } + } + .onFailure { + if (needsReturnValue) { + arguments += nullLiteral() + } + if (containsFailureExecution) { + arguments += CgGetJavaClass(it::class.id) + } + } + + emptyLineIfNeeded() + + return arguments + } + + protected fun withTestMethodScope(execution: UtExecution, block: () -> R): R { + clearTestMethodScope() + currentExecution = execution + determineExecutionType() + statesCache = EnvironmentFieldStateCache.emptyCacheFor(execution) +// modelToUsageCountInMethod = countUsages(ignoreAssembleOrigin = true) { counter -> +// execution.mapAllModels(counter) +// } + return try { + block() + } finally { + clearTestMethodScope() + } + } + + private fun withDataProviderScope(block: () -> R): R { + clearMethodScope() + return try { + block() + } finally { + clearMethodScope() + } + } + + /** + * This function makes sure some information about the method currently being generated is empty. + * It clears only the information that is relevant to all kinds of methods: + * - test methods + * - data provider methods + * - and any other kinds of methods that may be added in the future + */ + private fun clearMethodScope() { + collectedExceptions.clear() + collectedMethodAnnotations.clear() + } + + /** + * This function makes sure some information about the **test method** currently being generated is empty. + * It is used at the start of test method generation and right after it. + */ + private fun clearTestMethodScope() { + clearMethodScope() + prevStaticFieldValues.clear() + thisInstance = null + methodArguments.clear() + currentExecution = null + containsReflectiveCall = false + mockFrameworkManager.clearExecutionResources() + currentMethodParameters.clear() + } + + /** + * Generates a collection of [CgStatement] to prepare arguments + * for current execution in parameterized tests. + */ + private fun createArgumentsCallRepresentation( + executionIndex: Int, + argsVariable: CgVariable, + arguments: List, + ) { + val argsArray = newVar(objectArrayClassId, "testCaseObjects") { + CgAllocateArray(objectArrayClassId, objectClassId, arguments.size) + } + for ((i, argument) in arguments.withIndex()) { + setArgumentsArrayElement(argsArray, i, argument, this) + } + testFrameworkManager.passArgumentsToArgsVariable(argsVariable, argsArray, executionIndex) + } + + private fun containsFailureExecution(testSet: CgMethodTestSet) = + testSet.executions.any { it.result is UtExecutionFailure } + + + /** + * Determines [CgTestMethodType] for current execution according to its success or failure. + */ + private fun determineExecutionType() { + val currentExecution = currentExecution!! + + currentExecution.result + .onSuccess { methodType = SUCCESSFUL } + .onFailure { exception -> + methodType = when { + shouldTestPassWithException(currentExecution, exception) -> PASSED_EXCEPTION + shouldTestPassWithTimeoutException(currentExecution, exception) -> TIMEOUT + else -> when (exception) { + is ArtificialError -> ARTIFICIAL + is InstrumentedProcessDeathException -> CRASH + is AccessControlException -> CRASH // exception from sandbox + else -> FAILING + } + } + } + } + + protected fun testMethod( + methodName: String, + displayName: String?, + params: List = emptyList(), + parameterized: Boolean = false, + dataProviderMethodName: String? = null, + body: () -> Unit, + ): CgTestMethod { + if (parameterized) { + testFrameworkManager.addParameterizedTestAnnotations(dataProviderMethodName) + } else { + addAnnotation(testFramework.testAnnotationId, AnnotationTarget.Method) + } + + displayName?.let { + testFrameworkManager.addTestDescription(displayName) + } + + val result = currentExecution!!.result + if (result is UtTimeoutException) { + testFrameworkManager.setTestExecutionTimeout(hangingTestsTimeout.timeoutMs) + } + + if (result is UtTimeoutException && !enableTestsTimeout) { + testFrameworkManager.disableTestMethod( + "Disabled due to failing by exceeding the timeout" + ) + } + + if (result is UtConcreteExecutionFailure) { + testFrameworkManager.disableTestMethod( + "Disabled due to possible JVM crash" + ) + } + + if (result is UtSandboxFailure) { + testFrameworkManager.disableTestMethod( + "Disabled due to sandbox" + ) + } + + val testMethod = buildTestMethod { + name = methodName + parameters = params + statements = block(body) + // Exceptions and annotations assignment must run after the statements block is build, + // because we collect info about exceptions and required annotations while building the statements + exceptions += collectedExceptions + annotations += collectedMethodAnnotations + methodType = this@CgMethodConstructor.methodType + val docComment = currentExecution?.summary?.map { convertDocToCg(it) }?.toMutableList() ?: mutableListOf() + + documentation = CgDocumentationComment(docComment) + documentation = if (parameterized) { + CgDocumentationComment(text = null) + } else { + CgDocumentationComment(docComment) + } + } + testMethods += testMethod + return testMethod + } + + private fun dataProviderMethod(dataProviderMethodName: String, body: () -> Unit): CgParameterizedTestDataProviderMethod { + return buildParameterizedTestDataProviderMethod { + name = dataProviderMethodName + returnType = testFramework.argListClassId + statements = block(body) + // Exceptions and annotations assignment must run after the statements block is build, + // because we collect info about exceptions and required annotations while building the statements + testFrameworkManager.addDataProviderAnnotations(dataProviderMethodName) + exceptions += collectedExceptions + annotations += collectedMethodAnnotations + } + } + + fun errorMethod(testSet: CgMethodTestSet, errors: Map): CgRegion { + val name = nameGenerator.errorMethodNameFor(testSet.executableUnderTest) + val body = block { + comment("Couldn't generate some tests. List of errors:") + comment() + errors.entries.sortedByDescending { it.value }.forEach { (message, repeated) -> + val multilineMessage = message + .split("\r") // split stacktrace from concrete if present + .flatMap { line -> + line + .split(" ") + .windowed(size = 10, step = 10, partialWindows = true) { + it.joinToString(separator = " ") + } + } + comment("$repeated occurrences of:") + + if (multilineMessage.size <= 1) { + // wrap one liner with line comment + multilineMessage.singleOrNull()?.let { comment(it) } + } else { + // wrap all lines with multiline comment + multilineComment(multilineMessage) + } + + emptyLine() + } + } + val errorTestMethod = CgErrorTestMethod(name, body) + return CgSimpleRegion("Errors report for ${testSet.executableUnderTest.name}", listOf(errorTestMethod)) + } + + private fun CgExecutableCall.wrapReflectiveCall() { + +tryBlock { + +this@wrapReflectiveCall + }.catch(InvocationTargetException::class.id) { e -> + throwStatement { + e[getTargetException]() + } + } + } + + /** + * Intercept calls to [java.lang.reflect.Method.invoke] and to [java.lang.reflect.Constructor.newInstance] + * in order to wrap these calls in a try-catch block that will handle [InvocationTargetException] + * that may be thrown by these calls. + */ + protected fun CgExecutableCall.intercepted() { + 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/tree/CgSimpleTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSimpleTestClassConstructor.kt new file mode 100644 index 0000000000..f7207630e4 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSimpleTestClassConstructor.kt @@ -0,0 +1,202 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.TestNg +import org.utbot.framework.codegen.domain.builtin.TestClassUtilMethodProvider +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgClassBody +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.CgMethodsCluster +import org.utbot.framework.codegen.domain.models.CgRealNestedClassesRegion +import org.utbot.framework.codegen.domain.models.CgRegion +import org.utbot.framework.codegen.domain.models.CgSimpleRegion +import org.utbot.framework.codegen.domain.models.CgStaticsRegion +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.SimpleTestClassModel +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtSymbolicExecution +import org.utbot.framework.plugin.api.util.humanReadableName +import org.utbot.fuzzer.UtFuzzedExecution + +/** + * This test class constructor is used for pure Java/Kotlin applications. + */ +open class CgSimpleTestClassConstructor(context: CgContext): CgAbstractTestClassConstructor(context) { + + override fun constructTestClassBody(testClassModel: SimpleTestClassModel): CgClassBody = + buildClassBody(currentTestClass) { + val notYetConstructedTestSets = testClassModel.methodTestSets.toMutableList() + + for (nestedClass in testClassModel.nestedClasses) { + // It is not possible to run tests for both outer and inner class in JUnit4 at once, + // so we locate all test methods in outer test class for JUnit4. + // see https://stackoverflow.com/questions/69770700/how-to-run-tests-from-outer-class-and-nested-inner-classes-simultaneously-in-jun + // or https://stackoverflow.com/questions/28230277/test-cases-in-inner-class-and-outer-class-with-junit4 + when (testFramework) { + Junit4 -> { + notYetConstructedTestSets += collectTestSetsFromInnerClasses(nestedClass) + } + Junit5, + TestNg -> { + nestedClassRegions += CgRealNestedClassesRegion( + "Tests for ${nestedClass.classUnderTest.simpleName}", + listOf(withNestedClassScope(nestedClass) { constructTestClass(nestedClass) }) + ) + } + } + } + + for ((testSetIndex, testSet) in notYetConstructedTestSets.withIndex()) { + updateExecutableUnderTest(testSet.executableUnderTest) + withTestSetIdScope(testSetIndex) { + val currentMethodUnderTestRegions = constructTestSet(testSet) ?: return@withTestSetIdScope + val executableUnderTestCluster = CgMethodsCluster( + "Test suites for executable $currentExecutableUnderTest", + currentMethodUnderTestRegions + ) + methodRegions += executableUnderTestCluster + } + } + + val currentTestClassDataProviderMethods = currentTestClassContext.cgDataProviderMethods + if (currentTestClassDataProviderMethods.isNotEmpty()) { + staticDeclarationRegions += + CgStaticsRegion( + "Data provider methods for parametrized tests", + currentTestClassDataProviderMethods, + ) + } + + if (currentTestClass == outerMostTestClass) { + val utilEntities = collectUtilEntities() + // If utilMethodProvider is TestClassUtilMethodProvider, then util entities should be declared + // in the test class. Otherwise, util entities will be located elsewhere (e.g. another class). + if (utilMethodProvider is TestClassUtilMethodProvider && utilEntities.isNotEmpty()) { + staticDeclarationRegions += CgStaticsRegion("Util methods", utilEntities) + } + } + } + + override fun constructTestSet(testSet: CgMethodTestSet): List>? { + val regions = mutableListOf>() + + if (testSet.executions.any()) { + successfulExecutionsModels = testSet + .executions + .filter { it.result is UtExecutionSuccess } + .map { (it.result as UtExecutionSuccess).model } + + runCatching { + when (context.parametrizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> createTest(testSet, regions) + ParametrizedTestSource.PARAMETRIZE -> + createParametrizedTestAndDataProvider( + testSet, + regions + ).let { parametrizedTestCreatedSuccessfully -> + if (!parametrizedTestCreatedSuccessfully) createTest(testSet, regions) + } + } + }.onFailure { e -> processFailure(testSet, e) } + } + + val errors = testSet.allErrors + if (errors.isNotEmpty()) { + regions += methodConstructor.errorMethod(testSet, errors) + testsGenerationReport.addMethodErrors(testSet, errors) + } + + return if (regions.any()) regions else null + } + + private fun collectTestSetsFromInnerClasses(model: SimpleTestClassModel): List { + val testSets = model.methodTestSets.toMutableList() + for (nestedClass in model.nestedClasses) { + testSets += collectTestSetsFromInnerClasses(nestedClass) + } + + return testSets + } + + /** + * Returns `false` if parameterized test method can't be created for [testSet] + * (for example, when there are multiple distinct [CgMethodTestSet.executablesToCall]). + */ + private fun createParametrizedTestAndDataProvider( + testSet: CgMethodTestSet, + regions: MutableList> + ) : Boolean { + val (methodUnderTest, _, _) = testSet + + for (preparedTestSet in testSet.prepareTestSetsForParameterizedTestGeneration()) { + val dataProviderMethodName = nameGenerator.dataProviderMethodNameFor(preparedTestSet.executableUnderTest) + + val parameterizedTestMethod = + methodConstructor.createParameterizedTestMethod(preparedTestSet, dataProviderMethodName) ?: return false + + testFrameworkManager.addDataProvider( + methodConstructor.createParameterizedTestDataProvider(preparedTestSet, dataProviderMethodName) + ) + + regions += CgSimpleRegion( + "Parameterized test for method ${methodUnderTest.humanReadableName}", + listOf(parameterizedTestMethod), + ) + } + + regions += CgSimpleRegion( + "SYMBOLIC EXECUTION: additional tests for symbolic executions for method ${methodUnderTest.humanReadableName} that cannot be presented as parameterized", + collectAdditionalSymbolicTestsForParametrizedMode(testSet), + ) + + regions += CgSimpleRegion( + "FUZZER: Tests for method ${methodUnderTest.humanReadableName} that cannot be presented as parameterized", + collectFuzzerTestsForParameterizedMode(testSet), + ) + + return true + } + + /** + * Collects standard tests for fuzzer executions in parametrized mode. + * This is a requirement from [https://github.com/UnitTestBot/UTBotJava/issues/1137]. + */ + private fun collectFuzzerTestsForParameterizedMode(testSet: CgMethodTestSet): List { + val testMethods = mutableListOf() + + testSet.executions + .filterIsInstance() + .withIndex() + .forEach { (index, execution) -> + withExecutionIdScope(index) { + testMethods += methodConstructor.createTestMethod(testSet, execution) + } + } + + return testMethods + } + + /** + * Collects standard tests for symbolic executions that can't be included into parametrized tests. + * This is a requirement from [https://github.com/UnitTestBot/UTBotJava/issues/1231]. + */ + private fun collectAdditionalSymbolicTestsForParametrizedMode(testSet: CgMethodTestSet): List { + val testMethods = mutableListOf() + + testSet.executions + .filterIsInstance() + .filter { it.containsMocking } + .withIndex() + .forEach { (index, execution) -> + withExecutionIdScope(index) { + testMethods += methodConstructor.createTestMethod(testSet, execution) + } + } + + return testMethods + } +} + diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgStatementConstructor.kt similarity index 80% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgStatementConstructor.kt index 5908cd6d27..fc6a262dae 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgStatementConstructor.kt @@ -1,50 +1,12 @@ -package org.utbot.framework.codegen.model.constructor.util - -import org.utbot.framework.codegen.model.constructor.builtin.forName -import org.utbot.framework.codegen.model.constructor.builtin.mockMethodId -import org.utbot.framework.codegen.model.constructor.context.CgContext -import org.utbot.framework.codegen.model.constructor.context.CgContextOwner -import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager -import org.utbot.framework.codegen.model.tree.CgAllocateArray -import org.utbot.framework.codegen.model.tree.CgAnnotation -import org.utbot.framework.codegen.model.tree.CgAnonymousFunction -import org.utbot.framework.codegen.model.tree.CgComment -import org.utbot.framework.codegen.model.tree.CgDeclaration -import org.utbot.framework.codegen.model.tree.CgEmptyLine -import org.utbot.framework.codegen.model.tree.CgEnumConstantAccess -import org.utbot.framework.codegen.model.tree.CgErrorWrapper -import org.utbot.framework.codegen.model.tree.CgExecutableCall -import org.utbot.framework.codegen.model.tree.CgExpression -import org.utbot.framework.codegen.model.tree.CgForEachLoopBuilder -import org.utbot.framework.codegen.model.tree.CgForLoopBuilder -import org.utbot.framework.codegen.model.tree.CgGetClass -import org.utbot.framework.codegen.model.tree.CgIfStatement -import org.utbot.framework.codegen.model.tree.CgInnerBlock -import org.utbot.framework.codegen.model.tree.CgLiteral -import org.utbot.framework.codegen.model.tree.CgLogicalAnd -import org.utbot.framework.codegen.model.tree.CgLogicalOr -import org.utbot.framework.codegen.model.tree.CgMethodCall -import org.utbot.framework.codegen.model.tree.CgMultilineComment -import org.utbot.framework.codegen.model.tree.CgMultipleArgsAnnotation -import org.utbot.framework.codegen.model.tree.CgNamedAnnotationArgument -import org.utbot.framework.codegen.model.tree.CgParameterDeclaration -import org.utbot.framework.codegen.model.tree.CgReturnStatement -import org.utbot.framework.codegen.model.tree.CgSingleArgAnnotation -import org.utbot.framework.codegen.model.tree.CgSingleLineComment -import org.utbot.framework.codegen.model.tree.CgThrowStatement -import org.utbot.framework.codegen.model.tree.CgTryCatch -import org.utbot.framework.codegen.model.tree.CgVariable -import org.utbot.framework.codegen.model.tree.buildAssignment -import org.utbot.framework.codegen.model.tree.buildCgForEachLoop -import org.utbot.framework.codegen.model.tree.buildDeclaration -import org.utbot.framework.codegen.model.tree.buildDoWhileLoop -import org.utbot.framework.codegen.model.tree.buildForLoop -import org.utbot.framework.codegen.model.tree.buildTryCatch -import org.utbot.framework.codegen.model.tree.buildWhileLoop -import org.utbot.framework.codegen.model.util.buildExceptionHandler -import org.utbot.framework.codegen.model.util.isAccessibleFrom -import org.utbot.framework.codegen.model.util.nullLiteral -import org.utbot.framework.codegen.model.util.resolve +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.builtin.forName +import org.utbot.framework.codegen.domain.builtin.mockMethodId +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.util.isAccessibleFrom +import org.utbot.framework.codegen.util.nullLiteral +import org.utbot.framework.codegen.util.resolve import org.utbot.framework.plugin.api.BuiltinClassId import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ExecutableId @@ -56,14 +18,14 @@ import org.utbot.framework.plugin.api.util.isSubtypeOf import org.utbot.framework.plugin.api.util.objectArrayClassId import org.utbot.framework.plugin.api.util.objectClassId import fj.data.Either -import org.utbot.framework.codegen.model.constructor.builtin.getDeclaredConstructor -import org.utbot.framework.codegen.model.constructor.builtin.getDeclaredField -import org.utbot.framework.codegen.model.constructor.builtin.getDeclaredMethod -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getCallableAccessManagerBy -import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getNameGeneratorBy -import org.utbot.framework.codegen.model.tree.CgArrayInitializer -import org.utbot.framework.codegen.model.tree.CgGetJavaClass -import org.utbot.framework.codegen.model.tree.CgIsInstance +import org.utbot.framework.codegen.domain.builtin.getDeclaredConstructor +import org.utbot.framework.codegen.domain.builtin.getDeclaredField +import org.utbot.framework.codegen.domain.builtin.getDeclaredMethod +import org.utbot.framework.codegen.domain.models.* +import org.utbot.framework.codegen.domain.models.AnnotationTarget +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.tree.CgComponents.getCallableAccessManagerBy +import org.utbot.framework.codegen.tree.CgComponents.getNameGeneratorBy import org.utbot.framework.plugin.api.ConstructorId import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.MethodId @@ -73,12 +35,16 @@ import org.utbot.framework.plugin.api.util.fieldClassId import org.utbot.framework.plugin.api.util.isPrimitive import org.utbot.framework.plugin.api.util.methodClassId import org.utbot.framework.plugin.api.util.denotableType +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.isEnum +import org.utbot.framework.plugin.api.util.supertypeOfAnonymousClass import java.lang.reflect.Constructor import java.lang.reflect.Method import kotlin.reflect.KFunction import kotlin.reflect.jvm.kotlinFunction interface CgStatementConstructor { + fun newVar(baseType: ClassId, baseName: String? = null, init: () -> CgExpression): CgVariable = newVar(baseType, model = null, baseName, isMutable = false, init) @@ -160,11 +126,18 @@ interface CgStatementConstructor { fun lambda(type: ClassId, vararg parameters: CgVariable, body: () -> Unit): CgAnonymousFunction - fun annotation(classId: ClassId, argument: Any?): CgAnnotation - fun annotation(classId: ClassId, namedArguments: List>): CgAnnotation - fun annotation( + fun addAnnotation(classId: ClassId, argument: Any?, target: AnnotationTarget): CgAnnotation + + fun addAnnotation( classId: ClassId, - buildArguments: MutableList>.() -> Unit = {} + namedArguments: List, + target: AnnotationTarget, + ): CgAnnotation + + fun addAnnotation( + classId: ClassId, + target: AnnotationTarget, + buildArguments: MutableList>.() -> Unit = {}, ): CgAnnotation fun returnStatement(expression: () -> CgExpression) @@ -236,7 +209,7 @@ internal class CgStatementConstructorImpl(context: CgContext) : } classRef?.let { declaredClassRefs = declaredClassRefs.put(it, declaration.variable) } - updateVariableScope(declaration.variable, model) + rememberVariableForModel(declaration.variable, model) return Either.left(declaration) } @@ -428,33 +401,49 @@ internal class CgStatementConstructorImpl(context: CgContext) : } } - override fun annotation(classId: ClassId, argument: Any?): CgAnnotation { - val annotation = CgSingleArgAnnotation(classId, argument.resolve()) + override fun addAnnotation(classId: ClassId, argument: Any?, target: AnnotationTarget): CgAnnotation { + val annotation = CgSingleArgAnnotation(classId, argument.resolve(), target) addAnnotation(annotation) return annotation } - override fun annotation(classId: ClassId, namedArguments: List>): CgAnnotation { - val annotation = CgMultipleArgsAnnotation( - classId, - namedArguments.mapTo(mutableListOf()) { (name, value) -> CgNamedAnnotationArgument(name, value) } - ) + override fun addAnnotation( + classId: ClassId, + namedArguments: List, + target: AnnotationTarget, + ): CgAnnotation { + val annotation = CgMultipleArgsAnnotation(classId, namedArguments.toMutableList(), target) + addAnnotation(annotation) return annotation } - override fun annotation( + override fun addAnnotation( classId: ClassId, - buildArguments: MutableList>.() -> Unit + target: AnnotationTarget, + buildArguments: MutableList>.() -> Unit, ): CgAnnotation { val arguments = mutableListOf>() .apply(buildArguments) .map { (name, value) -> CgNamedAnnotationArgument(name, value) } - val annotation = CgMultipleArgsAnnotation(classId, arguments.toMutableList()) + val annotation = CgMultipleArgsAnnotation(classId, arguments.toMutableList(), target) + addAnnotation(annotation) return annotation } + private fun addAnnotation(annotation: CgAnnotation) { + when (annotation.target) { + AnnotationTarget.Method -> collectedMethodAnnotations.add(annotation) + AnnotationTarget.Class -> currentTestClassContext.collectedTestClassAnnotations.add(annotation) + // TODO: possibly introduce some scope cache like in methods and constructions processing. + // It may allow to avoid return type in CgAnnotation method. + AnnotationTarget.Field -> { } + } + + importIfNeeded(annotation.classId) + } + override fun returnStatement(expression: () -> CgExpression) { currentBlock += CgReturnStatement(expression()) } @@ -465,7 +454,7 @@ internal class CgStatementConstructorImpl(context: CgContext) : CgThrowStatement(exception()).also { currentBlock += it } override fun emptyLine() { - currentBlock += CgEmptyLine() + currentBlock += CgEmptyLine } /** @@ -480,11 +469,17 @@ internal class CgStatementConstructorImpl(context: CgContext) : override fun declareVariable(type: ClassId, name: String): CgVariable = CgVariable(name, type).also { - updateVariableScope(it) + rememberVariableForModel(it) } override fun wrapTypeIfRequired(baseType: ClassId): ClassId = - if (baseType.isAccessibleFrom(testClassPackageName)) baseType else objectClassId + when { + baseType.isAccessibleFrom(testClassPackageName) -> baseType + baseType.isAnonymous && baseType.supertypeOfAnonymousClass.isAccessibleFrom(testClassPackageName) -> + baseType.supertypeOfAnonymousClass + + else -> objectClassId + } // utils @@ -503,11 +498,13 @@ internal class CgStatementConstructorImpl(context: CgContext) : } private fun guardEnumConstantAccess(access: CgEnumConstantAccess): ExpressionWithType { - val (enumClass, constant) = access + val (enumAccessClass, constant) = access - return if (enumClass.isAccessibleFrom(testClassPackageName)) { - ExpressionWithType(enumClass, access) + return if (enumAccessClass.isAccessibleFrom(testClassPackageName)) { + ExpressionWithType(enumAccessClass, access) } else { + val enumClass: ClassId = + if (enumAccessClass.isEnum) enumAccessClass else enumAccessClass.outerClass!!.id val enumClassVariable = newVar(classCgClassId) { classClassId[forName](enumClass.name) } @@ -550,11 +547,12 @@ internal class CgStatementConstructorImpl(context: CgContext) : // TODO: in order to check whether we are working with a TypeVariable or not // val returnType = runCatching { call.executableId.kotlinFunction }.getOrNull()?.returnType?.javaType - if (call.executableId != mockMethodId) return guardExpression(baseType, call) - - // call represents a call to mock() method - val wrappedType = wrapTypeIfRequired(baseType) - return ExpressionWithType(wrappedType, call) + if (call.executableId == mockMethodId && call.arguments[0] is CgGetJavaClass) { + // call represents a call to mock() method + val wrappedType = wrapTypeIfRequired(baseType) + return ExpressionWithType(wrappedType, call) + } + return guardExpression(baseType, call) } override fun guardExpression(baseType: ClassId, expression: CgExpression): ExpressionWithType { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgVariableConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgVariableConstructor.kt new file mode 100644 index 0000000000..922c3f1a65 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgVariableConstructor.kt @@ -0,0 +1,590 @@ +package org.utbot.framework.codegen.tree + +import mu.KotlinLogging +import org.utbot.common.isStatic +import org.utbot.framework.codegen.domain.builtin.forName +import org.utbot.framework.codegen.domain.builtin.setArrayElement +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.* +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.tree.CgComponents.getCallableAccessManagerBy +import org.utbot.framework.codegen.tree.CgComponents.getMockFrameworkManagerBy +import org.utbot.framework.codegen.tree.CgComponents.getNameGeneratorBy +import org.utbot.framework.codegen.tree.CgComponents.getStatementConstructorBy +import org.utbot.framework.codegen.util.at +import org.utbot.framework.codegen.util.canBeSetFrom +import org.utbot.framework.codegen.util.canBeSetViaSetterFrom +import org.utbot.framework.codegen.util.fieldThatIsGotWith +import org.utbot.framework.codegen.util.fieldThatIsSetWith +import org.utbot.framework.codegen.util.inc +import org.utbot.framework.codegen.util.isAccessibleFrom +import org.utbot.framework.codegen.util.lessThan +import org.utbot.framework.codegen.util.nullLiteral +import org.utbot.framework.codegen.util.resolve +import org.utbot.framework.codegen.util.setter +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ConstructorId +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.UtClassRefModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtCustomModel +import org.utbot.framework.plugin.api.UtDirectGetFieldModel +import org.utbot.framework.plugin.api.UtDirectSetFieldModel +import org.utbot.framework.plugin.api.UtEnumConstantModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtLambdaModel +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.UtReferenceModel +import org.utbot.framework.plugin.api.UtStatementCallModel +import org.utbot.framework.plugin.api.UtVoidModel +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.booleanWrapperClassId +import org.utbot.framework.plugin.api.util.charClassId +import org.utbot.framework.plugin.api.util.classClassId +import org.utbot.framework.plugin.api.util.defaultValueModel +import org.utbot.framework.plugin.api.util.executable +import org.utbot.framework.plugin.api.util.jField +import org.utbot.framework.plugin.api.util.findFieldByIdOrNull +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.isEnum +import org.utbot.framework.plugin.api.util.isPrimitiveWrapperOrString +import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.framework.plugin.api.util.primitiveWrappers +import org.utbot.framework.plugin.api.util.primitives +import org.utbot.framework.plugin.api.util.replaceWithWrapperIfPrimitive +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.framework.plugin.api.util.supertypeOfAnonymousClass +import org.utbot.framework.plugin.api.util.wrapperByPrimitive + +/** + * Constructs CgValue or CgVariable given a UtModel + */ +open class CgVariableConstructor(val context: CgContext) : + CgContextOwner by context, + CgCallableAccessManager by getCallableAccessManagerBy(context), + CgStatementConstructor by getStatementConstructorBy(context) { + + companion object { + private val logger = KotlinLogging.logger {} + } + + private val nameGenerator = getNameGeneratorBy(context) + private val mockFrameworkManager = getMockFrameworkManagerBy(context) + + /** + * Take already created CgValue or construct either a new [CgVariable] or new [CgLiteral] for the given model. + * + * Here we consider reference models and other models separately. + * + * It is important, because variables for reference models are constructed differently from the others. + * The difference is that the reference model variables are put into [valueByModel] cache + * not after the whole object is set up, but right after the variable has been initialized. + * For example, when we do `A a = new A()` we already have the variable, and it is stored in [valueByModel]. + * After that, we set all the necessary fields and call all the necessary methods. + * + * On the other hand, non-reference model variables are put into [valueByModel] after they have been fully set up. + * + * The point of this early caching of reference model variables is that classes may be recursive. + * For example, we may want to create a looped object: `a.value = a`. + * If we did not cache the variable `a` on its instantiation, then we would try to create `a` from its model + * from scratch. Since this object is recursively pointing to itself, this process would lead + * to a stack overflow. Specifically to avoid this, we cache reference model variables right after + * their instantiation. + * + * We use [valueByModelId] for [UtReferenceModel] by id to not create new variable in case state before + * was not transformed. + */ + open fun getOrCreateVariable(model: UtModel, name: String? = null): CgValue = + valueByUtModelWrapper.getOrPut(model.wrap()) { + constructValueByModel(model, name) + } + + private fun constructValueByModel(model: UtModel, name: String?): CgValue { + // name could be taken from existing names, or be specified manually, or be created from generator + val baseName = name ?: nameGenerator.nameFrom(model.classId) + + return when (model) { + is UtCompositeModel -> constructComposite(model, baseName) + is UtAssembleModel -> constructAssemble(model, baseName) + is UtArrayModel -> constructArray(model, baseName) + is UtEnumConstantModel -> constructEnumConstant(model, baseName) + is UtClassRefModel -> constructClassRef(model, baseName) + is UtLambdaModel -> constructLambda(model, baseName) + is UtNullModel -> nullLiteral() + is UtPrimitiveModel -> CgLiteral(model.classId, model.value) + is UtCustomModel -> { + logger.error { "Unexpected behaviour: value for UtCustomModel [$model] is constructed by base CgVariableConstructor" } + constructValueByModel( + model.origin ?: error("Can't construct value for UtCustomModel without origin [$model]"), name + ) + } + is UtReferenceModel -> error("Unexpected UtReferenceModel: ${model::class}") + is UtVoidModel -> error("Unexpected UtVoidModel: ${model::class}") + else -> error("Unexpected UtModel: ${model::class}") + } + } + + private fun constructLambda(model: UtLambdaModel, baseName: String): CgVariable { + val lambdaMethodId = model.lambdaMethodId + val capturedValues = model.capturedValues + return newVar(model.samType, baseName) { + if (lambdaMethodId.isStatic) { + constructStaticLambda(model, capturedValues) + } else { + constructLambda(model, capturedValues) + } + } + } + + private fun constructStaticLambda(model: UtLambdaModel, capturedValues: List): CgMethodCall { + val capturedArguments = capturedValues.map { + utilMethodProvider.capturedArgumentConstructorId(getClassOf(it.classId), getOrCreateVariable(it)) + } + return utilsClassId[buildStaticLambda]( + getClassOf(model.samType), + getClassOf(model.declaringClass), + model.lambdaName, + *capturedArguments.toTypedArray() + ) + } + + private fun constructLambda(model: UtLambdaModel, capturedValues: List): CgMethodCall { + require(capturedValues.isNotEmpty()) { + "Non-static lambda must capture `this` instance, so there must be at least one captured value" + } + val capturedThisInstance = getOrCreateVariable(capturedValues.first()) + val capturedArguments = capturedValues + .subList(1, capturedValues.size) + .map { utilMethodProvider.capturedArgumentConstructorId(getClassOf(it.classId), getOrCreateVariable(it)) } + return utilsClassId[buildLambda]( + getClassOf(model.samType), + getClassOf(model.declaringClass), + model.lambdaName, + capturedThisInstance, + *capturedArguments.toTypedArray() + ) + } + + private fun constructComposite(model: UtCompositeModel, baseName: String): CgVariable { + val obj = if (model.isMock) { + mockFrameworkManager.createMockFor(model, baseName) + } else { + val modelType = model.classId + val variableType = if (modelType.isAnonymous) modelType.supertypeOfAnonymousClass else modelType + newVar(variableType, baseName) { utilsClassId[createInstance](model.classId.name) } + } + + valueByUtModelWrapper[model.wrap()] = obj + + require(obj.type !is BuiltinClassId) { + "Unexpected BuiltinClassId ${obj.type} found while constructing from composite model" + } + + for ((fieldId, fieldModel) in model.fields) { + val variableForField = getOrCreateVariable(fieldModel, name = fieldId.name) + if (!variableForField.hasDefaultValue()) + setFieldValue(obj, fieldId, variableForField) + } + return obj + } + + fun setFieldValue(obj: CgValue, fieldId: FieldId, valueForField: CgValue) { + val field = fieldId.jField + val fieldFromVariableSpecifiedType = obj.type.findFieldByIdOrNull(fieldId) + + // we cannot set field directly if variable declared type does not have such field + // or we cannot directly create variable for field with the specified type (it is private, for example) + // Example: + // Object heapByteBuffer = createInstance("java.nio.HeapByteBuffer"); + // branchRegisterRequest.byteBuffer = heapByteBuffer; + // byteBuffer is field of type ByteBuffer and upper line is incorrect + val canFieldBeDirectlySetByVariableAndFieldTypeRestrictions = + fieldFromVariableSpecifiedType != null && fieldFromVariableSpecifiedType.type.id == valueForField.type + if (canFieldBeDirectlySetByVariableAndFieldTypeRestrictions && fieldId.canBeSetFrom(context, obj.type)) { + // TODO: check if it is correct to use declaringClass of a field here + val fieldAccess = if (field.isStatic) CgStaticFieldAccess(fieldId) else CgFieldAccess(obj, fieldId) + fieldAccess `=` valueForField + } else if (context.codegenLanguage == CodegenLanguage.JAVA && + !field.isStatic && fieldId.canBeSetViaSetterFrom(context) + ) { + +obj[fieldId.setter](valueForField) + } else { + // composite models must not have info about static fields, hence only non-static fields are set here + +utilsClassId[setField](obj, fieldId.declaringClass.name, fieldId.name, valueForField) + } + } + + private fun CgValue.hasDefaultValue(): Boolean { + if (this !is CgLiteral) { + return false; + } + + return when { + this.value == null -> true + (this.type == booleanClassId || this.type == booleanWrapperClassId) && this.value == false -> true + (this.type in primitives || this.type in primitiveWrappers) && this.value == 0 -> true + else -> false + } + } + + private fun constructAssemble(model: UtAssembleModel, baseName: String?): CgValue { + instantiateAssembleModel(model, baseName) + return constructAssembleForVariable(model) + } + + private fun instantiateAssembleModel(model: UtAssembleModel, baseName: String?) { + val statementCall = model.instantiationCall + val executable = statementCall.statement + val params = statementCall.params + val singleParamOrNull = params.singleOrNull() + + val isWrappingConstructor = + executable is ConstructorId && isPrimitiveWrapperOrString(model.classId) && singleParamOrNull != null && + replaceWithWrapperIfPrimitive(singleParamOrNull.classId) == model.classId + + // Don't use redundant constructors for primitives and String + val initExpr = when { + isWrappingConstructor -> cgLiteralForWrapper(params) + executable is ConstructorId && model.classId == stringClassId && singleParamOrNull is UtArrayModel + && singleParamOrNull.classId.elementClassId == charClassId + && List(singleParamOrNull.length) { i -> singleParamOrNull[i] }.all { it is UtPrimitiveModel } -> { + val stringValue = List(singleParamOrNull.length) { i -> (singleParamOrNull[i] as UtPrimitiveModel).value } + .joinToString(separator = "") + cgLiteralForWrapper(params = listOf(UtPrimitiveModel(stringValue))) + } + else -> createCgExecutableCallFromUtExecutableCall(statementCall) + } + + newVar(model.classId, model, baseName) { initExpr } + .also { valueByUtModelWrapper[model.wrap()] = it } + } + + fun constructAssembleForVariable(model: UtAssembleModel): CgValue { + for (statementModel in model.modificationsChain) { + when (statementModel) { + is UtDirectSetFieldModel -> { + val instance = getOrCreateVariable(statementModel.instance) + // fields here are supposed to be accessible, so we assign them directly without any checks + instance[statementModel.fieldId] `=` getOrCreateVariable( + model = statementModel.fieldModel, + name = statementModel.fieldId.name, + ) + } + is UtStatementCallModel -> { + val call = createCgExecutableCallFromUtExecutableCall(statementModel) + val equivalentFieldAccess = replaceCgExecutableCallWithFieldAccessIfNeeded(call) + val thrownException = statementModel.thrownConcreteException + + if (equivalentFieldAccess != null) +equivalentFieldAccess + else if (thrownException != null) { + +tryBlock { +call } + .catch(thrownException) { /* do nothing */ } + } else +call + } + } + } + + return valueByUtModelWrapper.getValue(model.wrap()) + } + + private fun createCgExecutableCallFromUtExecutableCall(statementModel: UtStatementCallModel): CgExecutableCall = + when (statementModel) { + is UtExecutableCallModel -> { + val executable = statementModel.executable + val paramNames = runCatching { + executable.executable.parameters.map { if (it.isNamePresent) it.name else null } + }.getOrNull() + val params = statementModel.params + val caller = statementModel.instance?.let { getOrCreateVariable(it) } + val args = params.mapIndexed { i, param -> + getOrCreateVariable(param, name = paramNames?.getOrNull(i)) + } + + when (executable) { + is MethodId -> caller[executable](*args.toTypedArray()) + is ConstructorId -> executable(*args.toTypedArray()) + } + } + is UtDirectGetFieldModel -> { + val instance = getOrCreateVariable(statementModel.instance) + val fieldAccess = statementModel.fieldAccess + utilsClassId[getFieldValue](instance, fieldAccess.fieldId.declaringClass.canonicalName, fieldAccess.fieldId.name) + } + } + + /** + * If executable is getter/setter that should be syntactically replaced with field access + * (e.g., getter/setter generated by Kotlin in Kotlin code), this method returns [CgStatement] + * with which [call] should be replaced. + * + * Otherwise, returns null. + */ + private fun replaceCgExecutableCallWithFieldAccessIfNeeded(call: CgExecutableCall): CgStatement? { + when (context.codegenLanguage) { + CodegenLanguage.JAVA -> return null + CodegenLanguage.KOTLIN -> { + if (call !is CgMethodCall) + return null + + val caller = call.caller ?: return null + + caller.type.fieldThatIsSetWith(call.executableId)?.let { + return CgAssignment(caller[it], call.arguments.single()) + } + caller.type.fieldThatIsGotWith(call.executableId)?.let { + require(call.arguments.isEmpty()) { + "Method $call was detected as getter for $it, but its arguments list isn't empty" + } + return caller[it] + } + + return null + } + } + } + + /** + * Makes a replacement of constructor call to instantiate a primitive wrapper + * with direct setting of the value. The reason is that in Kotlin constructors + * of primitive wrappers are private. + */ + private fun cgLiteralForWrapper(params: List): CgLiteral { + val paramModel = params.singleOrNull() + require(paramModel is UtPrimitiveModel) { "Incorrect param models for primitive wrapper" } + + val classId = wrapperByPrimitive[paramModel.classId] + ?: if (paramModel.classId == stringClassId) { + stringClassId + } else { + error("${paramModel.classId} is not a primitive wrapper or a string") + } + + return CgLiteral(classId, paramModel.value) + } + + 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 allPrimitives = elementModels.all { it is UtPrimitiveModel } + val allNulls = elementModels.all { it is UtNullModel } + // we can use array initializer if all elements are primitives or all of them are null, + // and the size of an array is not greater than the fixed maximum size + val canInitWithValues = (allPrimitives || allNulls) && elementModels.size <= MAX_ARRAY_INITIALIZER_SIZE + + val initializer = if (canInitWithValues) { + val elements = elementModels.map { model -> + when (model) { + is UtPrimitiveModel -> model.value.resolve() + is UtNullModel -> null.resolve() + else -> error("Non primitive or null model $model is unexpected in array initializer") + } + } + arrayInitializer(arrayModel.classId, elementType, elements) + } else { + CgAllocateArray(arrayModel.classId, elementType, arrayModel.length) + } + + val array = newVar(arrayModel.classId, baseName) { initializer } + + valueByUtModelWrapper[arrayModel.wrap()] = 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 = arrayModel[0] + if (elementModel isNotDefaultValueOf elementType) { + array.setArrayElement(0, getOrCreateVariable(elementModel)) + } + } else { + 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) + } + } + + // choose all not default values + val defaultValue = if (arrayModel.constModel isDefaultValueOf elementType) { + arrayModel.constModel + } else { + elementType.defaultValueModel() + } + arrayModel.stores.entries.filter { (_, element) -> element != defaultValue } + } + + // set all values from stores manually + indexedValuesFromStores + .sortedBy { it.key } + .forEach { (index, element) -> array.setArrayElement(index, getOrCreateVariable(element)) } + } + + return array + } + + /** + * Splits sorted by indices pairs of index and value from stores to continuous by index chunks + * [indexedValuesFromStores] have to be sorted by key + * + * Сan not be used now but will be useful in case of storing stores in generated code + */ + @Suppress("unused") + private fun splitSettingFromStoresToForLoops( + array: CgVariable, + indexedValuesFromStores: List> + ) { + val ranges = mutableListOf() + + var start = 0 + for (i in 0 until indexedValuesFromStores.lastIndex) { + if (indexedValuesFromStores[i + 1].key - indexedValuesFromStores[i].key > 1) { + ranges += start..i + start = i + 1 + } + if (i == indexedValuesFromStores.lastIndex - 1) { + ranges += start..indexedValuesFromStores.lastIndex + } + } + + for (range in ranges) { + // IntRange stores end inclusively but sublist takes it exclusively + setStoresRange(array, indexedValuesFromStores.subList(range.first, range.last + 1)) + } + } + + /** + * [indexedValuesFromStores] have to be continuous sorted range + */ + private fun setStoresRange( + array: CgVariable, + indexedValuesFromStores: List> + ) { + if (indexedValuesFromStores.size < 3) { + // range is too small, better set manually + indexedValuesFromStores.forEach { (index, element) -> + array.setArrayElement(index, getOrCreateVariable(element)) + } + } else { + val minIndex = indexedValuesFromStores.first().key + val maxIndex = indexedValuesFromStores.last().key + + var indicesIndex = 0 + // we use until form of for loop so need to shift upper border + basicForLoop(start = minIndex, until = maxIndex + 1) { i -> + // use already sorted indices + val (_, value) = indexedValuesFromStores[indicesIndex++] + array.setArrayElement(i, getOrCreateVariable(value)) + } + } + } + + private fun constructEnumConstant(model: UtEnumConstantModel, baseName: String?): CgVariable { + val classId = model.classId + + require(classId.isEnum || + classId.isAnonymous && classId.outerClass?.isEnum == true) { + "Enum constant model $model should be a enum or an anonymous class that overrides enum methods" + } + + return newVar(classId, baseName) { + CgEnumConstantAccess(classId, model.value.name) + } + } + + private fun constructClassRef(model: UtClassRefModel, baseName: String?): CgVariable { + val classId = model.value + val init = if (classId.isAccessibleFrom(testClassPackageName)) { + CgGetJavaClass(classId) + } else { + classClassId[forName](classId.name) + } + + return newVar(Class::class.id, baseName) { init } + } + + private fun basicForLoop(start: Any, until: Any, body: (i: CgExpression) -> Unit) { + forLoop { + val (i, init) = loopInitialization(intClassId, "i", start.resolve()) + initialization = init + condition = i lessThan until.resolve() + update = i.inc() + statements = block { body(i) } + } + } + + /** + * A for-loop performing 'n' iterations starting with 0 + * + * for (int i = 0; i < n; i++) { + * ... + * } + */ + private fun basicForLoop(until: Any, body: (i: CgExpression) -> Unit) { + basicForLoop(start = 0, until, body) + } + + /** + * Create loop initializer expression + */ + internal fun loopInitialization( + variableType: ClassId, + baseVariableName: String, + initializer: Any? + ): Pair { + val declaration = CgDeclaration(variableType, baseVariableName.toVarName(), initializer.resolve()) + val variable = declaration.variable + rememberVariableForModel(variable) + return variable to declaration + } + + /** + * @receiver must represent a variable containing an array value. + * If an array was created with reflection, then the variable is of [Object] type. + * Otherwise, the variable is of the actual array type. + * + * Both cases are considered here. + * If the variable is [Object], we use reflection method to set an element. + * Otherwise, we set an element directly. + * + */ + private fun CgVariable.setArrayElement(index: Any, value: CgValue) { + val i = index.resolve() + // we have to use reflection if we cannot easily cast array element to array type + // (in case array does not have array type (maybe just object) or element is private class) + if (!type.isArray || (type != value.type && !value.type.isAccessibleFrom(testClassPackageName))) { + +java.lang.reflect.Array::class.id[setArrayElement](this, i, value) + } else { + val arrayElement = if (type == value.type) { + value + } else { + typeCast(type.elementClassId!!, value, isSafetyCast = true) + } + + this.at(i) `=` arrayElement + } + } + + private fun String.toVarName(): String = nameGenerator.variableName(this) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/ConstructorUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/ConstructorUtils.kt new file mode 100644 index 0000000000..bb7bd6c343 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/ConstructorUtils.kt @@ -0,0 +1,468 @@ +package org.utbot.framework.codegen.tree + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentSet +import org.utbot.framework.codegen.domain.RegularImport +import org.utbot.framework.codegen.domain.StaticImport +import org.utbot.framework.codegen.domain.builtin.setArrayElement +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgAllocateInitializedArray +import org.utbot.framework.codegen.domain.models.CgArrayInitializer +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgTypeCast +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.util.at +import org.utbot.framework.codegen.util.isAccessibleFrom +import org.utbot.framework.fields.ArrayElementAccess +import org.utbot.framework.fields.FieldAccess +import org.utbot.framework.fields.FieldPath +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.CgClassId +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.UtArrayModel +import org.utbot.framework.plugin.api.UtExecution +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.WildcardTypeParameter +import org.utbot.framework.plugin.api.util.arrayTypeOf +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.builtinStaticMethodId +import org.utbot.framework.plugin.api.util.byteClassId +import org.utbot.framework.plugin.api.util.charClassId +import org.utbot.framework.plugin.api.util.denotableType +import org.utbot.framework.plugin.api.util.doubleClassId +import org.utbot.framework.plugin.api.util.enclosingClass +import org.utbot.framework.plugin.api.util.executable +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.isRefType +import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.framework.plugin.api.util.isSubtypeOf +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.longClassId +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.plugin.api.util.shortClassId +import org.utbot.framework.plugin.api.util.signature +import org.utbot.framework.plugin.api.util.underlyingType +import soot.Scene +import java.lang.reflect.Method +import java.lang.reflect.Modifier + +data class EnvironmentFieldStateCache( + val thisInstance: FieldStateCache, + val arguments: Array, + val classesWithStaticFields: MutableMap +) { + companion object { + fun emptyCacheFor(execution: UtExecution): EnvironmentFieldStateCache { + val argumentsCache = Array(execution.stateBefore.parameters.size) { FieldStateCache() } + + val staticFields = execution.stateBefore.statics.keys + val classesWithStaticFields = staticFields.groupBy { it.declaringClass }.keys + val staticFieldsCache = mutableMapOf().apply { + for (classId in classesWithStaticFields) { + put(classId, FieldStateCache()) + } + } + + return EnvironmentFieldStateCache( + thisInstance = FieldStateCache(), + arguments = argumentsCache, + classesWithStaticFields = staticFieldsCache + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EnvironmentFieldStateCache + + if (thisInstance != other.thisInstance) return false + if (!arguments.contentEquals(other.arguments)) return false + if (classesWithStaticFields != other.classesWithStaticFields) return false + + return true + } + + override fun hashCode(): Int { + var result = thisInstance.hashCode() + result = 31 * result + arguments.contentHashCode() + result = 31 * result + classesWithStaticFields.hashCode() + return result + } +} + +class FieldStateCache { + val before: MutableMap = mutableMapOf() + val after: MutableMap = mutableMapOf() + + val paths: List + get() = (before.keys union after.keys).toList() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FieldStateCache + + if (before != other.before) return false + if (after != other.after) return false + + return true + } + + override fun hashCode(): Int { + var result = before.hashCode() + result = 31 * result + after.hashCode() + return result + } +} + +data class CgFieldState(val variable: CgVariable, val model: UtModel) + +data class ExpressionWithType(val type: ClassId, val expression: CgExpression) + +/** + * Check if a method is an util method of the current class + */ +internal fun CgContextOwner.isUtil(method: MethodId): Boolean { + return method in utilMethodProvider.utilMethodIds +} + +val classCgClassId = CgClassId(Class::class.id, typeParameters = WildcardTypeParameter, isNullable = false) + +/** + * A [MethodId] to add an item into [ArrayList]. + */ +internal val addToListMethodId: MethodId + get() = methodId( + classId = ArrayList::class.id, + name = "add", + returnType = booleanClassId, + arguments = arrayOf(Object::class.id), + ) + +/** + * A [ClassId] of class `org.junit.jupiter.params.provider.Arguments` + */ +internal val argumentsClassId: BuiltinClassId + get() = BuiltinClassId( + simpleName = "Arguments", + canonicalName = "org.junit.jupiter.params.provider.Arguments", + packageName = "org.junit.jupiter.params.provider" + ) + +/** + * A [MethodId] to call JUnit Arguments method. + */ +internal val argumentsMethodId: BuiltinMethodId + get() = builtinStaticMethodId( + classId = argumentsClassId, + name = "arguments", + returnType = argumentsClassId, + // vararg of Objects + arguments = arrayOf(objectArrayClassId) + ) + +internal fun getStaticFieldVariableName(owner: ClassId, path: FieldPath): String { + val elements = mutableListOf() + elements += owner.simpleName.decapitalize() + // TODO: check how capitalize() works with numeric strings e.g. "0" + elements += path.toStringList().map { it.capitalize() } + return elements.joinToString("") +} + +internal fun getFieldVariableName(owner: CgValue, path: FieldPath): String { + val elements = mutableListOf() + if (owner is CgVariable) { + elements += owner.name + } + // TODO: check how capitalize() works with numeric strings e.g. "0" + elements += path.toStringList().map { it.capitalize() } + if (elements.size > 0) { + elements[0] = elements[0].decapitalize() + } + return elements.joinToString("") +} + +private fun FieldPath.toStringList(): List = + elements.map { + when (it) { + is FieldAccess -> it.field.name + is ArrayElementAccess -> it.index.toString() + } + } + +internal fun infiniteInts(): Sequence = + generateSequence(1) { it + 1 } + +internal const val MAX_ARRAY_INITIALIZER_SIZE = 10 + +private val simpleNamesNotRequiringImports by lazy { findSimpleNamesNotRequiringImports() } + +/** + * Some class names do not require imports, but may lead to simple names clash. + * For example, custom class `Compiler` may clash with [java.lang.Compiler]. + */ +private fun findSimpleNamesNotRequiringImports(): Set = + Scene.v().classes + .filter { it.packageName == "java.lang" } + .mapNotNullTo(mutableSetOf()) { it.shortName } + +/** + * Checks if we have already imported a class with such simple name. + * If so, we cannot import [type] (because it will be used with simple name and will be clashed with already imported) + * and should use its fully qualified name instead. + */ +private fun CgContextOwner.doesNotHaveSimpleNameClash(type: ClassId): Boolean = + importedClasses.none { it.simpleName == type.simpleName } && type.simpleName !in simpleNamesNotRequiringImports + +fun CgContextOwner.importIfNeeded(type: ClassId) { + // TODO: for now we consider that tests are generated in the same package as CUT, but this may change + val underlyingType = type.underlyingType + + underlyingType + .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 + ?.takeIf { doesNotHaveSimpleNameClash(it) } + ?.let { + importedClasses += it + collectedImports += it.toImport() + } + + // for nested classes we need to import enclosing class + if (underlyingType.isNested) { + importIfNeeded(underlyingType.enclosingClass!!) + } +} + +fun CgContextOwner.importIfNeeded(method: MethodId) { + val name = method.name + val packageName = method.classId.packageName + method.takeIf { it.isStatic && packageName != testClassPackageName && packageName != "java.lang" } + .takeIf { importedStaticMethods.none { it.name == name } } + // do not import method under test in order to specify the declaring class directly for its calls + .takeIf { currentExecutableUnderTest != method } + ?.let { + importedStaticMethods += method + collectedImports += StaticImport(method.classId.canonicalName, method.name) + } +} + +/** + * Casts [expression] to [targetType]. + * + * This method uses [denotableType] of the given [targetType] to ensure + * that we are using a denotable type in the type cast. + * + * Specifically, if we attempt to do a type cast to an anonymous class, + * then we will actually perform a type cast to its supertype. + * + * @see denotableType + * + * @param isSafetyCast shows if we should render "as?" instead of "as" in Kotlin + */ +internal fun CgContextOwner.typeCast( + targetType: ClassId, + expression: CgExpression, + isSafetyCast: Boolean = false +): CgExpression { + val denotableTargetType = targetType.denotableType + importIfNeeded(denotableTargetType) + return CgTypeCast(denotableTargetType, expression, isSafetyCast) +} + +/** + * Casts [expression] to [targetType] only if downcast is needed, + * e.g. this method will cast `Collection` to `Set`, but not vice-versa. + * + * @see typeCast + */ +internal fun CgContextOwner.downcastIfNeeded( + targetType: ClassId, + expression: CgExpression, + isSafetyCast: Boolean = false +): CgExpression = + if (expression.type isSubtypeOf targetType) expression + else typeCast(targetType, expression, isSafetyCast) + +/** + * Sets an element of arguments array in parameterized test, + * if test framework represents arguments as array. + */ +internal fun T.setArgumentsArrayElement( + array: CgVariable, + index: Int, + value: CgExpression, + constructor: CgStatementConstructor +) where T : CgContextOwner, T: CgCallableAccessManager { + when (array.type) { + objectClassId -> { + +java.lang.reflect.Array::class.id[setArrayElement](array, index, value) + } + else -> with(constructor) { array.at(index) `=` value } + } +} + +@Suppress("unused") +internal fun newArrayOf(elementType: ClassId, values: List): CgAllocateInitializedArray { + val arrayType = arrayTypeOf(elementType) + return CgAllocateInitializedArray(arrayInitializer(arrayType, elementType, values)) +} + +internal fun arrayInitializer(arrayType: ClassId, elementType: ClassId, values: List): CgArrayInitializer = + CgArrayInitializer(arrayType, elementType, values) + +internal fun Class<*>.overridesEquals(): Boolean = + when { + // Object does not override equals + this == Any::class.java -> false + id isSubtypeOf Map::class.id -> true + id isSubtypeOf Collection::class.id -> true + else -> declaredMethods.any { it.name == "equals" && it.parameterTypes.contentEquals(arrayOf(Any::class.java)) } + } + +/** + * Returns all methods of [this] class (including inherited), except base methods (i.e., if any method is overridden, + * only the latest overriding will be included). + * NOTE: for the reference [see also](https://stackoverflow.com/a/28408148) + */ +private fun Class<*>.allMethodsWithoutBaseMethods(): Set { + val collectedMethods = mutableSetOf(*methods) + val types = collectedMethods.map { it.signature }.associateWithTo(mutableMapOf()) { mutableSetOf() } + val access = Modifier.PUBLIC or Modifier.PROTECTED or Modifier.PRIVATE + + var currentClass: Class<*>? = this + while (currentClass != null) { + for (method in currentClass.declaredMethods) { + val modifiers = method.modifiers + + if (!Modifier.isStatic(modifiers)) { + when (modifiers and access) { + Modifier.PUBLIC -> continue + Modifier.PROTECTED -> { + if (types.putIfAbsent(method.signature, mutableSetOf()) != null) { + continue + } + } + Modifier.PRIVATE -> {} + else -> { // package-private + val pkg = types.computeIfAbsent(method.signature) { mutableSetOf() } + + if (pkg.isNotEmpty() && pkg.add(currentClass.getPackage())) { + break + } else { + continue + } + } + } + } + + collectedMethods += method + } + + currentClass = currentClass.superclass + } + + return collectedMethods +} + +// NOTE: this function does not consider executable return type because it is not important in our case +internal fun ClassId.getAmbiguousOverloadsOf(executableId: ExecutableId): Sequence { + val allExecutables = when (executableId) { + is MethodId -> { + // For method we should check all overloadings and inherited methods, but do not consider base methods + // in case of overriding (for example, for ArrayList#add we should not take List#add and AbstractCollection#add) + executableId.classId.jClass.allMethodsWithoutBaseMethods().map { it.executableId }.asSequence() + } + is ConstructorId -> allConstructors + } + + return allExecutables.filter { + it.name == executableId.name && it.parameters.size == executableId.executable.parameters.size + } +} + + +internal infix fun ClassId.hasAmbiguousOverloadsOf(executableId: ExecutableId): Boolean { + // TODO: for now we assume that builtin classes do not have ambiguous overloads + if (this is BuiltinClassId) return false + + return getAmbiguousOverloadsOf(executableId).toList().size > 1 +} + +private val defaultByPrimitiveType: Map = mapOf( + booleanClassId to false, + byteClassId to 0.toByte(), + charClassId to '\u0000', + shortClassId to 0.toShort(), + intClassId to 0, + longClassId to 0L, + floatClassId to 0.0f, + doubleClassId to 0.0 +) + +/** + * By 'default' here we mean a value that is used for a type in one of the two cases: + * - When we allocate an array of some type in the following manner: `new int[10]`, + * the array is filled with some value. In case of `int` this value is `0`, for `boolean` + * it is `false`, etc. + * - When we allocate an instance of some reference type e.g. `new A()` and the class `A` has field `int a`. + * If the constructor we use does not initialize `a` directly and `a` is not assigned to some value + * at the declaration site, then `a` will be assigned to some `default` value, e.g. `0` for `int`. + * + * Here we do not consider default values of nested arrays of multidimensional arrays, + * because they depend on the way the outer array is allocated: + * - An array allocated using `new int[10][]` will contain 10 `null` elements. + * - An array allocated using `new int[10][5]` will contain 10 arrays of 5 elements, + * where each element is `0`. + */ +internal infix fun UtModel.isDefaultValueOf(type: ClassId): Boolean = + when (this) { + is UtNullModel -> type.isRefType // null is default for ref types + is UtPrimitiveModel -> value == defaultByPrimitiveType[type] + else -> false + } + +internal infix fun UtModel.isNotDefaultValueOf(type: ClassId): Boolean = !this.isDefaultValueOf(type) + +/** + * If the model contains a store for the given [index], return the model of this store. + * Otherwise, return a [UtArrayModel.constModel] of this array model. + */ +internal operator fun UtArrayModel.get(index: Int): UtModel = stores[index] ?: constModel + +fun ClassId.toImport(): RegularImport = RegularImport(packageName, simpleNameWithEnclosingClasses) + +// Immutable collections utils + +internal operator fun PersistentList.plus(element: T): PersistentList = + this.add(element) + +internal operator fun PersistentList.plus(other: PersistentList): PersistentList = + this.addAll(other) + +internal operator fun PersistentSet.plus(element: T): PersistentSet = + this.add(element) + +internal operator fun PersistentSet.plus(other: PersistentSet): PersistentSet = + this.addAll(other) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/DeclarationUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/DeclarationUtils.kt new file mode 100644 index 0000000000..abf2e4d4b5 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/DeclarationUtils.kt @@ -0,0 +1,26 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtPrimitiveModel + +/** + * Checks if an expected variable is needed, + * or it will be further asserted with assertNull or assertTrue/False. + */ +fun needExpectedDeclaration(model: UtModel): Boolean { + val representsNull = model is UtNullModel + val representsBoolean = model is UtPrimitiveModel && model.value is Boolean + return !(representsNull || representsBoolean) +} + +/** + * Contains all possible visibility modifiers that may be used in code generation. + */ +enum class VisibilityModifier { + PUBLIC, + PRIVATE, + PROTECTED, + INTERNAL, + PACKAGE_PRIVATE, +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/ututils/CgUtilClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/ututils/CgUtilClassConstructor.kt new file mode 100644 index 0000000000..1f2fb46003 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/ututils/CgUtilClassConstructor.kt @@ -0,0 +1,46 @@ +package org.utbot.framework.codegen.tree.ututils + +import org.utbot.framework.codegen.domain.builtin.selectUtilClassId +import org.utbot.framework.codegen.domain.models.CgAuxiliaryClass +import org.utbot.framework.codegen.domain.models.CgAuxiliaryNestedClassesRegion +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.domain.models.CgStaticsRegion +import org.utbot.framework.codegen.domain.models.CgUtilMethod +import org.utbot.framework.codegen.tree.buildClass +import org.utbot.framework.codegen.tree.buildClassBody +import org.utbot.framework.codegen.tree.buildClassFile +import org.utbot.framework.plugin.api.CodegenLanguage + +/** + * This class is used to construct a file containing an util class UtUtils. + * The util class is constructed when the argument `generateUtilClassFile` in the [CodeGenerator] is true. + */ +internal object CgUtilClassConstructor { + fun constructUtilsClassFile( + utilClassKind: UtilClassKind, + codegenLanguage: CodegenLanguage, + ): CgClassFile { + val utilMethodProvider = utilClassKind.utilMethodProvider + val utilsClassId = selectUtilClassId(codegenLanguage) + return buildClassFile { + // imports are empty, because we use fully qualified classes and static methods, + // so they will be imported once IDEA reformatting action has worked + declaredClass = buildClass { + id = utilsClassId + documentation = utilClassKind.utilClassDocumentation(codegenLanguage) + body = buildClassBody(utilsClassId) { + staticDeclarationRegions += CgStaticsRegion( + header = "Util methods", + content = utilMethodProvider.utilMethodIds.map { CgUtilMethod(it) }) + + nestedClassRegions += CgAuxiliaryNestedClassesRegion( + header = "Util classes", + nestedClasses = listOf( + CgAuxiliaryClass(utilMethodProvider.capturedArgumentClassId) + ) + ) + } + } + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/ututils/UtilClassKind.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/ututils/UtilClassKind.kt new file mode 100644 index 0000000000..7b318f9678 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/ututils/UtilClassKind.kt @@ -0,0 +1,149 @@ +package org.utbot.framework.codegen.tree.ututils + +import org.utbot.framework.codegen.domain.builtin.UtilClassFileMethodProvider +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgDocRegularLineStmt +import org.utbot.framework.codegen.domain.models.CgDocumentationComment +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MockFramework +import java.util.* + +/** + * A kind of util class. See the description of each kind at their respective classes. + * @property utilMethodProvider a [UtilClassFileMethodProvider] containing information about + * utilities that come from a separately generated UtUtils class + * (as opposed to utils that are declared directly in the test class, for example). + * @property mockFrameworkUsed a flag indicating if a mock framework was used. + * For detailed description see [CgContextOwner.mockFrameworkUsed]. + * @property mockFramework a framework used to create mocks + * @property priority when we generate multiple test classes, they can require different [UtilClassKind]. + * We will generate an util class corresponding to the kind with the greatest priority. + * For example, one test class may not use mocks, but the other one does. + * Then we will generate an util class with mocks, because it has a greater priority (see [UtUtilsWithMockito]). + */ +sealed class UtilClassKind( + internal val utilMethodProvider: UtilClassFileMethodProvider, + internal val mockFrameworkUsed: Boolean, + internal val mockFramework: MockFramework = MockFramework.MOCKITO, + private val priority: Int +) : Comparable { + + /** + * Contains comments specifying the version and the kind of util class being generated and + */ + fun utilClassDocumentation(codegenLanguage: CodegenLanguage): CgDocumentationComment + = CgDocumentationComment( + listOf( + CgDocRegularLineStmt(utilClassKindCommentText), + CgDocRegularLineStmt("$UTIL_CLASS_VERSION_COMMENT_PREFIX${utilClassVersion(codegenLanguage)}"), + ) + ) + + /** + * The version of util class being generated. + * For more details see [UtilClassFileMethodProvider.UTIL_CLASS_VERSION]. + */ + fun utilClassVersion(codegenLanguage: CodegenLanguage): String + = UtilClassFileMethodProvider(codegenLanguage).UTIL_CLASS_VERSION + + /** + * The text of comment specifying the kind of util class. + * At the moment, there are two kinds: [RegularUtUtils] (without Mockito) and [UtUtilsWithMockito]. + * + * This comment is needed when the plugin decides whether to overwrite an existing util class or not. + * When making that decision, it is important to determine if the existing class uses mocks or not, + * and this comment will help do that. + */ + abstract val utilClassKindCommentText: String + + /** + * A kind of regular UtUtils class. "Regular" here means that this class does not use a mock framework. + */ + class RegularUtUtils(val codegenLanguage: CodegenLanguage) : + UtilClassKind( + UtilClassFileMethodProvider(codegenLanguage), + mockFrameworkUsed = false, + priority = 0, + ) { + override val utilClassKindCommentText: String + get() = "This is a regular UtUtils class (without mock framework usage)" + } + + /** + * A kind of UtUtils class that uses a mock framework. At the moment the framework is Mockito. + */ + class UtUtilsWithMockito(val codegenLanguage: CodegenLanguage) : + UtilClassKind(UtilClassFileMethodProvider(codegenLanguage), mockFrameworkUsed = true, priority = 1) { + override val utilClassKindCommentText: String + get() = "This is UtUtils class with Mockito support" + } + + override fun compareTo(other: UtilClassKind): Int { + return priority.compareTo(other.priority) + } + + /** + * Construct an util class file as a [CgClassFile] and render it. + * @return the text of the generated util class file. + */ + fun getUtilClassText(codegenLanguage: CodegenLanguage): String { + val utilClassFile = CgUtilClassConstructor.constructUtilsClassFile(this, codegenLanguage) + val renderer = CgAbstractRenderer.makeRenderer(this, codegenLanguage) + utilClassFile.accept(renderer) + return renderer.toString() + } + + companion object { + + /** + * Class UtUtils will contain a comment specifying the version of this util class + * (if we ever change util methods, then util class will be different, hence the update of its version). + * This is a prefix that will go before the version in the comment. + */ + const val UTIL_CLASS_VERSION_COMMENT_PREFIX = "UtUtils class version: " + + fun utilClassKindByCommentOrNull( + comment: String, + codegenLanguage: CodegenLanguage + ) + : UtilClassKind? { + return when { + comment.contains(RegularUtUtils(codegenLanguage).utilClassKindCommentText) -> RegularUtUtils(codegenLanguage) + comment.contains(UtUtilsWithMockito(codegenLanguage).utilClassKindCommentText) -> UtUtilsWithMockito(codegenLanguage) + else -> null + } + } + + /** + * Check if an util class is required, and if so, what kind. + * @return `null` if [CgContext.utilMethodProvider] is not [UtilClassFileMethodProvider], + * because it means that util methods will be taken from some other provider (e.g. [TestClassUtilMethodProvider]). + */ + fun fromCgContextOrNull(context: CgContext): UtilClassKind? { + if (context.requiredUtilMethods.isEmpty()) return null + if (!context.mockFrameworkUsed) { + return RegularUtUtils(context.codegenLanguage) + } + return when (context.mockFramework) { + MockFramework.MOCKITO -> UtUtilsWithMockito(context.codegenLanguage) + // in case we will add any other mock frameworks, newer Kotlin compiler versions + // will report a non-exhaustive 'when', so we will not forget to support them here as well + } + } + + const val UT_UTILS_BASE_PACKAGE_NAME = "org.utbot.runtime.utils" + const val UT_UTILS_INSTANCE_NAME = "UtUtils" + const val PACKAGE_DELIMITER = "." + + /** + * List of package name components of UtUtils class. + * See whole package name at [UT_UTILS_BASE_PACKAGE_NAME]. + */ + fun utilsPackageNames(codegenLanguage: CodegenLanguage): List + = UT_UTILS_BASE_PACKAGE_NAME.split(PACKAGE_DELIMITER) + codegenLanguage.name.lowercase(Locale.getDefault()) + + fun utilsPackageFullName(codegenLanguage: CodegenLanguage): String + = utilsPackageNames(codegenLanguage).joinToString { PACKAGE_DELIMITER } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/ClassIdUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/ClassIdUtil.kt new file mode 100644 index 0000000000..1f54455325 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/ClassIdUtil.kt @@ -0,0 +1,50 @@ +package org.utbot.framework.codegen.util + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.isPackagePrivate +import org.utbot.framework.plugin.api.util.isProtected +import org.utbot.framework.plugin.api.util.isPublic +import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.framework.plugin.api.util.allDeclaredFieldIds +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.isArray + +/** + * For now we will count class accessible if it is: + * - Public or package-private within package [packageName]. + * - It's outer class (if exists) is accessible too. + * NOTE: local and synthetic classes are considered as inaccessible. + * NOTE: protected classes cannot be accessed because test class does not extend any classes. + * + * @param packageName name of the package we check accessibility from + */ +infix fun ClassId.isAccessibleFrom(packageName: String): Boolean { + if (this.isLocal || this.isAnonymous || this.isSynthetic) { + return false + } + + val outerClassId = outerClass?.id + if (outerClassId != null && !outerClassId.isAccessibleFrom(packageName)) { + return false + } + + return if (this.isArray) { + elementClassId!!.isAccessibleFrom(packageName) + } else { + isPublic || (this.packageName == packageName && (isPackagePrivate || isProtected)) + } +} + +/** + * Returns field of [this], such that [methodId] is a getter for it (or null if methodId doesn't represent a getter) + */ +internal fun ClassId.fieldThatIsGotWith(methodId: ExecutableId): FieldId? = + allDeclaredFieldIds.singleOrNull { !it.isStatic && it.getter.describesSameMethodAs(methodId) } + +/** + * Returns field of [this], such that [methodId] is a setter for it (or null if methodId doesn't represent a setter) + */ +internal fun ClassId.fieldThatIsSetWith(methodId: ExecutableId): FieldId? = + allDeclaredFieldIds.singleOrNull { !it.isStatic && it.setter.describesSameMethodAs(methodId) } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/CommentUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/CommentUtil.kt new file mode 100644 index 0000000000..7c22717f15 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/CommentUtil.kt @@ -0,0 +1,10 @@ +package org.utbot.framework.codegen.util + +import org.utbot.framework.plugin.api.util.IndentUtil.TAB + +fun String.escapeControlChars() = + replace("\b", "\\b") + .replace("\n", "\\n") + .replace("\t", TAB) + .replace("\r", "\\r") + .replace("\\u","\\\\u") \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/DslUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/DslUtil.kt new file mode 100644 index 0000000000..1f830a0612 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/DslUtil.kt @@ -0,0 +1,98 @@ +package org.utbot.framework.codegen.util + +import org.utbot.framework.codegen.domain.models.CgArrayElementAccess +import org.utbot.framework.codegen.domain.models.CgDecrement +import org.utbot.framework.codegen.domain.models.CgEqualTo +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgGetLength +import org.utbot.framework.codegen.domain.models.CgGreaterThan +import org.utbot.framework.codegen.domain.models.CgIncrement +import org.utbot.framework.codegen.domain.models.CgLessThan +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.tree.CgMethodConstructor +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.isArray +import org.utbot.framework.plugin.api.util.longClassId +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.shortClassId +import org.utbot.framework.plugin.api.util.stringClassId + +fun CgExpression.at(index: Any?): CgArrayElementAccess = + CgArrayElementAccess(this, index.resolve()) + +infix fun CgExpression.equalTo(other: Any?): CgEqualTo = + CgEqualTo(this, other.resolve()) + +infix fun CgExpression.lessThan(other: Any?): CgLessThan = + CgLessThan(this, other.resolve()) + +infix fun CgExpression.greaterThan(other: Any?): CgGreaterThan = + CgGreaterThan(this, other.resolve()) + +// Literals + +// TODO: is it OK to use Object as a type of null literal? +fun nullLiteral() = CgLiteral(objectClassId, null) + +fun intLiteral(num: Int) = CgLiteral(intClassId, num) + +fun longLiteral(num: Long) = CgLiteral(longClassId, num) + +fun byteLiteral(num: Byte) = CgLiteral(byteClassId, num) + +fun shortLiteral(num: Short) = CgLiteral(shortClassId, num) + +fun floatLiteral(num: Float) = CgLiteral(floatClassId, num) + +fun doubleLiteral(num: Double) = CgLiteral(doubleClassId, num) + +fun booleanLiteral(b: Boolean) = CgLiteral(booleanClassId, b) + +fun charLiteral(c: Char) = CgLiteral(charClassId, c) + +fun stringLiteral(string: String) = CgLiteral(stringClassId, string) + +// Get array length + +/** + * Returns length field access for array type variable and [UtilMethodProvider.getArrayLengthMethodId] call otherwise. + */ +internal fun CgVariable.length(methodConstructor: CgMethodConstructor): CgExpression { + val thisVariable = this + + return if (type.isArray) { + CgGetLength(thisVariable) + } else { + with(methodConstructor) { utilsClassId[getArrayLength](thisVariable) } + } +} + +// Increment and decrement + +fun CgVariable.inc(): CgIncrement = CgIncrement(this) + +fun CgVariable.dec(): CgDecrement = CgDecrement(this) + +fun Any?.resolve(): CgExpression = when (this) { + null -> nullLiteral() + is Int -> intLiteral(this) + is Long -> longLiteral(this) + is Byte -> byteLiteral(this) + is Short -> shortLiteral(this) + is Float -> floatLiteral(this) + is Double -> doubleLiteral(this) + is Boolean -> booleanLiteral(this) + is Char -> charLiteral(this) + is String -> stringLiteral(this) + is CgExpression -> this + else -> error("Expected primitive, string, null or CgExpression, but got: ${this::class}") +} + +fun Array<*>.resolve(): List = map { it.resolve() } + diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/ExecutableIdUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/ExecutableIdUtil.kt similarity index 93% rename from utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/ExecutableIdUtil.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/ExecutableIdUtil.kt index 22f7725c2f..af3cef3bc8 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/ExecutableIdUtil.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/ExecutableIdUtil.kt @@ -1,4 +1,4 @@ -package org.utbot.framework.codegen.model.util +package org.utbot.framework.codegen.util import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.util.isPackagePrivate diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/FieldIdUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/FieldIdUtil.kt new file mode 100644 index 0000000000..d5ec74b822 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/FieldIdUtil.kt @@ -0,0 +1,83 @@ +package org.utbot.framework.codegen.util + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.plugin.api.ClassId +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.isFinal +import org.utbot.framework.plugin.api.util.isPackagePrivate +import org.utbot.framework.plugin.api.util.isProtected +import org.utbot.framework.plugin.api.util.isPublic +import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.framework.plugin.api.util.voidClassId + +/** + * For now we will count field accessible if it is not private and its class is also accessible, + * because we generate tests in the same package with the class under test, + * which means we can access public, protected and package-private fields + * + * @param context context in which code is generated (it is needed because the method needs to know package and language) + * @param callerClassId object on which we try to access the field + */ +fun FieldId.isAccessibleFrom(packageName: String, callerClassId: ClassId): Boolean { + val isClassAccessible = declaringClass.isAccessibleFrom(packageName) + + /* + * There is a corner case which we ignore now. + * Protected fields are accessible in nested classes of inheritors. + */ + val isAccessibleByVisibility = + isPublic || + isPackagePrivate && callerClassId.packageName == packageName && declaringClass.packageName == packageName || + isProtected && declaringClass.packageName == packageName + val isAccessibleFromPackageByModifiers = isAccessibleByVisibility && !isSynthetic + + return isClassAccessible && isAccessibleFromPackageByModifiers +} + +internal fun FieldId.canBeReadViaGetterFrom(context: CgContext): Boolean = + declaringClass.allMethods.contains(getter) && getter.isAccessibleFrom(context.testClassPackageName) + +/** + * Returns whether you can read field's value without reflection + */ +internal fun FieldId.canBeReadFrom(context: CgContext, callerClassId: ClassId): Boolean { + if (context.codegenLanguage == CodegenLanguage.KOTLIN) { + // Kotlin will allow direct field access for non-static fields with accessible getter + if (!isStatic && canBeReadViaGetterFrom(context)) + return true + } + + return isAccessibleFrom(context.testClassPackageName, callerClassId) +} + +internal fun FieldId.canBeSetViaSetterFrom(context: CgContext): Boolean = + declaringClass.allMethods.contains(setter) && setter.isAccessibleFrom(context.testClassPackageName) + +/** + * Whether or not a field can be set without reflection + */ +internal fun FieldId.canBeSetFrom(context: CgContext, callerClassId: ClassId): Boolean { + if (context.codegenLanguage == CodegenLanguage.KOTLIN) { + // Kotlin will allow direct write access if both getter and setter is defined + // !isAccessibleFrom(context) is important here because above rule applies to final fields only if they are not accessible in Java terms + if (!isAccessibleFrom(context.testClassPackageName, callerClassId) && !isStatic && canBeReadViaGetterFrom(context) && canBeSetViaSetterFrom(context)) { + return true + } + } + + return isAccessibleFrom(context.testClassPackageName, callerClassId) && !isFinal +} + +/** + * The default getter method for field (the one which is generated by Kotlin compiler) + */ +val FieldId.getter: MethodId + get() = MethodId(declaringClass, "get${name.replaceFirstChar { it.uppercase() } }", type, emptyList()) + +/** + * The default setter method for field (the one which is generated by Kotlin compiler) + */ +val FieldId.setter: MethodId + get() = MethodId(declaringClass, "set${name.replaceFirstChar { it.uppercase() } }", voidClassId, listOf(type)) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/InstrumentationContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/InstrumentationContext.kt deleted file mode 100644 index 0233c7b6d9..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/InstrumentationContext.kt +++ /dev/null @@ -1,72 +0,0 @@ -package org.utbot.framework.concrete - -import org.utbot.framework.plugin.api.util.signature -import java.lang.reflect.Method -import java.util.IdentityHashMap - -/** - * Some information, which is computed after classes instrumentation. - * - * This information will be used later in `invoke` function. - */ -class InstrumentationContext { - /** - * Contains unique id for each method, which is required for this method mocking. - */ - val methodSignatureToId = mutableMapOf() - - object MockGetter { - data class MockContainer(private val values: List<*>) { - private var ptr: Int = 0 - fun hasNext(): Boolean = ptr < values.size - fun nextValue(): Any? = values[ptr++] - } - - /** - * Instance -> method -> list of values in the return order - */ - private val mocks = IdentityHashMap>() - private val callSites = HashMap>() - - /** - * Returns possibility of taking mock object of method with supplied [methodSignature] on an [obj] object. - */ - @JvmStatic - fun hasMock(obj: Any?, methodSignature: String): Boolean = - mocks[obj]?.get(methodSignature)?.hasNext() ?: false - - /** - * Returns the next value for mocked method with supplied [methodSignature] on an [obj] object. - * - * This function has only to be called from the instrumented bytecode everytime - * we need a next value for a mocked method. - */ - @JvmStatic - fun getMock(obj: Any?, methodSignature: String): Any? = - mocks[obj]?.get(methodSignature).let { container -> - container ?: error("Can't get mock container for method [$obj\$$methodSignature]") - container.nextValue() - } - - /** - * Returns current callSites for mocking new instance of [instanceType] contains [callSite] or not - */ - @JvmStatic - fun checkCallSite(instanceType: String, callSite: String): Boolean { - return callSites.getOrDefault(instanceType, emptySet()).contains(callSite) - } - - fun updateCallSites(instanceType: String, instanceCallSites: Set) { - callSites[instanceType] = instanceCallSites - } - - fun updateMocks(obj: Any?, methodSignature: String, values: List<*>) { - val methodMocks = mocks.getOrPut(obj) { mutableMapOf() } - methodMocks[methodSignature] = MockContainer(values) - } - - fun updateMocks(obj: Any?, method: Method, values: List<*>) { - updateMocks(obj, method.signature, values) - } - } -} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockController.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockController.kt deleted file mode 100644 index 679925c30e..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockController.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.utbot.framework.concrete - -import java.io.Closeable - -interface MockController : Closeable \ No newline at end of file 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 deleted file mode 100644 index 182c0c2f9d..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt +++ /dev/null @@ -1,540 +0,0 @@ -package org.utbot.framework.concrete - -import org.utbot.common.invokeCatching -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.FieldId -import org.utbot.framework.plugin.api.FieldMockTarget -import org.utbot.framework.plugin.api.MethodId -import org.utbot.framework.plugin.api.MockId -import org.utbot.framework.plugin.api.MockInfo -import org.utbot.framework.plugin.api.MockTarget -import org.utbot.framework.plugin.api.ObjectMockTarget -import org.utbot.framework.plugin.api.ParameterMockTarget -import org.utbot.framework.plugin.api.UtArrayModel -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtClassRefModel -import org.utbot.framework.plugin.api.UtCompositeModel -import org.utbot.framework.plugin.api.UtConcreteValue -import org.utbot.framework.plugin.api.UtDirectSetFieldModel -import org.utbot.framework.plugin.api.UtEnumConstantModel -import org.utbot.framework.plugin.api.UtExecutableCallModel -import org.utbot.framework.plugin.api.UtMockValue -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation -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.UtStaticMethodInstrumentation -import org.utbot.framework.plugin.api.UtVoidModel -import org.utbot.framework.plugin.api.isMockModel -import org.utbot.framework.plugin.api.util.constructor -import org.utbot.framework.plugin.api.util.executableId -import org.utbot.framework.plugin.api.util.jField -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.method -import org.utbot.framework.plugin.api.util.utContext -import org.utbot.framework.util.anyInstance -import java.io.Closeable -import java.lang.reflect.Field -import java.lang.reflect.Modifier -import java.util.IdentityHashMap -import kotlin.reflect.KClass -import org.mockito.Mockito -import org.mockito.stubbing.Answer -import org.objectweb.asm.Type -import org.utbot.engine.util.lambda.CapturedArgument -import org.utbot.engine.util.lambda.constructLambda -import org.utbot.engine.util.lambda.constructStaticLambda -import org.utbot.framework.plugin.api.UtLambdaModel -import org.utbot.framework.plugin.api.util.isStatic -import org.utbot.instrumentation.process.runSandbox - -/** - * Constructs values (including mocks) from models. - * - * Uses model->constructed object reference-equality cache. - * - * This class is based on `ValueConstructor.kt`. The main difference is the ability to create mocked objects and mock - * static methods. - * - * Note that `clearState` was deleted! - */ -// TODO: JIRA:1379 -- Refactor ValueConstructor and MockValueConstructor -class MockValueConstructor( - private val instrumentationContext: InstrumentationContext -) : Closeable { - private val classLoader: ClassLoader - get() = utContext.classLoader - - val objectToModelCache: IdentityHashMap - get() { - val objectToModel = IdentityHashMap() - constructedObjects.forEach { (model, obj) -> - objectToModel[obj] = model - } - return objectToModel - } - - // TODO: JIRA:1379 -- replace UtReferenceModel with Int - private val constructedObjects = HashMap() - private val mockInfo = mutableListOf() - private var mockTarget: MockTarget? = null - private var mockCounter = 0 - - /** - * Controllers contain info about mocked methods and have to be closed to restore initial state. - */ - private val controllers = mutableListOf() - - /** - * Sets mock context (possible mock target) before block execution and restores previous one after block execution. - */ - private inline fun withMockTarget(target: MockTarget?, block: () -> T): T { - val old = mockTarget - try { - mockTarget = target - return block() - } finally { - mockTarget = old - } - } - - fun constructMethodParameters(models: List): List> = - models.mapIndexed { index, model -> - val target = mockTarget(model) { ParameterMockTarget(model.classId.name, index) } - construct(model, target) - } - - fun constructStatics(staticsBefore: Map): Map> = - staticsBefore.mapValues { (field, model) -> // TODO: refactor this - val target = FieldMockTarget(model.classId.name, field.declaringClass.name, owner = null, field.name) - construct(model, target) - } - - /** - * Main construction method. - * - * Takes care of nulls. Does not use cache, instead construct(Object/Array/List/FromAssembleModel) method - * uses cache directly. - * - * Takes mock creation context (possible mock target) to create mock if required. - */ - private fun construct(model: UtModel, target: MockTarget?): UtConcreteValue<*> = withMockTarget(target) { - when (model) { - is UtNullModel -> UtConcreteValue(null, model.classId.jClass) - is UtPrimitiveModel -> UtConcreteValue(model.value, model.classId.jClass) - is UtEnumConstantModel -> UtConcreteValue(constructEnum(model)) - is UtClassRefModel -> UtConcreteValue(model.value) - is UtCompositeModel -> UtConcreteValue(constructObject(model), model.classId.jClass) - is UtArrayModel -> UtConcreteValue(constructArray(model)) - is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model), model.classId.jClass) - is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) - is UtVoidModel -> UtConcreteValue(Unit) - } - } - - /** - * Constructs an Enum<*> instance by model, uses reference-equality cache. - */ - private fun constructEnum(model: UtEnumConstantModel): Any { - constructedObjects[model]?.let { return it } - constructedObjects[model] = model.value - return model.value - } - - /** - * Constructs object by model, uses reference-equality cache. - * - * Returns null for mock cause cannot instantiate it. - */ - private fun constructObject(model: UtCompositeModel): Any { - constructedObjects[model]?.let { return it } - - this.mockTarget?.let { mockTarget -> - model.mocks.forEach { (methodId, models) -> - mockInfo += MockInfo(mockTarget, methodId, models.map { model -> - if (model.isMockModel()) { - val mockId = MockId("mock${++mockCounter}") - // Call to "construct" method still required to collect mock interaction - construct(model, ObjectMockTarget(model.classId.name, mockId)) - UtMockValue(mockId, model.classId.name) - } else { - construct(model, target = null) - } - }) - } - } - - - val javaClass = javaClass(model.classId) - - val classInstance = if (!model.isMock) { - val notMockInstance = javaClass.anyInstance - - constructedObjects[model] = notMockInstance - notMockInstance - } else { - val mockInstance = generateMockitoMock(javaClass, model.mocks) - - constructedObjects[model] = mockInstance - mockInstance - } - - model.fields.forEach { (fieldId, fieldModel) -> - val declaredField = fieldId.jField - val accessible = declaredField.isAccessible - declaredField.isAccessible = true - - val modifiersField = Field::class.java.getDeclaredField("modifiers") - modifiersField.isAccessible = true - - val target = mockTarget(fieldModel) { - FieldMockTarget(fieldModel.classId.name, model.classId.name, UtConcreteValue(classInstance), fieldId.name) - } - val value = construct(fieldModel, target).value - val instance = if (Modifier.isStatic(declaredField.modifiers)) null else classInstance - declaredField.set(instance, value) - declaredField.isAccessible = accessible - } - - return classInstance - } - - private fun generateMockitoAnswer(methodToValues: Map>): Answer<*> { - val pointers = methodToValues.mapValues { (_, _) -> 0 }.toMutableMap() - val concreteValues = methodToValues.mapValues { (_, models) -> - models.map { model -> - val mockId = MockId("mock${++mockCounter}") - val target = mockTarget(model) { ObjectMockTarget(model.classId.name, mockId) } - construct(model, target).value.takeIf { it != Unit } // if it is unit, then null should be returned - // This model has to be already constructed, so it is OK to pass null as a target - } - } - return Answer { invocation -> - with(invocation.method) { - pointers[executableId].let { pointer -> - concreteValues[executableId].let { values -> - if (pointer != null && values != null && pointer < values.size) { - pointers[executableId] = pointer + 1 - values[pointer] - } else { - invocation.callRealMethod() - } - } - } - } - } - } - - private fun generateMockitoMock(clazz: Class<*>, mocks: Map>): Any { - return Mockito.mock(clazz, generateMockitoAnswer(mocks)) - } - - private fun computeConcreteValuesForMethods( - methodToValues: Map>, - ): Map> = methodToValues.mapValues { (_, models) -> - models.map { mockAndGet(it) } - } - - /** - * Mocks methods on [instance] with supplied [methodToValues]. - * - * Also add new controllers to [controllers]. Each controller corresponds to one method. If it is a static method, then the controller - * must be closed. If it is a non-static method and you don't change the mocks behaviour on the passed instance, - * then the controller doesn't have to be closed - * - * @param [instance] must be non-`null` for non-static methods. - * @param [methodToValues] return values for methods. - */ - private fun mockMethods( - instance: Any?, - methodToValues: Map>, - ) { - controllers += computeConcreteValuesForMethods(methodToValues).map { (method, values) -> - if (method !is MethodId) { - throw IllegalArgumentException("Expected MethodId, but got: $method") - } - MethodMockController( - method.classId.jClass, - method.method, - instance, - values, - instrumentationContext - ) - } - - } - - /** - * Mocks static methods according to instrumentations. - */ - fun mockStaticMethods( - instrumentations: List, - ) { - val methodToValues = instrumentations.associate { it.methodId as ExecutableId to it.values } - mockMethods(null, methodToValues) - } - - /** - * Mocks new instances according to instrumentations - */ - fun mockNewInstances( - instrumentations: List, - ) { - controllers += instrumentations.map { mock -> - InstanceMockController( - mock.classId, - mock.instances.map { mockAndGet(it) }, - mock.callSites.map { Type.getType(it.jClass).internalName }.toSet() - ) - } - } - - /** - * Constructs array by model. - * - * Supports arrays of primitive, arrays of arrays and arrays of objects. - * - * Note: does not check isNull, but if isNull set returns empty array because for null array length set to 0. - */ - private fun constructArray(model: UtArrayModel): Any { - constructedObjects[model]?.let { return it } - - with(model) { - 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) } - }.also { constructedObjects[model] = it } - "S" -> ShortArray(length) { primitive(constModel) }.apply { - stores.forEach { (index, model) -> this[index] = primitive(model) } - }.also { constructedObjects[model] = it } - "C" -> CharArray(length) { primitive(constModel) }.apply { - stores.forEach { (index, model) -> this[index] = primitive(model) } - }.also { constructedObjects[model] = it } - "I" -> IntArray(length) { primitive(constModel) }.apply { - stores.forEach { (index, model) -> this[index] = primitive(model) } - }.also { constructedObjects[model] = it } - "J" -> LongArray(length) { primitive(constModel) }.apply { - stores.forEach { (index, model) -> this[index] = primitive(model) } - }.also { constructedObjects[model] = it } - "F" -> FloatArray(length) { primitive(constModel) }.apply { - stores.forEach { (index, model) -> this[index] = primitive(model) } - }.also { constructedObjects[model] = it } - "D" -> DoubleArray(length) { primitive(constModel) }.apply { - stores.forEach { (index, model) -> this[index] = primitive(model) } - }.also { constructedObjects[model] = it } - "Z" -> BooleanArray(length) { primitive(constModel) }.apply { - stores.forEach { (index, model) -> this[index] = primitive(model) } - }.also { constructedObjects[model] = it } - else -> { - val javaClass = javaClass(elementClassId) - val instance = java.lang.reflect.Array.newInstance(javaClass, length) as Array<*> - constructedObjects[model] = instance - for (i in instance.indices) { - val elementModel = stores[i] ?: constModel - val value = construct(elementModel, null).value - try { - java.lang.reflect.Array.set(instance, i, value) - } catch (iae:IllegalArgumentException) { - throw IllegalArgumentException( - iae.message + " array: ${instance.javaClass.name}; value: ${value?.javaClass?.name}" , iae - ) - } - } - instance - } - } - } - } - - /** - * Constructs object with [UtAssembleModel]. - */ - private fun constructFromAssembleModel(assembleModel: UtAssembleModel): Any { - constructedObjects[assembleModel]?.let { return it } - - val instantiationExecutableCall = assembleModel.instantiationCall - val result = updateWithExecutableCallModel(instantiationExecutableCall) - checkNotNull(result) { - "Tracked instance can't be null for call ${instantiationExecutableCall.executable} in model $assembleModel" - } - constructedObjects[assembleModel] = result - - assembleModel.modificationsChain.forEach { statementModel -> - when (statementModel) { - is UtExecutableCallModel -> updateWithExecutableCallModel(statementModel) - is UtDirectSetFieldModel -> updateWithDirectSetFieldModel(statementModel) - } - } - - return constructedObjects[assembleModel] ?: error("Can't assemble model: $assembleModel") - } - - private fun constructFromLambdaModel(lambdaModel: UtLambdaModel): Any { - constructedObjects[lambdaModel]?.let { return it } - // A class representing a functional interface. - val samType: Class<*> = lambdaModel.samType.jClass - // A class where the lambda is declared. - val declaringClass: Class<*> = lambdaModel.declaringClass.jClass - // A name of the synthetic method that represents a lambda. - val lambdaName = lambdaModel.lambdaName - - val lambda = if (lambdaModel.lambdaMethodId.isStatic) { - val capturedArguments = lambdaModel.capturedValues - .map { model -> CapturedArgument(type = model.classId.jClass, value = value(model)) } - .toTypedArray() - constructStaticLambda(samType, declaringClass, lambdaName, *capturedArguments) - } else { - val capturedReceiverModel = lambdaModel.capturedValues.firstOrNull() - ?: error("Non-static lambda must capture `this` instance, so there must be at least one captured value") - - // Values that the given lambda has captured. - val capturedReceiver = value(capturedReceiverModel) - val capturedArguments = lambdaModel.capturedValues.subList(1, lambdaModel.capturedValues.size) - .map { model -> CapturedArgument(type = model.classId.jClass, value = value(model)) } - .toTypedArray() - constructLambda(samType, declaringClass, lambdaName, capturedReceiver, *capturedArguments) - } - constructedObjects[lambdaModel] = lambda - return lambda - } - - /** - * Updates instance state with [callModel] invocation. - * - * @return the result of [callModel] invocation - */ - private fun updateWithExecutableCallModel( - callModel: UtExecutableCallModel, - ): Any? { - val executable = callModel.executable - val instanceValue = callModel.instance?.let { value(it) } - val params = callModel.params.map { value(it) } - - val result = when (executable) { - is MethodId -> executable.call(params, instanceValue) - is ConstructorId -> executable.call(params) - } - - return result - } - - /** - * Updates instance with [UtDirectSetFieldModel] execution. - */ - private fun updateWithDirectSetFieldModel(directSetterModel: UtDirectSetFieldModel) { - val instanceModel = directSetterModel.instance - val instance = value(instanceModel) - - val instanceClassId = instanceModel.classId - val fieldModel = directSetterModel.fieldModel - - val field = directSetterModel.fieldId.jField - val isAccessible = field.isAccessible - - try { - //set field accessible to support protected or package-private direct setters - field.isAccessible = true - - //prepare mockTarget for field if it is a mock - val mockTarget = mockTarget(fieldModel) { - FieldMockTarget( - fieldModel.classId.name, - instanceClassId.name, - UtConcreteValue(javaClass(instanceClassId).anyInstance), - field.name - ) - } - - //construct and set the value - val fieldValue = construct(fieldModel, mockTarget).value - field.set(instance, fieldValue) - } finally { - //restore accessibility property of the field - field.isAccessible = isAccessible - } - } - - /** - * Constructs value from [UtModel]. - */ - private fun value(model: UtModel) = construct(model, null).value - - private fun mockAndGet(model: UtModel): Any? { - val target = mockTarget(model) { // won't be called if model is not mockModel - val mockId = MockId("mock${++mockCounter}") - ObjectMockTarget(model.classId.name, mockId) - } - return construct(model, target).value - } - - private fun MethodId.call(args: List, instance: Any?): Any? = - method.runSandbox { - invokeCatching(obj = instance, args = args).getOrThrow() - } - - private fun ConstructorId.call(args: List): Any? = - constructor.runSandbox { - newInstance(*args.toTypedArray()) - } - - /** - * Fetches primitive value from NutsModel to create array of primitives. - */ - private inline fun primitive(model: UtModel): T = (model as UtPrimitiveModel).value as T - - private fun javaClass(id: ClassId) = kClass(id).java - - private fun kClass(id: ClassId) = - if (id.elementClassId != null) { - arrayClassOf(id.elementClassId!!) - } else { - when (id.jvmName) { - "B" -> Byte::class - "S" -> Short::class - "C" -> Char::class - "I" -> Int::class - "J" -> Long::class - "F" -> Float::class - "D" -> Double::class - "Z" -> Boolean::class - else -> classLoader.loadClass(id.name).kotlin - } - } - - private fun arrayClassOf(elementClassId: ClassId): KClass<*> = - if (elementClassId.elementClassId != null) { - val elementClass = arrayClassOf(elementClassId.elementClassId!!) - java.lang.reflect.Array.newInstance(elementClass.java, 0)::class - } else { - when (elementClassId.jvmName) { - "B" -> ByteArray::class - "S" -> ShortArray::class - "C" -> CharArray::class - "I" -> IntArray::class - "J" -> LongArray::class - "F" -> FloatArray::class - "D" -> DoubleArray::class - "Z" -> BooleanArray::class - else -> { - val elementClass = classLoader.loadClass(elementClassId.name) - java.lang.reflect.Array.newInstance(elementClass, 0)::class - } - } - } - - override fun close() { - controllers.forEach { it.close() } - } -} - -/** - * Creates mock target using init lambda if model represents mock or null otherwise. - */ -private fun mockTarget(model: UtModel, init: () -> MockTarget): MockTarget? = - if (model.isMockModel()) init() else null \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtAssembleModelConstructors.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtAssembleModelConstructors.kt deleted file mode 100644 index e870870492..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtAssembleModelConstructors.kt +++ /dev/null @@ -1,125 +0,0 @@ -package org.utbot.framework.concrete - -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtExecutableCallModel -import org.utbot.framework.plugin.api.UtStatementModel -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.primitiveWrappers -import org.utbot.framework.plugin.api.util.voidWrapperClassId -import org.utbot.framework.util.nextModelName - -private val predefinedConstructors = mutableMapOf, () -> UtAssembleModelConstructorBase>( - /** - * Optionals - */ - java.util.OptionalInt::class.java to { OptionalIntConstructor() }, - java.util.OptionalLong::class.java to { OptionalLongConstructor() }, - java.util.OptionalDouble::class.java to { OptionalDoubleConstructor() }, - java.util.Optional::class.java to { OptionalConstructor() }, - - /** - * Lists - */ - java.util.LinkedList::class.java to { CollectionConstructor() }, - java.util.ArrayList::class.java to { CollectionConstructor() }, - java.util.AbstractList::class.java to { CollectionConstructor() }, - java.util.List::class.java to { CollectionConstructor() }, - java.util.concurrent.CopyOnWriteArrayList::class.java to { CollectionConstructor() }, - - - /** - * Queues, deques - */ - java.util.PriorityQueue::class.java to { CollectionConstructor() }, - java.util.ArrayDeque::class.java to { CollectionConstructor() }, - java.util.concurrent.LinkedBlockingQueue::class.java to { CollectionConstructor() }, - java.util.concurrent.LinkedBlockingDeque::class.java to { CollectionConstructor() }, - java.util.concurrent.ConcurrentLinkedQueue::class.java to { CollectionConstructor() }, - java.util.concurrent.ConcurrentLinkedDeque::class.java to { CollectionConstructor() }, - java.util.Queue::class.java to { CollectionConstructor() }, - java.util.Deque::class.java to { CollectionConstructor() }, - - - /** - * Sets - */ - java.util.HashSet::class.java to { CollectionConstructor() }, - java.util.TreeSet::class.java to { CollectionConstructor() }, - java.util.LinkedHashSet::class.java to { CollectionConstructor() }, - java.util.AbstractSet::class.java to { CollectionConstructor() }, - java.util.Set::class.java to { CollectionConstructor() }, - - - - /** - * Maps - */ - java.util.HashMap::class.java to { MapConstructor() }, - java.util.TreeMap::class.java to { MapConstructor() }, - java.util.LinkedHashMap::class.java to { MapConstructor() }, - java.util.AbstractMap::class.java to { MapConstructor() }, - java.util.concurrent.ConcurrentMap::class.java to { MapConstructor() }, - java.util.concurrent.ConcurrentHashMap::class.java to { MapConstructor() }, - java.util.IdentityHashMap::class.java to { MapConstructor() }, - java.util.WeakHashMap::class.java to { MapConstructor() }, - - /** - * Hashtables - */ - java.util.Hashtable::class.java to { MapConstructor() }, - - - /** - * String wrapper - */ - java.lang.String::class.java.let { it to { PrimitiveWrapperConstructor() } }, - - /** - * TODO: JIRA:1405 -- Add assemble constructors for another standard classes as well. - */ -).apply { - /** - * Primitive wrappers - */ - this += primitiveWrappers - .filter { it != voidWrapperClassId } - .associate { it.jClass to { PrimitiveWrapperConstructor() } } -} - -internal fun findUtAssembleModelConstructor(classId: ClassId): UtAssembleModelConstructorBase? = - predefinedConstructors[classId.jClass]?.invoke() - -internal abstract class UtAssembleModelConstructorBase { - fun constructAssembleModel( - internalConstructor: UtModelConstructorInterface, - value: Any, - valueClassId: ClassId, - id: Int?, - init: (UtAssembleModel) -> Unit - ): UtAssembleModel { - val baseName = valueClassId.simpleName.decapitalize() - val instantiationCall = provideInstantiationCall(internalConstructor, value, valueClassId) - return UtAssembleModel(id, valueClassId, nextModelName(baseName), instantiationCall) { - init(this) - provideModificationChain(internalConstructor, value) - } - } - - protected abstract fun provideInstantiationCall( - internalConstructor: UtModelConstructorInterface, - value: Any, - classId: ClassId - ): UtExecutableCallModel - - protected abstract fun UtAssembleModel.provideModificationChain( - internalConstructor: UtModelConstructorInterface, - value: Any - ): List -} - -internal fun UtAssembleModelConstructorBase.checkClassCast(expected: Class<*>, actual: Class<*>) { - require(expected.isAssignableFrom(actual)) { - "Can't cast $actual to $expected in $this assemble constructor." - } -} \ No newline at end of file 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 deleted file mode 100644 index ab0b69e8b2..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt +++ /dev/null @@ -1,332 +0,0 @@ -package org.utbot.framework.concrete - -import org.objectweb.asm.Type -import org.utbot.common.StopWatch -import org.utbot.common.ThreadBasedExecutor -import org.utbot.common.withAccessibility -import org.utbot.framework.UtSettings -import org.utbot.framework.assemble.AssembleModelGenerator -import org.utbot.framework.plugin.api.Coverage -import org.utbot.framework.plugin.api.EnvironmentModels -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.Instruction -import org.utbot.framework.plugin.api.MissingState -import org.utbot.framework.plugin.api.TimeoutException -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtConcreteExecutionFailure -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.UtInstrumentation -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation -import org.utbot.framework.plugin.api.UtSandboxFailure -import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation -import org.utbot.framework.plugin.api.UtTimeoutException -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.jField -import org.utbot.framework.plugin.api.util.singleExecutableId -import org.utbot.framework.plugin.api.util.utContext -import org.utbot.framework.plugin.api.util.withUtContext -import org.utbot.framework.plugin.api.withReflection -import org.utbot.framework.util.isInaccessibleViaReflection -import org.utbot.instrumentation.instrumentation.ArgumentList -import org.utbot.instrumentation.instrumentation.Instrumentation -import org.utbot.instrumentation.instrumentation.InvokeInstrumentation -import org.utbot.instrumentation.instrumentation.et.EtInstruction -import org.utbot.instrumentation.instrumentation.et.ExplicitThrowInstruction -import org.utbot.instrumentation.instrumentation.et.TraceHandler -import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter -import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor -import java.security.AccessControlException -import java.security.ProtectionDomain -import java.util.IdentityHashMap -import kotlin.reflect.jvm.javaMethod - -/** - * Consists of the data needed to execute the method concretely. Also includes method arguments stored in models. - * - * @property [stateBefore] is necessary for construction of parameters of a concrete call. - * @property [instrumentation] is necessary for mocking static methods and new instances. - * @property [timeout] is timeout for specific concrete execution (in milliseconds). - * By default is initialized from [UtSettings.concreteExecutionTimeoutInChildProcess] - */ -data class UtConcreteExecutionData( - val stateBefore: EnvironmentModels, - val instrumentation: List, - val timeout: Long = UtSettings.concreteExecutionTimeoutInChildProcess -) - -class UtConcreteExecutionResult( - val stateAfter: EnvironmentModels, - val result: UtExecutionResult, - val coverage: Coverage -) { - private fun collectAllModels(): List { - val allModels = listOfNotNull(stateAfter.thisInstance).toMutableList() - allModels += stateAfter.parameters - allModels += stateAfter.statics.values - allModels += listOfNotNull((result as? UtExecutionSuccess)?.model) - return allModels - } - - private fun updateWithAssembleModels( - assembledUtModels: IdentityHashMap - ): UtConcreteExecutionResult { - val toAssemble: (UtModel) -> UtModel = { assembledUtModels.getOrDefault(it, it) } - - val resolvedStateAfter = EnvironmentModels( - stateAfter.thisInstance?.let { toAssemble(it) }, - stateAfter.parameters.map { toAssemble(it) }, - stateAfter.statics.mapValues { toAssemble(it.value) } - ) - val resolvedResult = - (result as? UtExecutionSuccess)?.model?.let { UtExecutionSuccess(toAssemble(it)) } ?: result - - return UtConcreteExecutionResult( - resolvedStateAfter, - resolvedResult, - coverage - ) - } - - /** - * Tries to convert all models from [UtExecutionResult] to [UtAssembleModel] if possible. - * - * @return [UtConcreteExecutionResult] with converted models. - */ - fun convertToAssemble(packageName: String): UtConcreteExecutionResult { - val allModels = collectAllModels() - - val modelsToAssembleModels = AssembleModelGenerator(packageName).createAssembleModels(allModels) - return updateWithAssembleModels(modelsToAssembleModels) - } - - override fun toString(): String = buildString { - appendLine("UtConcreteExecutionResult(") - appendLine("stateAfter=$stateAfter") - appendLine("result=$result") - appendLine("coverage=$coverage)") - } -} - -object UtExecutionInstrumentation : Instrumentation { - private val delegateInstrumentation = InvokeInstrumentation() - - private val instrumentationContext = InstrumentationContext() - - private val traceHandler = TraceHandler() - private val pathsToUserClasses = mutableSetOf() - - override fun init(pathsToUserClasses: Set) { - this.pathsToUserClasses.clear() - this.pathsToUserClasses += pathsToUserClasses - } - - /** - * Ignores [arguments], because concrete arguments will be constructed - * from models passed via [parameters]. - * - * Argument [parameters] must be of type [UtConcreteExecutionData]. - */ - override fun invoke( - clazz: Class<*>, - methodSignature: String, - arguments: ArgumentList, - parameters: Any? - ): UtConcreteExecutionResult { - withReflection { - if (parameters !is UtConcreteExecutionData) { - throw IllegalArgumentException("Argument parameters must be of type UtConcreteExecutionData, but was: ${parameters?.javaClass}") - } - val (stateBefore, instrumentations, timeout) = parameters // smart cast to UtConcreteExecutionData - val parametersModels = listOfNotNull(stateBefore.thisInstance) + stateBefore.parameters - - val methodId = clazz.singleExecutableId(methodSignature) - val returnClassId = methodId.returnType - traceHandler.resetTrace() - - return MockValueConstructor(instrumentationContext).use { constructor -> - val params = try { - constructor.constructMethodParameters(parametersModels) - } catch (e: Throwable) { - if (e.cause is AccessControlException) { - return@use UtConcreteExecutionResult( - MissingState, - UtSandboxFailure(e.cause!!), - Coverage() - ) - } - - throw e - } - val staticFields = constructor - .constructStatics( - stateBefore - .statics - .filterKeys { !it.isInaccessibleViaReflection } - ) - .mapValues { (_, value) -> value.value } - - val concreteExecutionResult = withStaticFields(staticFields) { - val staticMethodsInstrumentation = instrumentations.filterIsInstance() - constructor.mockStaticMethods(staticMethodsInstrumentation) - val newInstanceInstrumentation = instrumentations.filterIsInstance() - constructor.mockNewInstances(newInstanceInstrumentation) - - traceHandler.resetTrace() - val stopWatch = StopWatch() - val context = UtContext(utContext.classLoader, stopWatch) - val concreteResult = ThreadBasedExecutor.threadLocal.invokeWithTimeout(timeout, stopWatch) { - withUtContext(context) { - delegateInstrumentation.invoke(clazz, methodSignature, params.map { it.value }) - } - }?.getOrThrow() as? Result<*> ?: Result.failure(TimeoutException("Timeout $timeout elapsed")) - val traceList = traceHandler.computeInstructionList() - - val cache = constructor.objectToModelCache - val utCompositeModelStrategy = ConstructOnlyUserClassesOrCachedObjectsStrategy(pathsToUserClasses, cache) - val utModelConstructor = UtModelConstructor(cache, utCompositeModelStrategy) - utModelConstructor.run { - val concreteUtModelResult = concreteResult.fold({ - UtExecutionSuccess(construct(it, returnClassId)) - }) { - sortOutException(it) - } - - val stateAfterParametersWithThis = params.map { construct(it.value, it.clazz.id) } - val stateAfterStatics = (staticFields.keys/* + traceHandler.computePutStatics()*/) - .associateWith { fieldId -> - fieldId.jField.run { - val computedValue = withAccessibility { get(null) } - val knownModel = stateBefore.statics[fieldId] - val knownValue = staticFields[fieldId] - if (knownModel != null && knownValue != null && knownValue == computedValue) { - knownModel - } else { - construct(computedValue, fieldId.type) - } - } - } - val (stateAfterThis, stateAfterParameters) = if (stateBefore.thisInstance == null) { - null to stateAfterParametersWithThis - } else { - stateAfterParametersWithThis.first() to stateAfterParametersWithThis.drop(1) - } - val stateAfter = EnvironmentModels(stateAfterThis, stateAfterParameters, stateAfterStatics) - UtConcreteExecutionResult( - stateAfter, - concreteUtModelResult, - traceList.toApiCoverage( - traceHandler.processingStorage.getInstructionsCount( - Type.getInternalName(clazz) - ) - ) - ) - } - } - - concreteExecutionResult - } - } - } - - override fun getStaticField(fieldId: FieldId): Result = - delegateInstrumentation.getStaticField(fieldId).map { value -> - val cache = IdentityHashMap() - val strategy = ConstructOnlyUserClassesOrCachedObjectsStrategy( - pathsToUserClasses, cache - ) - UtModelConstructor(cache, strategy).run { - construct(value, fieldId.type) - } - } - - private fun sortOutException(exception: Throwable): UtExecutionFailure { - if (exception is TimeoutException) { - return UtTimeoutException(exception) - } - if (exception is AccessControlException || - exception is ExceptionInInitializerError && exception.exception is AccessControlException) { - return UtSandboxFailure(exception) - } - // there also can be other cases, when we need to wrap internal exception... I suggest adding them on demand - - val instrs = traceHandler.computeInstructionList() - 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) - } - - } - - override fun transform( - loader: ClassLoader?, - className: String, - classBeingRedefined: Class<*>?, - protectionDomain: ProtectionDomain, - classfileBuffer: ByteArray - ): ByteArray { - val instrumenter = Instrumenter(classfileBuffer, loader) - - traceHandler.registerClass(className) - instrumenter.visitInstructions(traceHandler.computeInstructionVisitor(className)) - - val mockClassVisitor = instrumenter.visitClass { writer -> - MockClassVisitor( - writer, - InstrumentationContext.MockGetter::getMock.javaMethod!!, - InstrumentationContext.MockGetter::checkCallSite.javaMethod!!, - InstrumentationContext.MockGetter::hasMock.javaMethod!! - ) - } - - mockClassVisitor.signatureToId.forEach { (method, id) -> - instrumentationContext.methodSignatureToId += method to id - } - - return instrumenter.classByteCode - } - - private fun withStaticFields(staticFields: Map, block: () -> T): T { - val savedFields = mutableMapOf() - try { - staticFields.forEach { (fieldId, value) -> - fieldId.jField.run { - withAccessibility { - savedFields[fieldId] = get(null) - set(null, value) - } - } - } - return block() - } finally { - savedFields.forEach { (fieldId, value) -> - fieldId.jField.run { - withAccessibility { - set(null, value) - } - } - } - } - } -} - -/** - * Transforms a list of internal [EtInstruction]s to a list of api [Instruction]s. - */ -private fun List.toApiCoverage(instructionsCount: Long? = null): Coverage = - Coverage( - map { Instruction(it.className, it.methodSignature, it.line, it.id) }, - instructionsCount - ) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt deleted file mode 100644 index a2194dedfe..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt +++ /dev/null @@ -1,330 +0,0 @@ -package org.utbot.framework.concrete - -import org.utbot.common.asPathToFile -import org.utbot.common.withAccessibility -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.UtArrayModel -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtClassRefModel -import org.utbot.framework.plugin.api.UtCompositeModel -import org.utbot.framework.plugin.api.UtEnumConstantModel -import org.utbot.framework.plugin.api.UtLambdaModel -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.UtReferenceModel -import org.utbot.framework.plugin.api.UtVoidModel -import org.utbot.framework.plugin.api.isMockModel -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.fieldId -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.isPrimitive -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.longClassId -import org.utbot.framework.plugin.api.util.objectClassId -import org.utbot.framework.plugin.api.util.shortClassId -import org.utbot.framework.util.isInaccessibleViaReflection -import org.utbot.framework.util.valueToClassId -import java.lang.reflect.Modifier -import java.util.IdentityHashMap - -/** - * Represents common interface for model constructors. - */ -internal interface UtModelConstructorInterface { - /** - * Constructs a UtModel from a concrete [value] with a specific [classId]. - */ - fun construct(value: Any?, classId: ClassId): UtModel -} - -/** - * Constructs models from concrete values. - * - * Uses reflection to traverse fields recursively ignoring static final fields. Also uses object->constructed model - * reference-equality cache. - * - * @param objectToModelCache cache used for the model construction with respect to stateBefore. For each object, it first - * @param compositeModelStrategy decides whether we should construct a composite model for a certain value or not. - * searches in [objectToModelCache] for [UtReferenceModel.id]. - */ -class UtModelConstructor( - private val objectToModelCache: IdentityHashMap, - private val compositeModelStrategy: UtCompositeModelStrategy = AlwaysConstructStrategy -) : UtModelConstructorInterface { - private val constructedObjects = IdentityHashMap() - - private var unusedId = 0 - private val usedIds = objectToModelCache.values - .filterIsInstance() - .mapNotNull { it.id } - .toMutableSet() - - private fun computeUnusedIdAndUpdate(): Int { - while (unusedId in usedIds) { - unusedId++ - } - return unusedId.also { usedIds += it } - } - - private fun handleId(value: Any): Int { - return objectToModelCache[value]?.let { (it as? UtReferenceModel)?.id } ?: computeUnusedIdAndUpdate() - } - - /** - * Constructs a UtModel from a concrete [value] with a specific [classId]. The result can be a [UtAssembleModel] - * as well. - * - * Handles cache on stateBefore values. - */ - override fun construct(value: Any?, classId: ClassId): UtModel { - objectToModelCache[value]?.let { model -> - if (model is UtLambdaModel) { - return model - } - } - return when (value) { - null -> UtNullModel(classId) - is Unit -> UtVoidModel - is Byte, - is Short, - is Char, - is Int, - is Long, - is Float, - is Double, - is Boolean -> if (classId.isPrimitive) UtPrimitiveModel(value) else constructFromAny(value) - is ByteArray -> constructFromByteArray(value) - is ShortArray -> constructFromShortArray(value) - is CharArray -> constructFromCharArray(value) - is IntArray -> constructFromIntArray(value) - is LongArray -> constructFromLongArray(value) - is FloatArray -> constructFromFloatArray(value) - is DoubleArray -> constructFromDoubleArray(value) - is BooleanArray -> constructFromBooleanArray(value) - is Array<*> -> constructFromArray(value) - is Enum<*> -> constructFromEnum(value) - is Class<*> -> constructFromClass(value) - else -> constructFromAny(value) - } - } - - // Q: Is there a way to get rid of duplicated code? - - private fun constructFromDoubleArray(array: DoubleArray): UtModel = - constructedObjects.getOrElse(array) { - val stores = mutableMapOf() - val utModel = - UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toDouble()), stores) - constructedObjects[array] = utModel - array.forEachIndexed { idx, value -> - stores[idx] = construct(value, doubleClassId) - } - utModel - } - - private fun constructFromFloatArray(array: FloatArray): UtModel = - constructedObjects.getOrElse(array) { - val stores = mutableMapOf() - val utModel = - UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toFloat()), stores) - constructedObjects[array] = utModel - array.forEachIndexed { idx, value -> - stores[idx] = construct(value, floatClassId) - } - utModel - } - - private fun constructFromLongArray(array: LongArray): UtModel = - constructedObjects.getOrElse(array) { - val stores = mutableMapOf() - val utModel = - UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toLong()), stores) - constructedObjects[array] = utModel - array.forEachIndexed { idx, value -> - stores[idx] = construct(value, longClassId) - } - utModel - } - - private fun constructFromIntArray(array: IntArray): UtModel = - constructedObjects.getOrElse(array) { - val stores = mutableMapOf() - val utModel = UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0), stores) - constructedObjects[array] = utModel - array.forEachIndexed { idx, value -> - stores[idx] = construct(value, intClassId) - } - utModel - } - - private fun constructFromCharArray(array: CharArray): UtModel = - constructedObjects.getOrElse(array) { - val stores = mutableMapOf() - val utModel = - UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toChar()), stores) - constructedObjects[array] = utModel - array.forEachIndexed { idx, value -> - stores[idx] = construct(value, charClassId) - } - utModel - } - - private fun constructFromShortArray(array: ShortArray): UtModel = - constructedObjects.getOrElse(array) { - val stores = mutableMapOf() - val utModel = - UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toShort()), stores) - constructedObjects[array] = utModel - array.forEachIndexed { idx, value -> - stores[idx] = construct(value, shortClassId) - } - utModel - } - - private fun constructFromByteArray(array: ByteArray): UtModel = - constructedObjects.getOrElse(array) { - val stores = mutableMapOf() - val utModel = - UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toByte()), stores) - constructedObjects[array] = utModel - array.forEachIndexed { idx, value -> - stores[idx] = construct(value, byteClassId) - } - utModel - } - - private fun constructFromBooleanArray(array: BooleanArray): UtModel = - constructedObjects.getOrElse(array) { - val stores = mutableMapOf() - val utModel = - UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(false), stores) - constructedObjects[array] = utModel - array.forEachIndexed { idx, value -> - stores[idx] = construct(value, booleanClassId) - } - utModel - } - - private fun constructFromArray(array: Array<*>): UtModel = - constructedObjects.getOrElse(array) { - val stores = mutableMapOf() - val utModel = - UtArrayModel(handleId(array), array::class.java.id, array.size, UtNullModel(objectClassId), stores) - constructedObjects[array] = utModel - array.forEachIndexed { idx, value -> - stores[idx] = construct(value, objectClassId) - } - utModel - } - - private fun constructFromEnum(enum: Enum<*>): UtModel = - constructedObjects.getOrElse(enum) { - val utModel = UtEnumConstantModel(handleId(enum), enum::class.java.id, enum) - constructedObjects[enum] = utModel - utModel - } - - private fun constructFromClass(clazz: Class<*>): UtModel = - constructedObjects.getOrElse(clazz) { - val utModel = UtClassRefModel(handleId(clazz), clazz::class.java.id, clazz) - System.err.println("ClassRef: $clazz \t\tClassloader: ${clazz.classLoader}") - constructedObjects[clazz] = utModel - utModel - } - - /** - * First tries to construct UtAssembleModel. If failure, constructs UtCompositeModel. - */ - private fun constructFromAny(value: Any): UtModel = - constructedObjects.getOrElse(value) { - tryConstructUtAssembleModel(value) ?: constructCompositeModel(value) - } - - /** - * Constructs UtAssembleModel but does it only for predefined list of classes. - * - * Uses runtime class of an object. - */ - private fun tryConstructUtAssembleModel(value: Any): UtModel? = - findUtAssembleModelConstructor(value::class.java.id)?.let { assembleConstructor -> - try { - assembleConstructor.constructAssembleModel(this, value, valueToClassId(value), handleId(value)) { - constructedObjects[value] = it - } - } catch (e: Exception) { // If UtAssembleModel constructor failed, we need to remove model and return null - constructedObjects.remove(value) - null - } - } - - /** - * Constructs UtCompositeModel. - * - * Uses runtime javaClass to collect ALL fields, except final static fields, and builds this model recursively. - */ - private fun constructCompositeModel(value: Any): UtModel { - // value can be mock only if it was previously constructed from UtCompositeModel - val isMock = objectToModelCache[value]?.isMockModel() ?: false - - val javaClazz = if (isMock) objectToModelCache.getValue(value).classId.jClass else value::class.java - if (!compositeModelStrategy.shouldConstruct(value, javaClazz)) { - return UtCompositeModel( - handleId(value), - javaClazz.id, - isMock, - fields = mutableMapOf() // we don't want to construct any further fields. - ) - } - - val fields = mutableMapOf() - val utModel = UtCompositeModel(handleId(value), javaClazz.id, isMock, fields) - constructedObjects[value] = utModel - generateSequence(javaClazz) { it.superclass }.forEach { clazz -> - val allFields = clazz.declaredFields - allFields - .asSequence() - .filter { !(Modifier.isFinal(it.modifiers) && Modifier.isStatic(it.modifiers)) } // TODO: what about static final fields? - .filterNot { it.fieldId.isInaccessibleViaReflection } - .forEach { it.withAccessibility { fields[it.fieldId] = construct(it.get(value), it.type.id) } } - } - return utModel - } -} - -/** - * Decides, should we construct a UtCompositeModel from a value or not. - */ -interface UtCompositeModelStrategy { - fun shouldConstruct(value: Any, clazz: Class<*>): Boolean -} - -internal object AlwaysConstructStrategy : UtCompositeModelStrategy { - override fun shouldConstruct(value: Any, clazz: Class<*>): Boolean = true -} - -/** - * This class constructs only user classes or values which are already in [objectToModelCache]. - * - * [objectToModelCache] is a cache which we build in the time of creating concrete values from [UtModel]s. - */ -internal class ConstructOnlyUserClassesOrCachedObjectsStrategy( - private val userDependencyPaths: Set, - private val objectToModelCache: IdentityHashMap -) : UtCompositeModelStrategy { - /** - * Check whether [clazz] is a user class or [value] is in cache. - */ - override fun shouldConstruct(value: Any, clazz: Class<*>): Boolean = - isUserClass(clazz) || value in objectToModelCache - - private fun isUserClass(clazz: Class<*>): Boolean = - clazz.protectionDomain.codeSource?.let { it.location.path.asPathToFile() in userDependencyPaths } ?: false - -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/ApplicationContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/ApplicationContext.kt new file mode 100644 index 0000000000..8a17675ee5 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/ApplicationContext.kt @@ -0,0 +1,17 @@ +package org.utbot.framework.context + +import org.utbot.framework.codegen.generator.AbstractCodeGenerator +import org.utbot.framework.codegen.generator.CodeGeneratorParams + +interface ApplicationContext { + val mockerContext: MockerContext + val typeReplacer: TypeReplacer + val nonNullSpeculator: NonNullSpeculator + + fun createConcreteExecutionContext( + fullClasspath: String, + classpathWithoutDependencies: String + ): ConcreteExecutionContext + + fun createCodeGenerator(params: CodeGeneratorParams): AbstractCodeGenerator +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/ConcreteExecutionContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/ConcreteExecutionContext.kt new file mode 100644 index 0000000000..e0d7c360cf --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/ConcreteExecutionContext.kt @@ -0,0 +1,41 @@ +package org.utbot.framework.context + +import org.utbot.engine.MockStrategy +import org.utbot.framework.fuzzer.IdentityPreservingIdGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConcreteContextLoadingResult +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation + +interface ConcreteExecutionContext { + val instrumentationFactory: UtExecutionInstrumentation.Factory<*> + + fun loadContext( + concreteExecutor: ConcreteExecutor, + ): ConcreteContextLoadingResult + + fun transformExecutionsBeforeMinimization( + executions: List, + methodUnderTest: ExecutableId, + ): List + + fun transformExecutionsAfterMinimization( + executions: List, + methodUnderTest: ExecutableId, + rerunExecutor: ConcreteExecutor, + ): List + + fun tryCreateFuzzingContext(params: FuzzingContextParams): JavaFuzzingContext + + data class FuzzingContextParams( + val concreteExecutor: ConcreteExecutor, + val classUnderTest: ClassId, + val idGenerator: IdentityPreservingIdGenerator, + val fuzzingStartTimeMillis: Long, + val fuzzingEndTimeMillis: Long, + val mockStrategy: MockStrategy, + ) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/JavaFuzzingContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/JavaFuzzingContext.kt new file mode 100644 index 0000000000..f383b2c9b6 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/JavaFuzzingContext.kt @@ -0,0 +1,28 @@ +package org.utbot.framework.context + +import org.utbot.framework.fuzzer.IdentityPreservingIdGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult + +interface JavaFuzzingContext { + val classUnderTest: ClassId + val idGenerator: IdentityPreservingIdGenerator + val valueProvider: JavaValueProvider + + fun createStateBefore( + thisInstance: UtModel?, + parameters: List, + statics: Map, + executableToCall: ExecutableId, + ): EnvironmentModels + + fun handleFuzzedConcreteExecutionResult( + methodUnderTest: ExecutableId, + concreteExecutionResult: UtConcreteExecutionResult, + ) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/MockerContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/MockerContext.kt new file mode 100644 index 0000000000..a85f591f5d --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/MockerContext.kt @@ -0,0 +1,13 @@ +package org.utbot.framework.context + +interface MockerContext { + /** + * Shows if we have installed framework dependencies + */ + val mockFrameworkInstalled: Boolean + + /** + * Shows if we have installed static mocking tools + */ + val staticsMockingIsConfigured: Boolean +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/NonNullSpeculator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/NonNullSpeculator.kt new file mode 100644 index 0000000000..9b5c80df46 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/NonNullSpeculator.kt @@ -0,0 +1,28 @@ +package org.utbot.framework.context + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId +import soot.SootField + +/** + * Checks whether accessing [field] (with a method invocation or field access) speculatively + * cannot produce [NullPointerException] (according to its finality or accessibility). + * + * @see docs/SpeculativeFieldNonNullability.md for more information. + * + * NOTE: methods for both [FieldId] and [SootField] are provided, because for some lambdas + * class names in byte code and in Soot do not match, making conversion between two field + * representations not always possible, which in turn makes us to support both [FieldId] + * and [SootField] to be useful for both fuzzer and symbolic engine respectively. + */ +interface NonNullSpeculator { + fun speculativelyCannotProduceNullPointerException( + field: SootField, + classUnderTest: ClassId, + ): Boolean + + fun speculativelyCannotProduceNullPointerException( + field: FieldId, + classUnderTest: ClassId, + ): Boolean +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/TypeReplacer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/TypeReplacer.kt new file mode 100644 index 0000000000..25aec4d025 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/TypeReplacer.kt @@ -0,0 +1,17 @@ +package org.utbot.framework.context + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.TypeReplacementMode + +interface TypeReplacer { + /** + * Shows if there are any restrictions on type implementors. + */ + val typeReplacementMode: TypeReplacementMode + + /** + * Finds a type to replace the original abstract type + * if it is guided with some additional information. + */ + fun replaceTypeIfNeeded(classId: ClassId): ClassId? +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/custom/CoverageFilteringConcreteExecutionContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/custom/CoverageFilteringConcreteExecutionContext.kt new file mode 100644 index 0000000000..8fa66a3f7f --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/custom/CoverageFilteringConcreteExecutionContext.kt @@ -0,0 +1,99 @@ +package org.utbot.framework.context.custom + +import mu.KotlinLogging +import org.utbot.common.hasOnClasspath +import org.utbot.common.tryLoadClass +import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.util.utContext +import java.io.File +import java.net.URLClassLoader + +/** + * Decorator of [delegateContext] that filters coverage before test set minimization + * (see [transformExecutionsBeforeMinimization]) to avoid generating too many tests that + * only increase coverage of third party libraries. + * + * This implementation: + * - always keeps instructions that are in class under test (even if other rules say otherwise) + * - filters out instructions in classes marked with annotations from [annotationsToIgnoreCoverage] + * - filters out instructions from classes that are not found in `classpathToIncludeCoverageFrom` + * + * Finally, if [keepOriginalCoverageOnEmptyFilteredCoverage] is `true` we restore original coverage + * for executions whose coverage becomes empty after filtering. + */ +class CoverageFilteringConcreteExecutionContext( + private val delegateContext: ConcreteExecutionContext, + classpathToIncludeCoverageFrom: String, + private val annotationsToIgnoreCoverage: Set, + private val keepOriginalCoverageOnEmptyFilteredCoverage: Boolean, +) : ConcreteExecutionContext by delegateContext { + private val urlsToIncludeCoverageFrom = classpathToIncludeCoverageFrom.split(File.pathSeparator) + .map { File(it).toURI().toURL() } + .toTypedArray() + + companion object { + private val logger = KotlinLogging.logger {} + } + + override fun transformExecutionsBeforeMinimization( + executions: List, + methodUnderTest: ExecutableId, + ): List { + @Suppress("NAME_SHADOWING") + val executions = delegateContext.transformExecutionsBeforeMinimization(executions, methodUnderTest) + + val classUnderTestId = methodUnderTest.classId + + val annotationsToIgnoreCoverage = + annotationsToIgnoreCoverage.mapNotNull { utContext.classLoader.tryLoadClass(it.name) } + + val classLoaderToIncludeCoverageFrom = URLClassLoader(urlsToIncludeCoverageFrom, null) + + val classesToIncludeCoverageFromCache = mutableMapOf() + + return executions.map { execution -> + val coverage = execution.coverage ?: return@map execution + + val filteredCoveredInstructions = + coverage.coveredInstructions + .filter { instruction -> + val instrClassName = instruction.className + + classesToIncludeCoverageFromCache.getOrPut(instrClassName) { + instrClassName == classUnderTestId.name || + (classLoaderToIncludeCoverageFrom.hasOnClasspath(instrClassName) && + !hasAnnotations(instrClassName, annotationsToIgnoreCoverage)) + } + } + .ifEmpty { + if (keepOriginalCoverageOnEmptyFilteredCoverage) { + logger.warn("Execution covered instruction list became empty. Proceeding with not filtered instruction list.") + coverage.coveredInstructions + } else { + logger.warn("Execution covered instruction list became empty. Proceeding with empty coverage.") + emptyList() + } + } + + execution.copy( + coverage = coverage.copy( + coveredInstructions = filteredCoveredInstructions + ) + ) + } + } + + private fun hasAnnotations(className: String, annotations: List>): Boolean = + utContext + .classLoader + .loadClass(className) + .annotations + .any { existingAnnotation -> + annotations.any { annotation -> + annotation.isInstance(existingAnnotation) + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/custom/MockingJavaFuzzingContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/custom/MockingJavaFuzzingContext.kt new file mode 100644 index 0000000000..1be43981ef --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/custom/MockingJavaFuzzingContext.kt @@ -0,0 +1,58 @@ +package org.utbot.framework.context.custom + +import org.utbot.framework.context.JavaFuzzingContext +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.providers.AnyDepthNullValueProvider +import org.utbot.fuzzing.providers.AnyObjectValueProvider +import org.utbot.fuzzing.spring.unit.MockValueProvider +import org.utbot.fuzzing.spring.decorators.filterSeeds +import org.utbot.fuzzing.spring.decorators.filterTypes +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult + +/** + * Makes fuzzer to use mocks in accordance with [mockPredicate]. + * + * NOTE: + * - fuzzer won't mock types, that have *specific* value providers + * (i.e. ones that do not implement [AnyObjectValueProvider]) + * - fuzzer may still resort to mocks despite [mockPredicate] and *specific* + * value providers if it can't create other non-null values or at runtime + */ +fun JavaFuzzingContext.useMocks(mockPredicate: (FuzzedType) -> Boolean) = + MockingJavaFuzzingContext(delegateContext = this, mockPredicate) + +class MockingJavaFuzzingContext( + val delegateContext: JavaFuzzingContext, + val mockPredicate: (FuzzedType) -> Boolean, +) : JavaFuzzingContext by delegateContext { + private val mockValueProvider = MockValueProvider(delegateContext.idGenerator) + + override val valueProvider: JavaValueProvider = + + delegateContext.valueProvider + // NOTE: we first remove `AnyObjectValueProvider` and `NullValueProvider` from `delegateContext.valueProvider` + // and then add them back as a part of our `withFallback` so they have the same priority as + // `mockValueProvider`, otherwise mocks will never be used where `null` or new object can be used. + .except { it is AnyObjectValueProvider } + .withFallback( + mockValueProvider.filterTypes(mockPredicate) + .with( + delegateContext.valueProvider + .filterTypes { !mockPredicate(it) } + .filterSeeds { (it as? Seed.Simple)?.value?.model !is UtNullModel } + ) + .withFallback(mockValueProvider.with(AnyDepthNullValueProvider)) + ) + + override fun handleFuzzedConcreteExecutionResult( + methodUnderTest: ExecutableId, + concreteExecutionResult: UtConcreteExecutionResult + ) { + delegateContext.handleFuzzedConcreteExecutionResult(methodUnderTest, concreteExecutionResult) + mockValueProvider.addMockingCandidates(concreteExecutionResult.detectedMockingCandidates) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/custom/RerunningConcreteExecutionContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/custom/RerunningConcreteExecutionContext.kt new file mode 100644 index 0000000000..55feae324f --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/custom/RerunningConcreteExecutionContext.kt @@ -0,0 +1,63 @@ +package org.utbot.framework.context.custom + +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import org.utbot.engine.executeConcretely +import org.utbot.framework.UtSettings +import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation + +class RerunningConcreteExecutionContext( + private val delegateContext: ConcreteExecutionContext, + private val maxRerunsPerMethod: Int, + private val rerunTimeoutInMillis: Long = 10L * UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis, +) : ConcreteExecutionContext by delegateContext { + companion object { + private val logger = KotlinLogging.logger {} + } + + override fun transformExecutionsAfterMinimization( + executions: List, + methodUnderTest: ExecutableId, + rerunExecutor: ConcreteExecutor, + ): List { + @Suppress("NAME_SHADOWING") + val executions = delegateContext.transformExecutionsAfterMinimization( + executions, + methodUnderTest, + rerunExecutor + ) + // it's better to rerun executions with non-empty coverage, + // because executions with empty coverage are often duplicated + .sortedBy { it.coverage?.coveredInstructions.isNullOrEmpty() } + return executions + .take(maxRerunsPerMethod) + .map { execution -> + runBlocking { + val result = try { + rerunExecutor.executeConcretely( + methodUnderTest = methodUnderTest, + stateBefore = execution.stateBefore, + instrumentation = emptyList(), + timeoutInMillis = rerunTimeoutInMillis, + isRerun = true, + ) + } catch (e: Throwable) { + // we can't update execution result if we don't have a result + logger.warn(e) { "Rerun failed, keeping original result for execution [$execution]" } + return@runBlocking execution + } + execution.copy( + stateBefore = result.stateBefore, + stateAfter = result.stateAfter, + result = result.result, + coverage = result.coverage, + ) + } + } + executions.drop(maxRerunsPerMethod) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleApplicationContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleApplicationContext.kt new file mode 100644 index 0000000000..3f54edd968 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleApplicationContext.kt @@ -0,0 +1,30 @@ +package org.utbot.framework.context.simple + +import org.utbot.framework.codegen.generator.AbstractCodeGenerator +import org.utbot.framework.codegen.generator.CodeGenerator +import org.utbot.framework.codegen.generator.CodeGeneratorParams +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.context.MockerContext +import org.utbot.framework.context.NonNullSpeculator +import org.utbot.framework.context.TypeReplacer + +/** + * A context to use when no specific data is required. + */ +class SimpleApplicationContext( + override val mockerContext: MockerContext = SimpleMockerContext( + mockFrameworkInstalled = true, + staticsMockingIsConfigured = true + ), + override val typeReplacer: TypeReplacer = SimpleTypeReplacer(), + override val nonNullSpeculator: NonNullSpeculator = SimpleNonNullSpeculator() +) : ApplicationContext { + override fun createConcreteExecutionContext( + fullClasspath: String, + classpathWithoutDependencies: String + ): ConcreteExecutionContext = SimpleConcreteExecutionContext(fullClasspath) + + override fun createCodeGenerator(params: CodeGeneratorParams): AbstractCodeGenerator = + CodeGenerator(params) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleConcreteExecutionContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleConcreteExecutionContext.kt new file mode 100644 index 0000000000..6f074d50f4 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleConcreteExecutionContext.kt @@ -0,0 +1,36 @@ +package org.utbot.framework.context.simple + +import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.context.ConcreteExecutionContext.FuzzingContextParams +import org.utbot.framework.context.JavaFuzzingContext +import org.utbot.framework.plugin.api.ConcreteContextLoadingResult +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.execution.SimpleUtExecutionInstrumentation +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation +import java.io.File + +class SimpleConcreteExecutionContext(fullClassPath: String) : ConcreteExecutionContext { + override val instrumentationFactory: UtExecutionInstrumentation.Factory<*> = + SimpleUtExecutionInstrumentation.Factory(fullClassPath.split(File.pathSeparator).toSet()) + + override fun loadContext( + concreteExecutor: ConcreteExecutor, + ): ConcreteContextLoadingResult = ConcreteContextLoadingResult.successWithoutExceptions() + + override fun transformExecutionsBeforeMinimization( + executions: List, + methodUnderTest: ExecutableId, + ): List = executions + + override fun transformExecutionsAfterMinimization( + executions: List, + methodUnderTest: ExecutableId, + rerunExecutor: ConcreteExecutor, + ): List = executions + + override fun tryCreateFuzzingContext(params: FuzzingContextParams): JavaFuzzingContext = + SimpleJavaFuzzingContext(params.classUnderTest, params.idGenerator) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleJavaFuzzingContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleJavaFuzzingContext.kt new file mode 100644 index 0000000000..fefed3bd7b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleJavaFuzzingContext.kt @@ -0,0 +1,38 @@ +package org.utbot.framework.context.simple + +import org.utbot.framework.context.JavaFuzzingContext +import org.utbot.framework.fuzzer.IdentityPreservingIdGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.defaultValueProviders +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult + +class SimpleJavaFuzzingContext( + override val classUnderTest: ClassId, + override val idGenerator: IdentityPreservingIdGenerator, +) : JavaFuzzingContext { + override val valueProvider: JavaValueProvider = + ValueProvider.of(defaultValueProviders(idGenerator)) + + override fun createStateBefore( + thisInstance: UtModel?, + parameters: List, + statics: Map, + executableToCall: ExecutableId, + ): EnvironmentModels = EnvironmentModels( + thisInstance = thisInstance, + parameters = parameters, + statics = statics, + executableToCall = executableToCall + ) + + override fun handleFuzzedConcreteExecutionResult( + methodUnderTest: ExecutableId, + concreteExecutionResult: UtConcreteExecutionResult + ) = Unit +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleMockerContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleMockerContext.kt new file mode 100644 index 0000000000..e7b0977d9e --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleMockerContext.kt @@ -0,0 +1,16 @@ +package org.utbot.framework.context.simple + +import org.utbot.framework.context.MockerContext + +class SimpleMockerContext( + override val mockFrameworkInstalled: Boolean, + staticsMockingIsConfigured: Boolean +) : MockerContext { + /** + * NOTE: Can only be `true` when [mockFrameworkInstalled], because + * situation when mock framework is not installed but static mocking + * is configured is semantically incorrect. + */ + override val staticsMockingIsConfigured: Boolean = + mockFrameworkInstalled && staticsMockingIsConfigured +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleNonNullSpeculator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleNonNullSpeculator.kt new file mode 100644 index 0000000000..5a0288fc01 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleNonNullSpeculator.kt @@ -0,0 +1,28 @@ +package org.utbot.framework.context.simple + +import org.utbot.framework.UtSettings +import org.utbot.framework.context.NonNullSpeculator +import org.utbot.framework.isFromTrustedLibrary +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.isFinal +import org.utbot.framework.plugin.api.util.isPublic +import soot.SootField + +class SimpleNonNullSpeculator : NonNullSpeculator { + override fun speculativelyCannotProduceNullPointerException( + field: SootField, + classUnderTest: ClassId, + ): Boolean = + !UtSettings.maximizeCoverageUsingReflection && + field.declaringClass.isFromTrustedLibrary() && + (field.isFinal || !field.isPublic) + + override fun speculativelyCannotProduceNullPointerException( + field: FieldId, + classUnderTest: ClassId + ): Boolean = + !UtSettings.maximizeCoverageUsingReflection && + field.declaringClass.isFromTrustedLibrary() && + (field.isFinal || !field.isPublic) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleTypeReplacer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleTypeReplacer.kt new file mode 100644 index 0000000000..3df1f5930e --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleTypeReplacer.kt @@ -0,0 +1,11 @@ +package org.utbot.framework.context.simple + +import org.utbot.framework.context.TypeReplacer +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.TypeReplacementMode + +class SimpleTypeReplacer : TypeReplacer { + override val typeReplacementMode: TypeReplacementMode = TypeReplacementMode.AnyImplementor + + override fun replaceTypeIfNeeded(classId: ClassId): ClassId? = null +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ApplicationContextUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ApplicationContextUtils.kt new file mode 100644 index 0000000000..607015e059 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ApplicationContextUtils.kt @@ -0,0 +1,23 @@ +package org.utbot.framework.context.utils + +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.context.ConcreteExecutionContext.FuzzingContextParams +import org.utbot.fuzzing.JavaValueProvider + +fun ApplicationContext.transformConcreteExecutionContext( + transformer: (ConcreteExecutionContext) -> ConcreteExecutionContext +) = object : ApplicationContext by this { + override fun createConcreteExecutionContext( + fullClasspath: String, + classpathWithoutDependencies: String + ): ConcreteExecutionContext = transformer( + this@transformConcreteExecutionContext.createConcreteExecutionContext( + fullClasspath, classpathWithoutDependencies + ) + ) +} + +fun ApplicationContext.transformValueProvider( + transformer: FuzzingContextParams.(JavaValueProvider) -> JavaValueProvider +) = transformConcreteExecutionContext { it.transformValueProvider(transformer) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ConcreteExecutionContextUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ConcreteExecutionContextUtils.kt new file mode 100644 index 0000000000..a2ff6a751c --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ConcreteExecutionContextUtils.kt @@ -0,0 +1,28 @@ +package org.utbot.framework.context.utils + +import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.context.ConcreteExecutionContext.FuzzingContextParams +import org.utbot.framework.context.JavaFuzzingContext +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation + +fun ConcreteExecutionContext.transformInstrumentationFactory( + transformer: (UtExecutionInstrumentation.Factory<*>) -> UtExecutionInstrumentation.Factory<*> +) = object : ConcreteExecutionContext by this { + override val instrumentationFactory: UtExecutionInstrumentation.Factory<*> = + transformer(this@transformInstrumentationFactory.instrumentationFactory) +} + +fun ConcreteExecutionContext.transformJavaFuzzingContext( + transformer: FuzzingContextParams.(JavaFuzzingContext) -> JavaFuzzingContext +) = object : ConcreteExecutionContext by this { + override fun tryCreateFuzzingContext(params: FuzzingContextParams): JavaFuzzingContext = params.transformer( + this@transformJavaFuzzingContext.tryCreateFuzzingContext(params) + ) +} + +fun ConcreteExecutionContext.transformValueProvider( + transformer: FuzzingContextParams.(JavaValueProvider) -> JavaValueProvider +) = transformJavaFuzzingContext { javaFuzzingContext -> + javaFuzzingContext.transformValueProvider { valueProvider -> transformer(valueProvider) } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/JavaFuzzingContextUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/JavaFuzzingContextUtils.kt new file mode 100644 index 0000000000..4f4cf0714d --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/JavaFuzzingContextUtils.kt @@ -0,0 +1,15 @@ +package org.utbot.framework.context.utils + +import org.utbot.framework.context.JavaFuzzingContext +import org.utbot.fuzzing.JavaValueProvider + +fun JavaFuzzingContext.transformValueProvider( + transformer: (JavaValueProvider) -> JavaValueProvider +) = object : JavaFuzzingContext by this { + override val valueProvider: JavaValueProvider = + transformer(this@transformValueProvider.valueProvider) +} + +fun JavaFuzzingContext.withValueProvider( + valueProvider: JavaValueProvider +) = transformValueProvider { it.with(valueProvider) } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/coverage/CoverageCalculator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/coverage/CoverageCalculator.kt index cfefe3c12e..c662da9b31 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/coverage/CoverageCalculator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/coverage/CoverageCalculator.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.runBlocking fun methodCoverage(executable: ExecutableId, executions: List>, classpath: String): Coverage { val methodSignature = executable.signature val classId = executable.classId - return ConcreteExecutor(CoverageInstrumentation, classpath).let { executor -> + return ConcreteExecutor(CoverageInstrumentation.Factory, classpath).let { executor -> for (execution in executions) { val args = execution.stateBefore.params.map { it.value }.toMutableList() val caller = execution.stateBefore.caller diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt index 196ee58532..a8731dc14d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt @@ -10,15 +10,19 @@ import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtClassRefModel import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtCustomModel import org.utbot.framework.plugin.api.UtEnumConstantModel import org.utbot.framework.plugin.api.UtExecution import org.utbot.framework.plugin.api.UtLambdaModel import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtModelWithCompositeOrigin 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.UtSymbolicExecution import org.utbot.framework.plugin.api.UtVoidModel +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.isSubtypeOf import org.utbot.framework.util.UtModelVisitor import org.utbot.framework.util.hasThisInstance import org.utbot.fuzzer.UtFuzzedExecution @@ -102,31 +106,23 @@ class ExecutionStateAnalyzer(val execution: UtExecution) { initialPath: FieldPath = FieldPath() ): FieldStatesInfo { var modelBefore = before + var modelAfter = after if (before::class != after::class) { - if (before is UtAssembleModel && after is UtCompositeModel && before.origin != null) { - modelBefore = before.origin ?: unreachableBranch("We have already checked the origin for a null value") - } else { - doNotRun { - // it is ok because we might have modelBefore with some absent fields (i.e. statics), but - // modelAfter (constructed by concrete executor) will consist all these fields, - // therefore, AssembleModelGenerator won't be able to transform the given composite model - - val reason = if (before is UtAssembleModel && after is UtCompositeModel) { - "ModelBefore is an AssembleModel and ModelAfter " + - "is a CompositeModel, but modelBefore doesn't have an origin model." - } else { - "The model before and the model after have different types: " + - "model before is ${before::class}, but model after is ${after::class}." - } + if (before is UtModelWithCompositeOrigin) + modelBefore = before.origin ?: before + if (after is UtModelWithCompositeOrigin) + modelAfter = after.origin ?: after + } - error("Cannot analyze fields modification. $reason") - } + if (modelBefore::class != modelAfter::class) { + doNotRun { + error("Cannot analyze model fields modification, before: [$before], after: [$after]") + } - // remove it when we will fix assemble models in the resolver JIRA:1464 - workaround(WorkaroundReason.IGNORE_MODEL_TYPES_INEQUALITY) { - return FieldStatesInfo(fieldsBefore = emptyMap(), fieldsAfter = emptyMap()) - } + // remove it when we will fix assemble models in the resolver JIRA:1464 + workaround(WorkaroundReason.IGNORE_MODEL_TYPES_INEQUALITY) { + return FieldStatesInfo(fieldsBefore = emptyMap(), fieldsAfter = emptyMap()) } } @@ -137,7 +133,7 @@ class ExecutionStateAnalyzer(val execution: UtExecution) { modelBefore.accept(FieldStateVisitor(), dataBefore) val dataAfter = FieldData(FieldsVisitorMode.AFTER, fieldsAfter, initialPath, previousFields = fieldsBefore) - after.accept(FieldStateVisitor(), dataAfter) + modelAfter.accept(FieldStateVisitor(), dataAfter) return FieldStatesInfo(fieldsBefore, fieldsAfter) } @@ -237,6 +233,10 @@ private class FieldStateVisitor : UtModelVisitor() { recordFieldState(data, element) } + override fun visit(element: UtCustomModel, data: FieldData) { + recordFieldState(data, element) + } + private fun recordFieldState(data: FieldData, model: UtModel) { val fields = data.fields val path = data.path @@ -254,6 +254,13 @@ private class FieldStateVisitor : UtModelVisitor() { // sometimes we don't have initial state of the field, e.g. if it is static and we didn't `touch` it // during the analysis, but the concrete executor included it in the modelAfter val initial = previousFields[path] ?: return false + + // TODO usvm-sbft: In USVM descriptors for classes, enums, and throwables don't implement `UTestRefDescriptor` + // and don't have `refId`, which causes `UtReferenceModel.id` to diverge in `stateBefore` and `stateAfter` + if (initial is UtClassRefModel) return initial.value != ((model as? UtClassRefModel)?.value) + if (initial is UtEnumConstantModel) return initial.value != ((model as? UtEnumConstantModel)?.value) + if (initial.classId isSubtypeOf java.lang.Throwable::class.id) return initial.classId != model.classId + return initial != model } } @@ -266,5 +273,7 @@ fun UtModel.accept(visitor: UtModelVisitor, data: D) = visitor.run { is UtPrimitiveModel -> visit(element, data) is UtReferenceModel -> visit(element, data) is UtVoidModel -> visit(element, data) + // PythonModel, JsUtModel may be here + else -> throw UnsupportedOperationException() } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/GreedyEssential.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/GreedyEssential.kt index bbbcafd17f..4f229e3712 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/GreedyEssential.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/GreedyEssential.kt @@ -10,7 +10,8 @@ private inline class LineNumber(val number: Int) * [Greedy essential algorithm](CONFLUENCE:Test+Minimization) */ class GreedyEssential private constructor( - executionToCoveredLines: Map> + executionToCoveredLines: Map>, + executionToPriority: Map ) { private val executionToUsefulLines: Map> = executionToCoveredLines @@ -23,8 +24,11 @@ class GreedyEssential private constructor( .mapValues { it.value.toMutableSet() } private val executionByPriority = - PriorityQueue(compareByDescending> { it.second }.thenBy { it.first.number }) - .apply { + PriorityQueue( + compareBy> { executionToPriority[it.first] } + .thenByDescending { it.second } + .thenBy { it.first.number } + ).apply { addAll( executionToCoveredLines .keys @@ -89,12 +93,16 @@ class GreedyEssential private constructor( * * @return retained execution ids. */ - fun minimize(executions: Map>): List { + fun minimize(executions: Map>, executionToPriority: Map = mapOf()): List { val convertedExecutions = executions .entries .associate { (execution, lines) -> ExecutionNumber(execution) to lines.map { LineNumber(it) } } - val prioritizer = GreedyEssential(convertedExecutions) + val convertedExecutionToPriority = executionToPriority + .entries + .associate { (execution, priority) -> ExecutionNumber(execution) to priority } + + val prioritizer = GreedyEssential(convertedExecutions, convertedExecutionToPriority) val list = mutableListOf() while (prioritizer.hasMore()) { list.add(prioritizer.getExecutionAndRemove()) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt index 3a054916b3..847dca32e4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt @@ -1,24 +1,15 @@ package org.utbot.framework.minimization import org.utbot.framework.UtSettings -import org.utbot.framework.plugin.api.EnvironmentModels -import org.utbot.framework.plugin.api.UtArrayModel -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtClassRefModel -import org.utbot.framework.plugin.api.UtCompositeModel import org.utbot.framework.plugin.api.UtConcreteExecutionFailure -import org.utbot.framework.plugin.api.UtDirectSetFieldModel -import org.utbot.framework.plugin.api.UtEnumConstantModel -import org.utbot.framework.plugin.api.UtExecutableCallModel import org.utbot.framework.plugin.api.UtExecution import org.utbot.framework.plugin.api.UtExecutionFailure import org.utbot.framework.plugin.api.UtExecutionResult -import org.utbot.framework.plugin.api.UtLambdaModel -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.UtStatementModel -import org.utbot.framework.plugin.api.UtVoidModel +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtSymbolicExecution +import org.utbot.framework.util.calculateSize +import org.utbot.fuzzer.UtFuzzedExecution +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructor /** @@ -27,7 +18,8 @@ import org.utbot.framework.plugin.api.UtVoidModel * We have 4 different test suites: * * Regression suite * * Error suite (invocations in which implicitly thrown unchecked exceptions reached to the top) - * * Crash suite (invocations in which the child process crashed or unexpected exception in our code occurred) + * * Crash suite (invocations in which the instrumented process crashed or unexpected exception in our code occurred) + * * Artificial error suite (invocations in which some custom exception like overflow detection occurred) * * Timeout suite * * We want to minimize tests independently in each of these suites. @@ -50,13 +42,31 @@ fun minimizeTestCase( fun minimizeExecutions(executions: List): List { val unknownCoverageExecutions = - executions.indices.filter { executions[it].coverage?.coveredInstructions?.isEmpty() ?: true }.toSet() - // ^^^ here we add executions with empty or null coverage, because it happens only if a concrete execution failed, - // so we don't know the actual coverage for such executions - val mapping = buildMapping(executions.filterIndexed { idx, _ -> idx !in unknownCoverageExecutions }) - val usedExecutionIndexes = (GreedyEssential.minimize(mapping) + unknownCoverageExecutions).toSet() + executions + .filter { it.coverage?.coveredInstructions.isNullOrEmpty() } + .groupBy { + it.result.javaClass to ( + (it.result as? UtExecutionSuccess)?.model ?: (it.result as? UtExecutionFailure)?.exception + )?.javaClass + } + .values + .flatMap { executionsGroup -> + val executionToSize = executionsGroup.associateWith { it.stateBefore.calculateSize() } + executionsGroup + .sortedBy { executionToSize[it] } + .take(UtSettings.maxUnknownCoverageExecutionsPerMethodPerResultType) + } + .toSet() + // Here we add executions with empty or null coverage, because if concrete execution failed, we don't know actual coverage. + // The amount of such executions is limited with [UtSettings.maxUnknownCoverageExecutionsPerMethodPerResultType]. + + val filteredExecutions = filterOutDuplicateCoverages(executions - unknownCoverageExecutions) + val (mapping, executionToPriorityMapping) = buildMapping(filteredExecutions) + + val usedFilteredExecutionIndexes = GreedyEssential.minimize(mapping, executionToPriorityMapping).toSet() + val usedFilteredExecutions = filteredExecutions.filterIndexed { idx, _ -> idx in usedFilteredExecutionIndexes } - val usedMinimizedExecutions = executions.filterIndexed { idx, _ -> idx in usedExecutionIndexes } + val usedMinimizedExecutions = usedFilteredExecutions + unknownCoverageExecutions return if (UtSettings.minimizeCrashExecutions) { usedMinimizedExecutions.filteredCrashExecutions() @@ -65,6 +75,18 @@ fun minimizeExecutions(executions: List): List { } } +private fun filterOutDuplicateCoverages(executions: List): List { + val (executionIdxToCoveredEdgesMap, _) = buildMapping(executions) + return executions + .withIndex() + // we need to group by coveredEdges and not just Coverage to not miss exceptional edges that buildMapping() function adds + .groupBy( + keySelector = { indexedExecution -> executionIdxToCoveredEdgesMap[indexedExecution.index] }, + valueTransform = { indexedExecution -> indexedExecution.value } + ).values + .map { executionsWithEqualCoverage -> executionsWithEqualCoverage.chooseOneExecution() } +} + /** * Groups the [executions] by their `paths` on `first` [branchInstructionsNumber] `branch` instructions. * @@ -86,15 +108,15 @@ private fun groupByBranchInstructions( executions: List, branchInstructionsNumber: Int ): Collection> { - val instructionToPossibleNextInstructions = mutableMapOf>() + val instructionToPossibleNextInstructions = mutableMapOf>() for (execution in executions) { execution.coverage?.let { coverage -> val coveredInstructionIds = coverage.coveredInstructions.map { it.id } - for (i in 0 until coveredInstructionIds.size - 1) { + for (i in coveredInstructionIds.indices) { instructionToPossibleNextInstructions .getOrPut(coveredInstructionIds[i]) { mutableSetOf() } - .add(coveredInstructionIds[i + 1]) + .add(coveredInstructionIds.getOrNull(i + 1)) } } } @@ -143,13 +165,14 @@ private fun groupExecutionsByTestSuite( executions.groupBy { executionToTestSuite(it) }.values /** - * Builds a mapping from execution id to edges id. + * Builds a mapping from execution id to edges id and from execution id to its priority. */ -private fun buildMapping(executions: List): Map> { +private fun buildMapping(executions: List): Pair>, Map> { // (inst1, instr2) -> edge id --- edge represents as a pair of instructions, which are connected by this edge - val allCoveredEdges = mutableMapOf, Int>() + val allCoveredEdges = mutableMapOf, Int>() val thrownExceptions = mutableMapOf() val mapping = mutableMapOf>() + val executionToPriorityMapping = mutableMapOf() executions.forEachIndexed { idx, execution -> @@ -160,20 +183,17 @@ private fun buildMapping(executions: List): Map> { execution.result, thrownExceptions ).let { instructions -> - for (i in 0 until instructions.size - 1) { - allCoveredEdges.putIfAbsent(instructions[i] to instructions[i + 1], allCoveredEdges.size) + val edges = instructions.indices.map { i -> + allCoveredEdges.getOrPut(instructions[i] to instructions.getOrNull(i + 1)) { allCoveredEdges.size } } - val edges = mutableListOf() - for (i in 0 until instructions.size - 1) { - edges += allCoveredEdges[instructions[i] to instructions[i + 1]]!! - } mapping[idx] = edges + executionToPriorityMapping[idx] = execution.getExecutionPriority() } } } - return mapping + return Pair(mapping, executionToPriorityMapping) } /** @@ -188,53 +208,20 @@ private fun List.filteredCrashExecutions(): List { val notCrashExecutions = filterNot { it.result is UtConcreteExecutionFailure } - return notCrashExecutions + crashExecutions.chooseMinimalCrashExecution() -} - -/** - * As for now crash execution can only be produced by Concrete Executor, it does not have [UtExecution.stateAfter] and - * [UtExecution.result] is [UtExecutionFailure], so we check only [UtExecution.stateBefore]. - */ -private fun List.chooseMinimalCrashExecution(): UtExecution = minByOrNull { - it.stateBefore.calculateSize() -} ?: error("Cannot find minimal crash execution within empty executions") - -private fun EnvironmentModels.calculateSize(): Int { - val thisInstanceSize = thisInstance?.calculateSize() ?: 0 - val parametersSize = parameters.sumOf { it.calculateSize() } - val staticsSize = statics.values.sumOf { it.calculateSize() } - - return thisInstanceSize + parametersSize + staticsSize + return notCrashExecutions + crashExecutions.chooseOneExecution() } /** - * We assume that "size" for "common" models is 1, 0 for [UtVoidModel] (as they do not return anything) and - * [UtPrimitiveModel] and [UtNullModel] (we use them as literals in codegen), summarising for all statements for [UtAssembleModel] and - * summarising for all fields and mocks for [UtCompositeModel]. As [UtCompositeModel] could be recursive, we need to - * store it in [used]. Moreover, if we already calculate size for [this], it means that we will use already created - * variable by this model and do not need to create it again, so size should be equal to 0. + * Chooses one execution with the highest [execution priority][getExecutionPriority]. If multiple executions + * have the same priority, then the one with the [smallest][calculateSize] [UtExecution.stateBefore] is chosen. + * + * Only [UtExecution.stateBefore] is considered, because [UtExecution.result] and [UtExecution.stateAfter] + * don't represent true picture as they are limited by [construction depth][UtModelConstructor.maxDepth] and their + * sizes can't be calculated for crushed executions. */ -private fun UtModel.calculateSize(used: MutableSet = mutableSetOf()): Int { - if (this in used) return 0 - - used += this - - return when (this) { - is UtNullModel, is UtPrimitiveModel, UtVoidModel -> 0 - is UtClassRefModel, is UtEnumConstantModel, is UtArrayModel -> 1 - is UtAssembleModel -> { - 1 + instantiationCall.calculateSize(used) + modificationsChain.sumOf { it.calculateSize(used) } - } - is UtCompositeModel -> 1 + fields.values.sumOf { it.calculateSize(used) } - is UtLambdaModel -> 1 + capturedValues.sumOf { it.calculateSize(used) } - } -} - -private fun UtStatementModel.calculateSize(used: MutableSet = mutableSetOf()): Int = - when (this) { - is UtDirectSetFieldModel -> 1 + fieldModel.calculateSize(used) - is UtExecutableCallModel -> 1 + params.sumOf { it.calculateSize(used) } - } +private fun List.chooseOneExecution(): UtExecution = minWithOrNull( + compareBy({ it.getExecutionPriority() }, { it.stateBefore.calculateSize() }) +) ?: error("Cannot find minimal execution within empty executions") /** * Extends the [instructionsWithoutExtra] with one extra instruction if the [result] is @@ -262,4 +249,17 @@ private fun addExtraIfLastInstructionIsException( * Takes an exception name, a class name, a method signature and a line number from exception. */ private fun Throwable.exceptionToInfo(): String = - this::class.java.name + (this.stackTrace.firstOrNull()?.run { className + methodName + lineNumber } ?: "null") \ No newline at end of file + this::class.java.name + (this.stackTrace.firstOrNull()?.run { className + methodName + lineNumber } ?: "null") + +/** + * Returns an execution priority. [UtSymbolicExecution] has the highest priority + * over other executions like [UtFuzzedExecution], [UtFailedExecution], etc. + * + * NOTE! Smaller number represents higher priority. + * + * See [https://github.com/UnitTestBot/UTBotJava/issues/1504] for more details. + */ +private fun UtExecution.getExecutionPriority(): Int = when (this) { + is UtSymbolicExecution -> 0 + else -> 1 +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/AnalysisMode.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/AnalysisMode.kt deleted file mode 100644 index 4b23912483..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/AnalysisMode.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.utbot.framework.modifications - -/** - * Restrictions on demanded modificators - */ -enum class AnalysisMode { - - /** - * Search for all field modificators - */ - AllModificators, - - /** - * Search setters and possible direct accesses - */ - SettersAndDirectAccessors, - - /** - * Search constructors only - */ - Constructors, -} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/AnalysisUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/AnalysisUtils.kt deleted file mode 100644 index 0d08ae5c52..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/AnalysisUtils.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.utbot.framework.modifications - -import org.utbot.engine.canRetrieveBody -import org.utbot.engine.jimpleBody -import soot.SootMethod -import soot.jimple.JimpleBody - -/** - * Retrieves Jimple body of SootMethod. - */ -fun retrieveJimpleBody(sootMethod: SootMethod): JimpleBody? = - if (sootMethod.canRetrieveBody()) sootMethod.jimpleBody() else null \ No newline at end of file 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 deleted file mode 100644 index 6284c344ea..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/ConstructorAnalyzer.kt +++ /dev/null @@ -1,261 +0,0 @@ -package org.utbot.framework.modifications - -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 -import soot.jimple.InvokeExpr -import soot.jimple.JimpleBody -import soot.jimple.ParameterRef -import soot.jimple.StaticFieldRef -import soot.jimple.internal.JAssignStmt -import soot.jimple.internal.JIdentityStmt -import soot.jimple.internal.JInstanceFieldRef -import soot.jimple.internal.JInvokeStmt -import soot.jimple.internal.JReturnStmt -import soot.jimple.internal.JReturnVoidStmt -import soot.jimple.internal.JimpleLocal - -/** - * Information about constructor required to use it - * in assemble model construction process. - * - * @param params describes the params to call constructor with - * @param setFields describes fields set to required value in constructor - * @param affectedFields describes all fields affected in constructor - * */ -data class ConstructorAssembleInfo( - val constructorId: ConstructorId, - val params: Map, - val setFields: Set, - val affectedFields: Set -) - -/** - * Analyzer of constructors based on Soot. - */ -class ConstructorAnalyzer { - private val scene = Scene.v() - - /** - * Verifies that [constructorId] can be used in assemble models. - * Analyses Soot representation of constructor for that. - */ - fun isAppropriate(constructorId: ConstructorId): Boolean { - val sootConstructor = sootConstructor(constructorId) ?: return false - return isAppropriate(sootConstructor) - } - - /** - * Retrieves information about [constructorId] params and modified fields from Soot. - */ - fun analyze(constructorId: ConstructorId): ConstructorAssembleInfo { - val setFields = mutableSetOf() - val affectedFields = mutableSetOf() - - val sootConstructor = sootConstructor(constructorId) - ?: error("Soot representation of $constructorId is not found.") - val params = analyze(sootConstructor, setFields, affectedFields) - - return ConstructorAssembleInfo(constructorId, params, setFields, affectedFields) - } - - //A cache of constructors been analyzed if they are appropriate or not - private val analyzedConstructors: MutableMap = mutableMapOf() - - /** - *Verifies that the body of this constructor - * contains only statements matching pattern - * this.a = something - * where "a" is an argument of the constructor. - */ - private fun isAppropriate(sootConstructor: SootMethod): Boolean { - if (sootConstructor in analyzedConstructors) { - return analyzedConstructors[sootConstructor]!! - } - analyzedConstructors[sootConstructor] = false - - val jimpleBody = retrieveJimpleBody(sootConstructor) ?: return false - if (hasSuspiciousInstructions(jimpleBody) || modifiesStatics(jimpleBody)) { - return false - } - - //find all invoked constructors (support of inheritance), verify they are appropriate - val invocations = invocations(jimpleBody).map { it.method } - invocations.forEach { constructor -> - if (!constructor.isConstructor || !isAppropriate(constructor)) { - return false - } - } - - analyzedConstructors[sootConstructor] = true - return true - } - - /** - * Verifies that assignment has only parameter variable in right part. - * - * Parameter variables differ form other by the first symbol: it is not $. - */ - private fun JAssignStmt.isPrimitive(): Boolean { - val jimpleLocal = this.rightOp as? JimpleLocal ?: return false - return jimpleLocal.name.first() != '$' - } - - private val visitedConstructors = mutableSetOf() - - 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, setFields, affectedFields) - - val indexOfLocals = jimpleVariableIndices(jimpleBody) - val indexedFields = indexToField(sootConstructor).toMutableMap() - - for (invocation in invocations(jimpleBody)) { - val invokedIndexedFields = analyze(invocation.method, setFields, affectedFields) - - for ((index, argument) in invocation.args.withIndex()) { - val fieldId = invokedIndexedFields[index] ?: continue - val fieldIndex = indexOfLocals[argument] ?: continue - - indexedFields[fieldIndex] = fieldId - } - } - - return indexedFields - } - - /** - * Analyze assignments if they are primitive and allow - * to set a field into required value so on. - */ - private fun analyzeAssignments( - jimpleBody: JimpleBody, - setFields: MutableSet, - affectedFields: MutableSet, - ) { - for (assn in assignments(jimpleBody)) { - val leftPart = assn.leftOp as? JInstanceFieldRef ?: continue - - val fieldId = FieldId(leftPart.field.declaringClass.id, leftPart.field.name) - if (assn.isPrimitive()) { - setFields.add(fieldId) - } else { - affectedFields.add(fieldId) - } - } - } - - /** - * Matches an index of constructor argument with a [FieldId]. - */ - private fun indexToField(sootConstructor: SootMethod): Map { - val jimpleBody = retrieveJimpleBody(sootConstructor) ?: return emptyMap() - val assignments = assignments(jimpleBody) - - val indexedFields = mutableMapOf() - for (assn in assignments) { - val jimpleLocal = assn.rightOp as? JimpleLocal ?: continue - - val field = (assn.leftOp as? JInstanceFieldRef)?.field ?: continue - val parameterIndex = jimpleBody.locals.indexOfFirst { it.name == jimpleLocal.name } - indexedFields[parameterIndex - 1] = FieldId(field.declaringClass.id, field.name) - } - - return indexedFields - } - - /** - * Matches Jimple variable name with an index in current constructor. - */ - private fun jimpleVariableIndices(jimpleBody: JimpleBody) = jimpleBody.units - .filterIsInstance() - .filter { it.leftOp is JimpleLocal && it.rightOp is ParameterRef } - .associate { it.leftOp as JimpleLocal to (it.rightOp as ParameterRef).index } - - private val sootConstructorCache = mutableMapOf() - - private fun sootConstructor(constructorId: ConstructorId): SootMethod? { - if (constructorId in sootConstructorCache) { - return sootConstructorCache[constructorId] - } - val sootClass = scene.getSootClass(constructorId.classId.name) - val allConstructors = sootClass.methods.filter { it.isConstructor } - val sootConstructor = allConstructors.firstOrNull { sameParameterTypes(it, constructorId) } - - if (sootConstructor != null) { - sootConstructorCache[constructorId] = sootConstructor - return sootConstructor - } - - return null - } - - private fun hasSuspiciousInstructions(jimpleBody: JimpleBody): Boolean = - jimpleBody.units.any { - it !is JIdentityStmt - && !(it is JAssignStmt && it.rightOp !is InvokeExpr) - && it !is JInvokeStmt - && it !is JReturnStmt - && it !is JReturnVoidStmt - } - - private fun modifiesStatics(jimpleBody: JimpleBody): Boolean = - jimpleBody.units.any { it is JAssignStmt && it.leftOp is StaticFieldRef } - - private fun assignments(jimpleBody: JimpleBody) = - jimpleBody.units - .filterIsInstance() - - private fun invocations(jimpleBody: JimpleBody): List = - jimpleBody.units - .filterIsInstance() - .map { it.invokeExpr } - - private fun sameParameterTypes(sootMethod: SootMethod, constructorId: ConstructorId): Boolean { - val sootConstructorTypes = sootMethod.parameterTypes - val constructorTypes = constructorId.parameters.map { getParameterType(it) } - - val sootConstructorParamsCount = sootConstructorTypes.count() - val constructorParamsCount = constructorTypes.count() - - if (sootConstructorParamsCount != constructorParamsCount) return false - for (i in 0 until sootConstructorParamsCount) { - if (sootConstructorTypes[i] != constructorTypes[i]) return false - } - - return true - } - - /** - * Restores [Type] by [ClassId] if possible. - * - * Note: we return null if restore process failed. Possibly we need to - * enlarge a set of cases types we can deal with in the future. - */ - private fun getParameterType(type: ClassId): Type? = - try { - when { - type.isRefType -> scene.getRefType(type.name) - type.isArray -> scene.getType(type.jClass.canonicalName) - else -> scene.getType(type.name) - } - } catch (e: Exception) { - null - } -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/UtBotFieldsModificatorsSearcher.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/UtBotFieldsModificatorsSearcher.kt deleted file mode 100644 index dfde6cbb4e..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/UtBotFieldsModificatorsSearcher.kt +++ /dev/null @@ -1,44 +0,0 @@ -package org.utbot.framework.modifications - -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.StatementId - -class UtBotFieldsModificatorsSearcher { - - private var statementsStorage = StatementsStorage() - - fun update(classIds: Set) = statementsStorage.update(classIds) - - fun delete(classIds: Set) = statementsStorage.delete(classIds) - - /** - * Finds field modificators. - * - * @param analysisMode represents which type of modificators (e.g. setters) are considered. - */ - fun findModificators(analysisMode: AnalysisMode): Map> { - statementsStorage.updateCaches() - return findModificatorsInCache(analysisMode) - } - - /** - * Requests modifications in storage and does the inversion - * of storage map into a FieldId -> Set one. - */ - private fun findModificatorsInCache(analysisMode: AnalysisMode): Map> { - val modifications = mutableMapOf>() - - for (statementWithInfo in statementsStorage.items.filter { it.value.isRoot }) { - val statementId = statementWithInfo.key - - val modifiedFields = statementsStorage.find(statementId, analysisMode) - - modifiedFields.forEach { - modifications[it]?.add(statementId) ?: modifications.put(it, mutableSetOf(statementId)) - } - } - - return modifications - } -} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JVMTestFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JVMTestFrameworkManager.kt new file mode 100644 index 0000000000..4bc9f33249 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JVMTestFrameworkManager.kt @@ -0,0 +1,25 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.TestNg +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.services.framework.Junit4Manager +import org.utbot.framework.codegen.services.framework.Junit5Manager +import org.utbot.framework.codegen.services.framework.TestNgManager +import org.utbot.framework.codegen.services.language.LanguageTestFrameworkManager + +class JVMTestFrameworkManager : LanguageTestFrameworkManager() { + + override fun managerByFramework(context: CgContext) = when (context.testFramework) { + is Junit4 -> Junit4Manager(context) + is Junit5 -> Junit5Manager(context) + is TestNg -> TestNgManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework = Junit5 + + override val testFrameworks = listOf(Junit4, Junit5, TestNg) + +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/MethodDescriptionUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/MethodDescriptionUtil.kt new file mode 100644 index 0000000000..33d5b1142c --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/MethodDescriptionUtil.kt @@ -0,0 +1,25 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.plugin.api.util.declaringClazz +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.jvm.javaType + +// Note that rules for obtaining signature here should correlate with PsiMethod.signature() +fun KFunction<*>.methodDescription() = + MethodDescription( + this.name, + this.declaringClazz.name, + this.parameters.filter { it.kind != KParameter.Kind.INSTANCE }.map { it.type.javaType.typeName } + ) + +// Similar to MethodId, but significantly simplified -- used only to match methods from psi and their reflections +data class MethodDescription(val name: String, val containingClass: String?, val parameterTypes: List) { + + fun normalized() = this.copy( + containingClass = containingClass?.replace("$", "."), // normalize names of nested classes + parameterTypes = parameterTypes.map { + it?.replace("$", ".") + } + ) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/SignatureUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/SignatureUtil.kt deleted file mode 100644 index d8825e22e4..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/SignatureUtil.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.utbot.framework.plugin.api - -import kotlin.reflect.KFunction -import kotlin.reflect.KParameter -import kotlin.reflect.jvm.javaType - -fun KFunction<*>.signature() = - Signature(this.name, this.parameters.filter { it.kind == KParameter.Kind.VALUE }.map { it.type.javaType.typeName }) - -data class Signature(val name: String, val parameterTypes: List) { - - fun normalized() = this.copy( - parameterTypes = parameterTypes.map { - it?.replace("$", ".") // normalize names of nested classes - } - ) -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt index d16101086c..634a66f6e8 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt @@ -1,53 +1,52 @@ package org.utbot.framework.plugin.api -import com.google.protobuf.compiler.PluginProtos import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.yield import mu.KLogger import mu.KotlinLogging -import org.utbot.common.bracket -import org.utbot.common.runBlockingWithCancellationPredicate -import org.utbot.common.runIgnoringCancellationException -import org.utbot.common.trace +import org.utbot.common.* import org.utbot.engine.EngineController import org.utbot.engine.Mocker import org.utbot.engine.UtBotSymbolicEngine +import org.utbot.engine.util.mockListeners.ForceMockListener +import org.utbot.engine.util.mockListeners.ForceStaticMockListener import org.utbot.framework.TestSelectionStrategyType import org.utbot.framework.UtSettings import org.utbot.framework.UtSettings.checkSolverTimeoutMillis import org.utbot.framework.UtSettings.disableCoroutinesDebug import org.utbot.framework.UtSettings.utBotGenerationTimeoutInMillis import org.utbot.framework.UtSettings.warmupConcreteExecution -import org.utbot.framework.codegen.model.util.checkFrameworkDependencies -import org.utbot.framework.concrete.UtConcreteExecutionData -import org.utbot.framework.concrete.UtExecutionInstrumentation -import org.utbot.framework.concrete.UtModelConstructor +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.context.simple.SimpleApplicationContext +import org.utbot.framework.context.simple.SimpleMockerContext +import org.utbot.framework.plugin.api.utils.checkFrameworkDependencies import org.utbot.framework.minimization.minimizeTestCase -import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.intArrayClassId import org.utbot.framework.plugin.api.util.utContext -import org.utbot.framework.plugin.api.util.withUtContext import org.utbot.framework.plugin.services.JdkInfo +import org.utbot.framework.util.Conflict +import org.utbot.framework.util.ConflictTriggers import org.utbot.framework.util.SootUtils import org.utbot.framework.util.jimpleBody import org.utbot.framework.util.toModel import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation import org.utbot.instrumentation.warmup -import org.utbot.instrumentation.warmup.Warmup +import org.utbot.taint.TaintConfigurationProvider import java.io.File import java.nio.file.Path -import java.util.* import kotlin.coroutines.cancellation.CancellationException import kotlin.math.min -import kotlin.reflect.KCallable /** * Generates test cases: one by one or a whole set for the method under test. @@ -67,11 +66,21 @@ open class TestCaseGenerator( val engineActions: MutableList<(UtBotSymbolicEngine) -> Unit> = mutableListOf(), val isCanceled: () -> Boolean = { false }, val forceSootReload: Boolean = true, + val applicationContext: ApplicationContext = SimpleApplicationContext( + SimpleMockerContext( + mockFrameworkInstalled = true, + staticsMockingIsConfigured = true + ) + ), ) { private val logger: KLogger = KotlinLogging.logger {} private val timeoutLogger: KLogger = KotlinLogging.logger(logger.name + ".timeout") + protected val concreteExecutionContext = applicationContext.createConcreteExecutionContext( + fullClasspath = classpathForEngine, + classpathWithoutDependencies = buildDirs.joinToString(File.pathSeparator) + ) - private val classpathForEngine: String + protected val classpathForEngine: String get() = (buildDirs + listOfNotNull(classpath)).joinToString(File.pathSeparator) init { @@ -85,36 +94,43 @@ open class TestCaseGenerator( System.setProperty(kotlinx.coroutines.DEBUG_PROPERTY_NAME, kotlinx.coroutines.DEBUG_PROPERTY_VALUE_OFF) } - timeoutLogger.trace().bracket("Soot initialization") { + timeoutLogger.trace().measureTime({ "Soot initialization"} ) { SootUtils.runSoot(buildDirs, classpath, forceSootReload, jdkInfo) } //warmup if (warmupConcreteExecution) { + // force pool to create an appropriate executor + // TODO ensure that instrumented process that starts here is properly terminated ConcreteExecutor( - UtExecutionInstrumentation, + concreteExecutionContext.instrumentationFactory, classpathForEngine, - dependencyPaths ).apply { - classLoader = utContext.classLoader - withUtContext(UtContext(Warmup::class.java.classLoader)) { - runBlocking { - constructExecutionsForWarmup().forEach { (method, data) -> - executeAsync(method, emptyArray(), data) - } - } - } warmup() } } } } - fun minimizeExecutions(executions: List): List = - if (UtSettings.testMinimizationStrategyType == TestSelectionStrategyType.DO_NOT_MINIMIZE_STRATEGY) { - executions - } else { - minimizeTestCase(executions) { it.result::class.java } + fun minimizeExecutions( + methodUnderTest: ExecutableId, + executions: List, + rerunExecutor: ConcreteExecutor, + ): List = + when (UtSettings.testMinimizationStrategyType) { + TestSelectionStrategyType.DO_NOT_MINIMIZE_STRATEGY -> executions + TestSelectionStrategyType.COVERAGE_STRATEGY -> + concreteExecutionContext.transformExecutionsAfterMinimization( + minimizeTestCase( + concreteExecutionContext.transformExecutionsBeforeMinimization( + executions, + methodUnderTest, + ), + executionToTestSuite = { it.result::class.java } + ), + methodUnderTest, + rerunExecutor = rerunExecutor, + ) } @Throws(CancellationException::class) @@ -123,13 +139,30 @@ open class TestCaseGenerator( method: ExecutableId, mockStrategy: MockStrategyApi, chosenClassesToMockAlways: Set = Mocker.javaDefaultClasses.mapTo(mutableSetOf()) { it.id }, - executionTimeEstimator: ExecutionTimeEstimator = ExecutionTimeEstimator(utBotGenerationTimeoutInMillis, 1) - ): Flow { + executionTimeEstimator: ExecutionTimeEstimator = ExecutionTimeEstimator(utBotGenerationTimeoutInMillis, 1), + userTaintConfigurationProvider: TaintConfigurationProvider? = null, + ): Flow = flow { + if (isCanceled()) + return@flow + + val contextLoadingResult = loadConcreteExecutionContext() + emitAll(flowOf(*contextLoadingResult.utErrors.toTypedArray())) + if (!contextLoadingResult.contextLoaded) + return@flow + try { - val engine = createSymbolicEngine(controller, method, mockStrategy, chosenClassesToMockAlways, executionTimeEstimator) + val engine = createSymbolicEngine( + controller, + method, + mockStrategy, + chosenClassesToMockAlways, + applicationContext, + executionTimeEstimator, + userTaintConfigurationProvider, + ) engineActions.map { engine.apply(it) } engineActions.clear() - return defaultTestFlow(engine, executionTimeEstimator.userTimeout) + emitAll(defaultTestFlow(engine, executionTimeEstimator.userTimeout)) } catch (e: Exception) { logger.error(e) {"Generate async failed"} throw e @@ -141,9 +174,19 @@ open class TestCaseGenerator( mockStrategy: MockStrategyApi, chosenClassesToMockAlways: Set = Mocker.javaDefaultClasses.mapTo(mutableSetOf()) { it.id }, methodsGenerationTimeout: Long = utBotGenerationTimeoutInMillis, + userTaintConfigurationProvider: TaintConfigurationProvider? = null, generate: (engine: UtBotSymbolicEngine) -> Flow = defaultTestFlow(methodsGenerationTimeout) - ): List { - if (isCanceled()) return methods.map { UtMethodTestSet(it) } + ): List = ConcreteExecutor.defaultPool.use { _ -> // TODO: think on appropriate way to close instrumented processes + if (isCanceled()) return@use methods.map { UtMethodTestSet(it) } + + val contextLoadingResult = loadConcreteExecutionContext() + + val method2errors: Map> = methods.associateWith { + contextLoadingResult.utErrors.associateTo(mutableMapOf()) { it.description to 1 } + } + + if (!contextLoadingResult.contextLoaded) + return@use methods.map { method -> UtMethodTestSet(method, errors = method2errors.getValue(method)) } val executionStartInMillis = System.currentTimeMillis() val executionTimeEstimator = ExecutionTimeEstimator(methodsGenerationTimeout, methods.size) @@ -152,7 +195,10 @@ open class TestCaseGenerator( val method2controller = methods.associateWith { EngineController() } val method2executions = methods.associateWith { mutableListOf() } - val method2errors = methods.associateWith { mutableMapOf() } + + val conflictTriggers = ConflictTriggers() + val forceMockListener = ForceMockListener.create(this, conflictTriggers) + val forceStaticMockListener = ForceStaticMockListener.create(this, conflictTriggers) runIgnoringCancellationException { runBlockingWithCancellationPredicate(isCanceled) { @@ -169,10 +215,13 @@ open class TestCaseGenerator( method, mockStrategy, chosenClassesToMockAlways, - executionTimeEstimator + applicationContext, + executionTimeEstimator, + userTaintConfigurationProvider, ) engineActions.map { engine.apply(it) } + engineActions.clear() generate(engine) .catch { @@ -180,20 +229,34 @@ open class TestCaseGenerator( } .collect { when (it) { - is UtExecution -> method2executions.getValue(method) += it - is UtError -> method2errors.getValue(method).merge(it.description, 1, Int::plus) + is UtExecution -> { + if (it is UtSymbolicExecution && + (conflictTriggers.triggered(Conflict.ForceMockHappened) || + conflictTriggers.triggered(Conflict.ForceStaticMockHappened)) + ) { + it.containsMocking = true + } + method2executions.getValue(method) += it + } + is UtError -> { + method2errors.getValue(method).merge(it.description, 1, Int::plus) + logger.error(it.error) { "UtError occurred" } + } } } } catch (e: Exception) { logger.error(e) {"Error in engine"} + throw e } } controller.paused = true + conflictTriggers.reset(Conflict.ForceMockHappened, Conflict.ForceStaticMockHappened) } // All jobs are in the method2controller now (paused). execute them with timeout GlobalScope.launch { + logger.debug("test generator global scope lifecycle check started") while (isActive) { var activeCount = 0 for ((method, controller) in method2controller) { @@ -203,7 +266,7 @@ open class TestCaseGenerator( method2controller.values.forEach { it.paused = true } controller.paused = false - logger.info { "|> Resuming method $method" } + logger.info { "Resuming method $method" } val startTime = System.currentTimeMillis() while (controller.job!!.isActive && (System.currentTimeMillis() - startTime) < executionTimeEstimator.timeslotForOneToplevelMethodTraversalInMillis @@ -219,55 +282,36 @@ open class TestCaseGenerator( } if (activeCount == 0) break } + logger.debug("test generator global scope lifecycle check ended") } } } - ConcreteExecutor.defaultPool.close() // TODO: think on appropriate way to close child processes + forceMockListener.detach(this, forceMockListener) + forceStaticMockListener.detach(this, forceStaticMockListener) - return methods.map { method -> + return@use methods.map { method -> UtMethodTestSet( method, - minimizeExecutions(method2executions.getValue(method)), + minimizeExecutions( + method, + method2executions.getValue(method), + rerunExecutor = ConcreteExecutor(concreteExecutionContext.instrumentationFactory, classpathForEngine) + ), jimpleBody(method), method2errors.getValue(method) ) } } - private fun constructExecutionsForWarmup(): Sequence, UtConcreteExecutionData>> = - UtModelConstructor(IdentityHashMap()).run { - sequenceOf( - Warmup::doWarmup1 to UtConcreteExecutionData( - EnvironmentModels( - construct(Warmup(5), Warmup::class.java.id), - listOf(construct(Warmup(1), Warmup::class.java.id)), - emptyMap() - ), emptyList() - ), - Warmup::doWarmup2 to UtConcreteExecutionData( - EnvironmentModels( - construct(Warmup(1), Warmup::class.java.id), - listOf(construct(intArrayOf(1, 2, 3), intArrayClassId)), - emptyMap() - ), emptyList() - ), - Warmup::doWarmup2 to UtConcreteExecutionData( - EnvironmentModels( - construct(Warmup(1), Warmup::class.java.id), - listOf(construct(intArrayOf(1, 2, 3, 4, 5, 6), intArrayClassId)), - emptyMap() - ), emptyList() - ), - ) - } - private fun createSymbolicEngine( controller: EngineController, method: ExecutableId, mockStrategyApi: MockStrategyApi, chosenClassesToMockAlways: Set, - executionTimeEstimator: ExecutionTimeEstimator + applicationContext: ApplicationContext, + executionTimeEstimator: ExecutionTimeEstimator, + userTaintConfigurationProvider: TaintConfigurationProvider? = null, ): UtBotSymbolicEngine { logger.debug("Starting symbolic execution for $method --$mockStrategyApi--") return UtBotSymbolicEngine( @@ -277,7 +321,10 @@ open class TestCaseGenerator( dependencyPaths = dependencyPaths, mockStrategy = mockStrategyApi.toModel(), chosenClassesToMockAlways = chosenClassesToMockAlways, - solverTimeoutInMillis = executionTimeEstimator.updatedSolverCheckTimeoutMillis + applicationContext = applicationContext, + concreteExecutionContext = concreteExecutionContext, + solverTimeoutInMillis = executionTimeEstimator.updatedSolverCheckTimeoutMillis, + userTaintConfigurationProvider = userTaintConfigurationProvider, ) } @@ -288,8 +335,7 @@ open class TestCaseGenerator( private val halfTimeUserExpectsToWaitInMillis = userTimeout / 2 // If the half is too much for concrete execution, decrease the concrete timeout - var concreteExecutionBudgetInMillis = - min(halfTimeUserExpectsToWaitInMillis, 300L * methodsUnderTestNumber) + val concreteExecutionBudgetInMillis = min(halfTimeUserExpectsToWaitInMillis, 300L * methodsUnderTestNumber) // The symbolic execution time is the reminder but not longer than checkSolverTimeoutMillis times methods number val symbolicExecutionTimeout = userTimeout - concreteExecutionBudgetInMillis @@ -304,14 +350,6 @@ open class TestCaseGenerator( // Now we calculate the solver timeout. Each method is supposed to get some time in worst-case scenario val updatedSolverCheckTimeoutMillis = if (symbolicExecutionTimePerMethod < checkSolverTimeoutMillis) symbolicExecutionTimePerMethod else checkSolverTimeoutMillis - - init { - // Update the concrete execution time, if symbolic execution time is small - // because of UtSettings.checkSolverTimeoutMillis - concreteExecutionBudgetInMillis = userTimeout - symbolicExecutionTimeout - require(symbolicExecutionTimeout > 10) - require(concreteExecutionBudgetInMillis > 10) - } } private fun updateLifecycle( @@ -342,6 +380,9 @@ open class TestCaseGenerator( } } + private fun loadConcreteExecutionContext(): ConcreteContextLoadingResult { + // force pool to create an appropriate executor + val concreteExecutor = ConcreteExecutor(concreteExecutionContext.instrumentationFactory, classpathForEngine) + return concreteExecutionContext.loadContext(concreteExecutor) + } } - - diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestFlow.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestFlow.kt index 2407407acd..b62ecace4d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestFlow.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestFlow.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.flattenConcat import kotlinx.coroutines.flow.flowOf import org.utbot.engine.UtBotSymbolicEngine import org.utbot.framework.UtSettings +import org.utbot.framework.process.generated.GenerateParams /** * Constructs [TestFlow] for customization and creates flow producer. diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/ClassNameUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/ClassNameUtils.kt new file mode 100644 index 0000000000..c816bc65ab --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/ClassNameUtils.kt @@ -0,0 +1,22 @@ +package org.utbot.framework.plugin.api.utils + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.nameWithEnclosingClassesAsContigousString + +object ClassNameUtils { + fun generateTestClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId, + ): Pair { + val packagePrefix = if (testClassPackageName.isNotEmpty()) "$testClassPackageName." else "" + val simpleName = testClassCustomName ?: generateTestClassShortName(classUnderTest) + val name = "$packagePrefix$simpleName" + return Pair(name, simpleName) + } + + fun generateTestClassShortName(classUnderTest: ClassId): String = + "${classUnderTest.nameWithEnclosingClassesAsContigousString}Test" +} + + diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/DependencyPatterns.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/DependencyPatterns.kt new file mode 100644 index 0000000000..8a4f3e1729 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/DependencyPatterns.kt @@ -0,0 +1,107 @@ +package org.utbot.framework.plugin.api.utils + +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.TestNg +import org.utbot.framework.plugin.api.MockFramework + +data class Patterns( + val moduleLibraryPatterns: List, + val libraryPatterns: List, +) + +fun TestFramework.patterns(): Patterns { + val moduleLibraryPatterns = when (this) { + Junit4 -> junit4ModulePatterns + Junit5 -> junit5ModulePatterns + TestNg -> testNgModulePatterns + else -> throw UnsupportedOperationException("Unknown test framework $this") + } + val libraryPatterns = when (this) { + Junit4 -> junit4Patterns + Junit5 -> junit5Patterns + TestNg -> testNgPatterns + else -> throw UnsupportedOperationException("Unknown test framework $this") + } + + return Patterns(moduleLibraryPatterns, libraryPatterns) +} + + +fun TestFramework.parametrizedTestsPatterns(): Patterns { + val moduleLibraryPatterns = when (this) { + Junit4 -> emptyList() + Junit5 -> emptyList() // emptyList here because JUnit5 module may not be enough for parametrized tests if :junit-jupiter-params: is not installed + TestNg -> testNgModulePatterns + else -> throw UnsupportedOperationException("Unknown test framework $this") + } + val libraryPatterns = when (this) { + Junit4 -> emptyList() + Junit5 -> junit5ParametrizedTestsPatterns + TestNg -> testNgPatterns + else -> throw UnsupportedOperationException("Unknown test framework $this") + } + + return Patterns(moduleLibraryPatterns, libraryPatterns) +} + + +fun MockFramework.patterns(): Patterns { + val moduleLibraryPatterns = when (this) { + MockFramework.MOCKITO -> mockitoModulePatterns + } + val libraryPatterns = when (this) { + MockFramework.MOCKITO -> mockitoPatterns + } + + return Patterns(moduleLibraryPatterns, libraryPatterns) +} + +fun MockFramework.manifestPatterns(): Patterns { + val moduleLibraryPatterns = when (this) { + MockFramework.MOCKITO -> mockitoManifestPatterns + } + val libraryPatterns = when (this) { + MockFramework.MOCKITO -> mockitoManifestPatterns + } + return Patterns(moduleLibraryPatterns, libraryPatterns) +} + +val JUNIT_4_JAR_PATTERN = Regex("junit-4(\\.1[2-9])(\\.[0-9]+)?") +val JUNIT_4_MVN_PATTERN = Regex("junit:junit:4(\\.1[2-9])(\\.[0-9]+)?") +val junit4Patterns = listOf(JUNIT_4_JAR_PATTERN, JUNIT_4_MVN_PATTERN) +val junit4ModulePatterns = listOf(JUNIT_4_JAR_PATTERN, JUNIT_4_MVN_PATTERN) + +val JUNIT_5_JAR_PATTERN = Regex("junit-jupiter-5(\\.[0-9]+){1,2}") +val JUNIT_5_MVN_PATTERN = Regex("org\\.junit\\.jupiter:junit-jupiter-api:5(\\.[0-9]+){1,2}") +val JUNIT_5_BASIC_PATTERN = Regex("JUnit5\\.4") +val junit5Patterns = listOf(JUNIT_5_JAR_PATTERN, JUNIT_5_MVN_PATTERN, JUNIT_5_BASIC_PATTERN) + +val JUNIT_5_PARAMETRIZED_JAR_PATTERN = Regex("junit-jupiter-params-5(\\.[0-9]+){1,2}") +val JUNIT_5_PARAMETRIZED_MVN_PATTERN = Regex("org\\.junit\\.jupiter\\.junit-jupiter-params:5(\\.[0-9]+){1,2}") +val junit5ParametrizedTestsPatterns = listOf(JUNIT_5_JAR_PATTERN, JUNIT_5_BASIC_PATTERN, + JUNIT_5_PARAMETRIZED_JAR_PATTERN, JUNIT_5_PARAMETRIZED_MVN_PATTERN) + +val JUNIT5_BASIC_MODULE_PATTERN = Regex("junit-jupiter") +val junit5ModulePatterns = listOf(JUNIT5_BASIC_MODULE_PATTERN) + +val TEST_NG_JAR_PATTERN = Regex("testng-[0-9](\\.[0-9]+){2}") +val TEST_NG_MVN_PATTERN = Regex("org\\.testng:testng:[0-9](\\.[0-9]+){2}") +val TEST_NG_BASIC_PATTERN = Regex("testng") +val testNgPatterns = listOf(TEST_NG_JAR_PATTERN, TEST_NG_MVN_PATTERN, TEST_NG_BASIC_PATTERN) + +val TEST_NG_BASIC_MODULE_PATTERN = Regex("testng") +val testNgModulePatterns = listOf(TEST_NG_BASIC_MODULE_PATTERN) + +val MOCKITO_JAR_PATTERN = Regex("mockito-core-[3-9](\\.[0-9]+){2}") +val MOCKITO_MVN_PATTERN = Regex("org\\.mockito:mockito-core:[3-9](\\.[0-9]+){2}") +val mockitoPatterns = listOf(MOCKITO_JAR_PATTERN, MOCKITO_MVN_PATTERN) +val mockitoModulePatterns = listOf(MOCKITO_JAR_PATTERN, MOCKITO_MVN_PATTERN) + +val MOCKITO_MANIFEST_PATTERN = Regex("mockito-core") +val mockitoManifestPatterns = listOf(MOCKITO_MANIFEST_PATTERN) + +const val MOCKITO_EXTENSIONS_FOLDER = "mockito-extensions" +const val MOCKITO_MOCKMAKER_FILE_NAME = "org.mockito.plugins.MockMaker" +val MOCKITO_EXTENSIONS_FILE_CONTENT = "mock-maker-inline" diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/DependencyUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/DependencyUtils.kt new file mode 100644 index 0000000000..285c30e33f --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/DependencyUtils.kt @@ -0,0 +1,97 @@ +package org.utbot.framework.plugin.api.utils + +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation +import org.utbot.framework.plugin.api.MockFramework +import java.io.File +import java.util.jar.JarFile +import mu.KotlinLogging + +private val logger = KotlinLogging.logger {} + +/** + * Checks that dependency paths contains some frameworks + * and their versions correspond to our requirements. + * + * Note: [UtExecutionInstrumentation] must be in dependency path too + * as it is used by Engine in the instrumented process in Concrete Executor. + */ +fun checkFrameworkDependencies(dependencyPaths: String?) { + if (dependencyPaths.isNullOrEmpty()) { + error("Dependency paths is empty, no test framework and mock framework to generate tests") + } + + //TODO: JIRA:1659 + // check (somehow) that [UtExecutionInstrumentation] is in dependency path: in one of jars or folders + + val dependencyPathsSequence = dependencyPaths.splitToSequence(File.pathSeparatorChar) + + val dependencyNames = dependencyPathsSequence + .mapNotNull { findDependencyName(it) } + .map { it.toLowerCase() } + .toSet() + +//todo: stopped working after transition to Java 11. Ask Egor to take a look. +// val testFrameworkPatterns = TestFramework.allItems.map { it.patterns() } +// val testFrameworkFound = dependencyNames.matchesAnyOf(testFrameworkPatterns) || +// dependencyPathsSequence.any { checkDependencyIsFatJar(it) } +// +// if (!testFrameworkFound) { +// error(""" +// Test frameworks are not found in dependency path $dependencyPaths, dependency names are: +// ${dependencyNames.joinToString(System.lineSeparator())} +// """ +// ) +// } + + val mockFrameworkPatterns = MockFramework.allItems.map { it.manifestPatterns() } + val mockFrameworkFound = dependencyNames.matchesAnyOf(mockFrameworkPatterns) || + dependencyPathsSequence.any { checkDependencyIsFatJar(it) } + + if (!mockFrameworkFound) { + error(""" + Mock frameworks are not found in dependency path $dependencyPaths, dependency names are: + ${dependencyNames.joinToString(System.lineSeparator())} + """ + ) + } +} + +private fun Set.matchesAnyOf(patterns: List): Boolean { + val expressions = patterns.flatMap { it.moduleLibraryPatterns + it.libraryPatterns } + return any { libraryName -> + expressions.any { expr -> libraryName.let { expr.containsMatchIn(it) } } + } +} + +private fun findDependencyName(jarPath: String): String? { + try { + val attributes = JarFile(jarPath).manifest.mainAttributes + + val bundleName = attributes.getValue("Bundle-SymbolicName") + val bundleVersion = attributes.getValue("Bundle-Version") + val moduleName = attributes.getValue("Automatic-Module-Name") + val implementationTitle = attributes.getValue("Implementation-Title") + val implementationVersion = attributes.getValue("Implementation-Version") + + if (bundleName != null) return "$bundleName:$bundleVersion" + if (moduleName != null) return "$moduleName:$implementationTitle:$implementationVersion" + } catch (e: Exception) { + logger.warn { "Unexpected error during parsing $jarPath manifest file $e" } + } + + return null +} + +// We consider Fat JARs contain test frameworks and mock frameworks in the dependencies. +private fun checkDependencyIsFatJar(jarPath: String): Boolean { + try { + val attributes = JarFile(jarPath).manifest.mainAttributes + val jarType = attributes.getValue("JAR-Type") + + return jarType == "Fat JAR" + } catch (e: Exception) { + logger.warn { "Unexpected error during parsing $jarPath manifest file $e" } + } + + return false +} 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 index 4c41010460..26547c212b 100644 --- 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 @@ -1,14 +1,16 @@ package org.utbot.framework.plugin.sarif -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.NoStaticMocking -import org.utbot.framework.codegen.model.CodeGenerator +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.NoStaticMocking +import org.utbot.framework.codegen.generator.CodeGenerator +import org.utbot.framework.codegen.generator.CodeGeneratorParams +import org.utbot.framework.codegen.services.language.CgLanguageAssistant import org.utbot.framework.plugin.api.TestCaseGenerator import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.plugin.api.util.id import org.utbot.sarif.SarifReport import org.utbot.sarif.SourceFindingStrategy -import org.utbot.summary.summarize +import org.utbot.summary.summarizeAll import java.io.File import java.nio.file.Path @@ -58,9 +60,7 @@ class GenerateTestsAndSarifReportFacade( sarifProperties.mockStrategy, sarifProperties.classesToMockAlways, sarifProperties.generationTimeout - ).map { - it.summarize(targetClass.sourceCodeFile, workingDirectory) - } + ).summarizeAll(workingDirectory, targetClass.sourceCodeFile) private fun generateTestCode(targetClass: TargetClassWrapper, testSets: List): String = initializeCodeGenerator(targetClass) @@ -71,13 +71,17 @@ class GenerateTestsAndSarifReportFacade( val isForceStaticMocking = sarifProperties.forceStaticMocking == ForceStaticMocking.FORCE return CodeGenerator( - classUnderTest = targetClass.classUnderTest.id, - testFramework = sarifProperties.testFramework, - mockFramework = sarifProperties.mockFramework, - staticsMocking = sarifProperties.staticsMocking, - forceStaticMocking = sarifProperties.forceStaticMocking, - generateWarningsForStaticMocking = isNoStaticMocking && isForceStaticMocking, - codegenLanguage = sarifProperties.codegenLanguage + CodeGeneratorParams( + classUnderTest = targetClass.classUnderTest.id, + projectType = sarifProperties.projectType, + testFramework = sarifProperties.testFramework, + mockFramework = sarifProperties.mockFramework, + staticsMocking = sarifProperties.staticsMocking, + forceStaticMocking = sarifProperties.forceStaticMocking, + generateWarningsForStaticMocking = isNoStaticMocking && isForceStaticMocking, + codegenLanguage = sarifProperties.codegenLanguage, + cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(sarifProperties.codegenLanguage), + ) ) } @@ -91,7 +95,7 @@ class GenerateTestsAndSarifReportFacade( testClassBody: String, sourceFinding: SourceFindingStrategy ) { - val sarifReport = SarifReport(testSets, testClassBody, sourceFinding).createReport() + val sarifReport = SarifReport(testSets, testClassBody, sourceFinding).createReport().toJson() 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 index a8a633ec0d..f71370b066 100644 --- 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 @@ -1,7 +1,16 @@ package org.utbot.framework.plugin.sarif import org.utbot.engine.Mocker -import org.utbot.framework.codegen.* +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.MockitoStaticMocking +import org.utbot.framework.codegen.domain.NoStaticMocking +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.ProjectType.* +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.TestNg import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MockFramework @@ -44,6 +53,8 @@ interface SarifExtensionProvider { */ val testPrivateMethods: Boolean + val projectType: ProjectType + val testFramework: TestFramework val mockFramework: MockFramework @@ -69,6 +80,15 @@ interface SarifExtensionProvider { // transform functions + fun projectTypeParse(projectType: String): ProjectType = + when (projectType.toLowerCase()) { + "purejvm" -> PureJvm + "spring" -> Spring + "python" -> Python + "javascript" -> JavaScript + else -> error("Parameter projectType == '$projectType', but it can take only 'pureJvm', 'spring', 'python' or 'javascript'") + } + fun testFrameworkParse(testFramework: String): TestFramework = when (testFramework.toLowerCase()) { "junit4" -> Junit4 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 index b8bdf1144f..a5858209f7 100644 --- 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 @@ -56,7 +56,7 @@ object ClassUtil { val clazz = classLoader.tryLoadClass(classFqn) ?: return null val sourceFileName = withUtContext(UtContext(classLoader)) { - Instrumenter.computeSourceFileName(clazz) // finds the file name in bytecode + Instrumenter.adapter.computeSourceFileName(clazz) // finds the file name in bytecode } ?: return null val candidates = sourceCodeFiles.filter { sourceCodeFile -> sourceCodeFile.endsWith(File(sourceFileName)) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineMain.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineMain.kt deleted file mode 100644 index 16c888ddda..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineMain.kt +++ /dev/null @@ -1,283 +0,0 @@ -package org.utbot.framework.process - -import com.jetbrains.rd.framework.IProtocol -import com.jetbrains.rd.util.Logger -import com.jetbrains.rd.util.lifetime.Lifetime -import kotlinx.coroutines.runBlocking -import mu.KotlinLogging -import org.utbot.analytics.AnalyticsConfigureUtil -import org.utbot.common.AbstractSettings -import org.utbot.common.allNestedClasses -import org.utbot.common.appendHtmlLine -import org.utbot.common.nameOfPackage -import org.utbot.engine.util.mockListeners.ForceMockListener -import org.utbot.engine.util.mockListeners.ForceStaticMockListener -import org.utbot.framework.codegen.* -import org.utbot.framework.codegen.model.CodeGenerator -import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport -import org.utbot.framework.plugin.api.* -import org.utbot.framework.plugin.api.Signature -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.framework.plugin.api.util.executableId -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.services.JdkInfo -import org.utbot.framework.process.generated.* -import org.utbot.framework.util.Conflict -import org.utbot.framework.util.ConflictTriggers -import org.utbot.instrumentation.util.KryoHelper -import org.utbot.rd.CallsSynchronizer -import org.utbot.rd.ClientProtocolBuilder -import org.utbot.rd.findRdPort -import org.utbot.rd.loggers.UtRdKLoggerFactory -import org.utbot.sarif.RdSourceFindingStrategyFacade -import org.utbot.sarif.SarifReport -import org.utbot.summary.summarize -import soot.SootMethod -import soot.UnitPatchingChain -import soot.util.HashChain -import java.io.File -import java.net.URLClassLoader -import java.nio.file.Paths -import java.util.* -import kotlin.reflect.full.functions -import kotlin.time.Duration.Companion.seconds - -private val messageFromMainTimeoutMillis = 120.seconds -private val logger = KotlinLogging.logger {} - -// use log4j2.configurationFile property to set log4j configuration -suspend fun main(args: Array) = runBlocking { - // 0 - auto port for server, should not be used here - val port = findRdPort(args) - - Logger.set(Lifetime.Eternal, UtRdKLoggerFactory(logger)) - - ClientProtocolBuilder().withProtocolTimeout(messageFromMainTimeoutMillis).start(port) { - settingsModel - rdSourceFindingStrategy - - AbstractSettings.setupFactory(RdSettingsContainerFactory(protocol)) - val kryoHelper = KryoHelper(lifetime) - engineProcessModel.setup(kryoHelper, it, protocol) - } - logger.info { "runBlocking ending" } -}.also { - logger.info { "runBlocking ended" } -} - -private lateinit var testGenerator: TestCaseGenerator -private val testSets: MutableMap> = mutableMapOf() -private val testGenerationReports: MutableList = mutableListOf() -private var idCounter: Long = 0 - -private fun EngineProcessModel.setup( - kryoHelper: KryoHelper, synchronizer: CallsSynchronizer, realProtocol: IProtocol -) { - val model = this - synchronizer.measureExecutionForTermination(setupUtContext) { params -> - UtContext.setUtContext(UtContext(URLClassLoader(params.classpathForUrlsClassloader.map { - File(it).toURI().toURL() - }.toTypedArray()))) - } - synchronizer.measureExecutionForTermination(createTestGenerator) { params -> - AnalyticsConfigureUtil.configureML() - testGenerator = TestCaseGenerator(buildDirs = params.buildDir.map { Paths.get(it) }, - classpath = params.classpath, - dependencyPaths = params.dependencyPaths, - jdkInfo = JdkInfo(Paths.get(params.jdkInfo.path), params.jdkInfo.version), - isCanceled = { - runBlocking { - model.isCancelled.startSuspending(Unit) - } - }) - } - synchronizer.measureExecutionForTermination(generate) { params -> - val mockFrameworkInstalled = params.mockInstalled - val conflictTriggers = ConflictTriggers(kryoHelper.readObject(params.conflictTriggers)) - if (!mockFrameworkInstalled) { - ForceMockListener.create(testGenerator, conflictTriggers) - } - val staticsMockingConfigured = params.staticsMockingIsConfigureda - if (!staticsMockingConfigured) { - ForceStaticMockListener.create(testGenerator, conflictTriggers) - } - val result = testGenerator.generate(kryoHelper.readObject(params.methods), - MockStrategyApi.valueOf(params.mockStrategy), - kryoHelper.readObject(params.chosenClassesToMockAlways), - params.timeout, - generate = testFlow { - generationTimeout = params.generationTimeout - isSymbolicEngineEnabled = params.isSymbolicEngineEnabled - isFuzzingEnabled = params.isFuzzingEnabled - fuzzingValue = params.fuzzingValue - }) - .map { it.summarize(Paths.get(params.searchDirectory)) } - .filterNot { it.executions.isEmpty() && it.errors.isEmpty() } - - val id = ++idCounter - - testSets[id] = result - GenerateResult(result.size, id) - } - synchronizer.measureExecutionForTermination(render) { params -> - val testFramework = testFrameworkByName(params.testFramework) - val staticMocking = if (params.staticsMocking.startsWith("No")) { - NoStaticMocking - } else { - MockitoStaticMocking - } - val classId: ClassId = kryoHelper.readObject(params.classUnderTest) - val testSetsId: Long = params.testSetsId - val codeGenerator = CodeGenerator( - classUnderTest = classId, - generateUtilClassFile = params.generateUtilClassFile, - paramNames = kryoHelper.readObject(params.paramNames), - testFramework = testFramework, - mockFramework = MockFramework.valueOf(params.mockFramework), - codegenLanguage = CodegenLanguage.valueOf(params.codegenLanguage), - parameterizedTestSource = ParametrizedTestSource.valueOf(params.parameterizedTestSource), - staticsMocking = staticMocking, - forceStaticMocking = kryoHelper.readObject(params.forceStaticMocking), - generateWarningsForStaticMocking = params.generateWarningsForStaticMocking, - runtimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.valueOf(params.runtimeExceptionTestsBehaviour), - hangingTestsTimeout = HangingTestsTimeout(params.hangingTestsTimeout), - enableTestsTimeout = params.enableTestsTimeout, - testClassPackageName = params.testClassPackageName - ) - codeGenerator.generateAsStringWithTestReport(testSets[testSetsId]!!) - .let { - testGenerationReports.add(it.testsGenerationReport) - RenderResult(it.generatedCode, kryoHelper.writeObject(it.utilClassKind)) - } - } - synchronizer.measureExecutionForTermination(stopProcess) { synchronizer.stopProtocol() } - synchronizer.measureExecutionForTermination(obtainClassId) { canonicalName -> - kryoHelper.writeObject(UtContext.currentContext()!!.classLoader.loadClass(canonicalName).id) - } - synchronizer.measureExecutionForTermination(findMethodsInClassMatchingSelected) { params -> - val classId = kryoHelper.readObject(params.classId) - val selectedSignatures = params.signatures.map { Signature(it.name, it.parametersTypes) } - FindMethodsInClassMatchingSelectedResult(kryoHelper.writeObject(classId.jClass.kotlin.allNestedClasses.flatMap { clazz -> - clazz.functions.sortedWith(compareBy { selectedSignatures.indexOf(it.signature()) }) - .filter { it.signature().normalized() in selectedSignatures } - .map { it.executableId } - })) - } - synchronizer.measureExecutionForTermination(findMethodParamNames) { params -> - val classId = kryoHelper.readObject(params.classId) - val bySignature = kryoHelper.readObject>>(params.bySignature) - FindMethodParamNamesResult(kryoHelper.writeObject( - classId.jClass.kotlin.allNestedClasses.flatMap { it.functions } - .mapNotNull { method -> bySignature[method.signature()]?.let { params -> method.executableId to params } } - .toMap() - )) - } - synchronizer.measureExecutionForTermination(writeSarifReport) { params -> - val reportFilePath = Paths.get(params.reportFilePath) - reportFilePath.toFile().writeText( - SarifReport( - testSets[params.testSetsId]!!, - params.generatedTestsCode, - RdSourceFindingStrategyFacade(realProtocol.rdSourceFindingStrategy) - ).createReport() - ) - } - synchronizer.measureExecutionForTermination(generateTestReport) { params -> - val eventLogMessage = params.eventLogMessage - val testPackageName: String? = params.testPackageName - var hasWarnings = false - val reports = testGenerationReports - val isMultiPackage = params.isMultiPackage - val (notifyMessage, statistics) = if (reports.size == 1) { - val report = reports.first() - processInitialWarnings(report, params) - - val message = buildString { - appendHtmlLine(report.toString(isShort = true)) - - val classUnderTestPackageName = - report.classUnderTest.java.nameOfPackage - - destinationWarningMessage(testPackageName, classUnderTestPackageName) - ?.let { - hasWarnings = true - appendHtmlLine(it) - appendHtmlLine() - } - eventLogMessage?.let { - appendHtmlLine(it) - } - } - hasWarnings = hasWarnings || report.hasWarnings - Pair(message, report.detailedStatistics) - } else { - val accumulatedReport = reports.first() - processInitialWarnings(accumulatedReport, params) - - val message = buildString { - appendHtmlLine("${reports.sumBy { it.executables.size }} tests generated for ${reports.size} classes.") - - if (accumulatedReport.initialWarnings.isNotEmpty()) { - accumulatedReport.initialWarnings.forEach { appendHtmlLine(it()) } - appendHtmlLine() - } - - // TODO maybe add statistics info here - - for (report in reports) { - val classUnderTestPackageName = - report.classUnderTest.java.nameOfPackage - - hasWarnings = hasWarnings || report.hasWarnings - if (!isMultiPackage) { - val destinationWarning = - destinationWarningMessage(testPackageName, classUnderTestPackageName) - if (destinationWarning != null) { - hasWarnings = true - appendHtmlLine(destinationWarning) - appendHtmlLine() - } - } - } - eventLogMessage?.let { - appendHtmlLine(it) - } - } - - Pair(message, null) - } - GenerateTestReportResult(notifyMessage, statistics, hasWarnings) - } -} - -private fun processInitialWarnings(report: TestsGenerationReport, params: GenerateTestReportArgs) { - val hasInitialWarnings = params.hasInitialWarnings - - if (!hasInitialWarnings) { - return - } - - report.apply { - params.forceMockWarning?.let { - initialWarnings.add { it } - } - params.forceStaticMockWarnings?.let { - initialWarnings.add { it } - } - params.testFrameworkWarning?.let { - initialWarnings.add { it } - } - } -} - -private fun destinationWarningMessage(testPackageName: String?, classUnderTestPackageName: String): String? { - return if (classUnderTestPackageName != testPackageName) { - """ - Warning: Destination package $testPackageName does not match package of the class $classUnderTestPackageName. - This may cause unnecessary usage of reflection for protected or package-private fields and methods access. - """.trimIndent() - } else { - null - } -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt new file mode 100644 index 0000000000..2be3d2f84d --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt @@ -0,0 +1,323 @@ +package org.utbot.framework.process + +import com.jetbrains.rd.framework.IProtocol +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import org.utbot.analytics.AnalyticsConfigureUtil +import org.utbot.common.* +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.framework.codegen.domain.MockitoStaticMocking +import org.utbot.framework.codegen.domain.NoStaticMocking +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.testFrameworkByName +import org.utbot.framework.codegen.generator.AbstractCodeGenerator +import org.utbot.framework.codegen.generator.CodeGeneratorParams +import org.utbot.framework.codegen.reports.TestsGenerationReport +import org.utbot.framework.codegen.services.language.CgLanguageAssistant +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.MethodDescription +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.method +import org.utbot.framework.plugin.api.utils.ClassNameUtils +import org.utbot.framework.plugin.services.JdkInfo +import org.utbot.framework.process.generated.* +import org.utbot.framework.process.kryo.KryoHelper +import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter +import org.utbot.rd.IdleWatchdog +import org.utbot.rd.ClientProtocolBuilder +import org.utbot.rd.RdSettingsContainerFactory +import org.utbot.rd.generated.settingsModel +import org.utbot.sarif.RdSourceFindingStrategyFacade +import org.utbot.sarif.SarifReport +import org.utbot.summary.summarizeAll +import org.utbot.taint.TaintConfigurationProviderUserRules +import java.io.File +import java.nio.file.Paths +import kotlin.reflect.jvm.kotlinFunction +import kotlin.time.Duration.Companion.seconds + +private val messageFromMainTimeoutMillis = 120.seconds +private val logger = KotlinLogging.logger {} + +@Suppress("unused") +object EngineProcessMain + +// use log4j2.configurationFile property to set log4j configuration +suspend fun main(args: Array) = runBlocking { + logger.info("-----------------------------------------------------------------------") + logger.info("-------------------NEW ENGINE PROCESS STARTED--------------------------") + logger.info("-----------------------------------------------------------------------") + + ClientProtocolBuilder().withProtocolTimeout(messageFromMainTimeoutMillis).start(args) { + AbstractSettings.setupFactory(RdSettingsContainerFactory(protocol.settingsModel)) + val kryoHelper = KryoHelper(lifetime) + engineProcessModel.setup(kryoHelper, it, protocol) + } +} + +private lateinit var testGenerator: TestCaseGenerator +private val testSets: MutableMap> = mutableMapOf() +private val testGenerationReports: MutableList = mutableListOf() +private var idCounter: Long = 0 + +private fun EngineProcessModel.setup(kryoHelper: KryoHelper, watchdog: IdleWatchdog, realProtocol: IProtocol) { + val model = this + watchdog.measureTimeForActiveCall(setupUtContext, "UtContext setup") { params -> + val urls = params.classpathForUrlsClassloader.map { File(it).toURI().toURL() }.toTypedArray() + // - First, we try to load class/resource with platform class loader `ClassLoader.getSystemClassLoader().parent` + // - at this step we load classes like + // - `java.util.ArrayList` (from boostrap classloader) + // - `javax.sql.DataSource` (from platform classloader) + // - Next, we try to load class/resource from user class path + // - at this step we load classes like class under test and other classes from user project and its dependencies + // - Finally, if all else fails we try to load class/resource from UtBot classpath + // - at this step we load classes from UtBot project and its dependencies (e.g. Mockito if user doesn't have it, see #2545) + val classLoader = FallbackClassLoader( + urls = urls, + fallback = ClassLoader.getSystemClassLoader(), + commonParent = ClassLoader.getSystemClassLoader().parent, + ) + UtContext.setUtContext(UtContext(classLoader)) + } + watchdog.measureTimeForActiveCall(createTestGenerator, "Creating Test Generator") { params -> + AnalyticsConfigureUtil.configureML() + Instrumenter.adapter = RdInstrumenter(realProtocol.rdInstrumenterAdapter) + val applicationContext: ApplicationContext = kryoHelper.readObject(params.applicationContext) + + testGenerator = TestCaseGenerator( + buildDirs = params.buildDir.map { Paths.get(it) }, + classpath = params.classpath, + dependencyPaths = params.dependencyPaths, + jdkInfo = JdkInfo(Paths.get(params.jdkInfo.path), params.jdkInfo.version), + applicationContext = applicationContext, + isCanceled = { + runBlocking { + model.isCancelled.startSuspending(Unit) + } + } + ) + } + watchdog.measureTimeForActiveCall(findTestClassName, "Creating test class name") { params -> + val classUnderTest: ClassId = kryoHelper.readObject(params.classUnderTest) + val testClassName = ClassNameUtils.generateTestClassShortName(classUnderTest) + TestClassNameResult(testClassName) + } + watchdog.measureTimeForActiveCall(generate, "Generating tests") { params -> + val methods: List = kryoHelper.readObject(params.methods) + logger.debug() + .measureTime({ "starting generation for ${methods.size} methods, starting with ${methods.first()}" }) { + val generateFlow = testFlow { + generationTimeout = params.generationTimeout + isSymbolicEngineEnabled = params.isSymbolicEngineEnabled + isFuzzingEnabled = params.isFuzzingEnabled + fuzzingValue = params.fuzzingValue + } + + val userTaintConfigurationProvider = params.taintConfigPath?.let { taintConfigPath -> + TaintConfigurationProviderUserRules(taintConfigPath) + } + + val result = testGenerator.generate( + methods, + MockStrategyApi.valueOf(params.mockStrategy), + kryoHelper.readObject(params.chosenClassesToMockAlways), + params.timeout, + userTaintConfigurationProvider, + generate = generateFlow, + ) + .summarizeAll(Paths.get(params.searchDirectory), null) + .filterNot { it.executions.isEmpty() && it.errors.isEmpty() } + + val id = ++idCounter + + testSets[id] = result + GenerateResult(result.size, id) + } + } + watchdog.measureTimeForActiveCall(render, "Rendering tests") { params -> + val codeGenerator = createCodeGenerator(kryoHelper, params, testGenerator.applicationContext) + + codeGenerator.generateAsStringWithTestReport(testSets[params.testSetsId]!!).let { + testGenerationReports.add(it.testsGenerationReport) + RenderResult(it.generatedCode, it.utilClassKind?.javaClass?.simpleName) + } + } + watchdog.measureTimeForActiveCall(obtainClassId, "Obtain class id in UtContext") { binaryName -> + kryoHelper.writeObject(UtContext.currentContext()!!.classLoader.loadClass(binaryName).id) + } + watchdog.measureTimeForActiveCall(findMethodsInClassMatchingSelected, "Find methods in Class") { params -> + val classId = kryoHelper.readObject(params.classId) + val selectedMethodDescriptions = + params.methodDescriptions.map { MethodDescription(it.name, it.containingClass, it.parametersTypes) } + FindMethodsInClassMatchingSelectedResult(kryoHelper.writeObject(classId.jClass.allNestedClasses.flatMap { clazz -> + clazz.id.allMethods.mapNotNull { it.method.kotlinFunction } + .sortedWith(compareBy { selectedMethodDescriptions.indexOf(it.methodDescription()) }) + .filter { it.methodDescription().normalized() in selectedMethodDescriptions }.map { it.executableId } + })) + } + watchdog.measureTimeForActiveCall(findMethodParamNames, "Find method parameters names") { params -> + val classId = kryoHelper.readObject(params.classId) + val bySignatureRaw = kryoHelper.readObject>>>(params.bySignature) + val byMethodDescription = bySignatureRaw.associate { it.first to it.second } + FindMethodParamNamesResult(kryoHelper.writeObject(classId.jClass.allNestedClasses.flatMap { clazz -> clazz.id.allMethods.mapNotNull { it.method.kotlinFunction } } + .mapNotNull { method -> byMethodDescription[method.methodDescription()]?.let { params -> method.executableId to params } } + .toMap())) + } + watchdog.measureTimeForActiveCall(writeSarifReport, "Writing Sarif report") { params -> + val reportFilePath = Paths.get(params.reportFilePath) + reportFilePath.parent.toFile().mkdirs() + val sarifReport = SarifReport( + testSets[params.testSetsId]!!, + params.generatedTestsCode, + RdSourceFindingStrategyFacade(params.testSetsId, realProtocol.rdSourceFindingStrategy) + ).createReport().toJson() + reportFilePath.toFile().writeText(sarifReport) + sarifReport + } + watchdog.measureTimeForActiveCall(generateTestReport, "Generating test report") { params -> + val eventLogMessage = params.eventLogMessage + val testPackageName: String? = params.testPackageName + var hasWarnings = false + val reports = testGenerationReports + if (reports.isEmpty()) return@measureTimeForActiveCall GenerateTestReportResult( + "No tests were generated", + null, + true + ) + val isMultiPackage = params.isMultiPackage + val (notifyMessage, statistics) = if (reports.size == 1) { + val report = reports.first() + processInitialWarnings(report, params) + + val message = buildString { + appendHtmlLine(report.toString(isShort = true)) + + val classUnderTestPackageName = report.classUnderTest.java.nameOfPackage + + destinationWarningMessage(testPackageName, classUnderTestPackageName)?.let { + hasWarnings = true + appendHtmlLine(it) + appendHtmlLine() + } + eventLogMessage?.let { + appendHtmlLine(it) + } + } + hasWarnings = hasWarnings || report.hasWarnings + Pair(message, report.detailedStatistics) + } else { + val accumulatedReport = reports.first() + processInitialWarnings(accumulatedReport, params) + + val message = buildString { + appendHtmlLine("${reports.sumOf { it.countTestMethods() }} tests generated for ${reports.size} classes.") + + if (accumulatedReport.initialWarnings.isNotEmpty()) { + accumulatedReport.initialWarnings.forEach { appendHtmlLine(it()) } + appendHtmlLine() + } + + // TODO maybe add statistics info here + + for (report in reports) { + val classUnderTestPackageName = report.classUnderTest.java.nameOfPackage + + hasWarnings = hasWarnings || report.hasWarnings + if (!isMultiPackage) { + val destinationWarning = destinationWarningMessage(testPackageName, classUnderTestPackageName) + if (destinationWarning != null) { + hasWarnings = true + appendHtmlLine(destinationWarning) + appendHtmlLine() + } + } + } + eventLogMessage?.let { + appendHtmlLine(it) + } + } + + Pair(message, null) + } + GenerateTestReportResult(notifyMessage, statistics, hasWarnings) + } + watchdog.measureTimeForActiveCall(perform, "Performing dynamic task") { params -> + val task = kryoHelper.readObject>(params.engineProcessTask) + val result = task.perform() + kryoHelper.writeObject(result) + } +} + +private fun processInitialWarnings(report: TestsGenerationReport, params: GenerateTestReportArgs) { + val hasInitialWarnings = params.hasInitialWarnings + + if (!hasInitialWarnings) { + return + } + + report.apply { + params.forceMockWarning?.let { + initialWarnings.add { it } + } + params.forceStaticMockWarnings?.let { + initialWarnings.add { it } + } + params.testFrameworkWarning?.let { + initialWarnings.add { it } + } + } +} + +private fun destinationWarningMessage(testPackageName: String?, classUnderTestPackageName: String): String? { + return if (!testPackageName.isNullOrEmpty() && classUnderTestPackageName != testPackageName) { + """ + Warning: Destination package $testPackageName does not match package of the class $classUnderTestPackageName. + This may cause unnecessary usage of reflection for protected or package-private fields and methods access. + """.trimIndent() + } else { + null + } +} + +private fun createCodeGenerator(kryoHelper: KryoHelper, params: RenderParams, applicationContext: ApplicationContext): AbstractCodeGenerator { + with(params) { + val classUnderTest: ClassId = kryoHelper.readObject(classUnderTest) + val paramNames: MutableMap> = kryoHelper.readObject(paramNames) + val testFramework = testFrameworkByName(testFramework) + val staticMocking = if (staticsMocking.startsWith("No")) NoStaticMocking else MockitoStaticMocking + val forceStaticMocking: ForceStaticMocking = kryoHelper.readObject(forceStaticMocking) + val projectType = ProjectType.valueOf(projectType) + + return applicationContext.createCodeGenerator(CodeGeneratorParams( + classUnderTest = classUnderTest, + projectType = projectType, + generateUtilClassFile = generateUtilClassFile, + paramNames = paramNames, + testFramework = testFramework, + mockFramework = MockFramework.valueOf(mockFramework), + codegenLanguage = CodegenLanguage.valueOf(codegenLanguage), + cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage( + CodegenLanguage.valueOf( + codegenLanguage + ) + ), + parameterizedTestSource = ParametrizedTestSource.valueOf(parameterizedTestSource), + staticsMocking = staticMocking, + forceStaticMocking = forceStaticMocking, + generateWarningsForStaticMocking = generateWarningsForStaticMocking, + runtimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.valueOf( + runtimeExceptionTestsBehaviour + ), + hangingTestsTimeout = HangingTestsTimeout(hangingTestsTimeout), + enableTestsTimeout = enableTestsTimeout, + testClassPackageName = testClassPackageName, + )) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessTask.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessTask.kt new file mode 100644 index 0000000000..d42c3f65b0 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessTask.kt @@ -0,0 +1,14 @@ +package org.utbot.framework.process + +/** + * Implementations of this interface can be passed to engine process for execution and should + * be used for adding feature-specific (e.g. Spring-specific) tasks without inflating core UtBot codebase. + * + * Such tasks are serialised with kryo when passed between processes, meaning that for successful execution same + * implementation of [EngineProcessTask] should be present on the classpath of both parent process and engine process. + * + * @param R result type of the task (should be present on the classpath of both processes). + */ +interface EngineProcessTask { + fun perform(): R +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/RdInstrumenter.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/RdInstrumenter.kt new file mode 100644 index 0000000000..e7d033181b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/process/RdInstrumenter.kt @@ -0,0 +1,30 @@ +package org.utbot.framework.process + +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import org.utbot.common.logException +import org.utbot.framework.process.generated.ComputeSourceFileByClassArguments +import org.utbot.framework.process.generated.RdInstrumenterAdapter +import org.utbot.instrumentation.instrumentation.instrumenter.InstrumenterAdapter +import org.utbot.rd.startBlocking +import java.io.File +import java.nio.file.Path + +private val logger = KotlinLogging.logger { } + +class RdInstrumenter(private val rdInstrumenterAdapter: RdInstrumenterAdapter) : InstrumenterAdapter() { + override fun computeSourceFileByClass( + clazz: Class<*>, + directoryToSearchRecursively: Path + ): File? { + val canonicalClassName = clazz.canonicalName + logger.debug { "starting computeSourceFileByClass for class - $canonicalClassName" } + val result = logger.logException { + val arguments = ComputeSourceFileByClassArguments(canonicalClassName) + + rdInstrumenterAdapter.computeSourceFileByClass.startBlocking(arguments) + } + logger.debug { "computeSourceFileByClass result for $canonicalClassName from idea: $result" } + return result?.let { File(it) } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/RdSettingsContainer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/RdSettingsContainer.kt deleted file mode 100644 index 4566dba88c..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/process/RdSettingsContainer.kt +++ /dev/null @@ -1,44 +0,0 @@ -package org.utbot.framework.process - -import com.jetbrains.rd.framework.IProtocol -import kotlinx.coroutines.runBlocking -import mu.KLogger -import org.utbot.common.SettingsContainer -import org.utbot.common.SettingsContainerFactory -import org.utbot.framework.process.generated.SettingForArgument -import org.utbot.framework.process.generated.settingsModel -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -class RdSettingsContainerFactory(private val protocol: IProtocol) : SettingsContainerFactory { - override fun createSettingsContainer( - logger: KLogger, - defaultKeyForSettingsPath: String, - defaultSettingsPath: String? - ): SettingsContainer { - return RdSettingsContainer(logger, defaultKeyForSettingsPath, protocol) - } -} - -class RdSettingsContainer(val logger: KLogger, val key: String, val protocol: IProtocol): SettingsContainer { - - override fun settingFor( - defaultValue: T, - converter: (String) -> T - ): PropertyDelegateProvider> { - return PropertyDelegateProvider { _, prop -> - object: ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking { - return@runBlocking protocol.settingsModel.settingFor.startSuspending(SettingForArgument(key, property.name)).value?.let { - converter(it) - } ?: defaultValue - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - throw NotImplementedError("Setting properties from child process not supported") - } - } - } - } -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessModel.Generated.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessModel.Generated.kt index 72ba022123..c399ead4df 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessModel.Generated.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessModel.Generated.kt @@ -9,26 +9,28 @@ import com.jetbrains.rd.util.lifetime.* import com.jetbrains.rd.util.reactive.* import com.jetbrains.rd.util.string.* import com.jetbrains.rd.util.* +import kotlin.time.Duration import kotlin.reflect.KClass import kotlin.jvm.JvmStatic /** - * #### Generated from [EngineProcessModel.kt:20] + * #### Generated from [EngineProcessModel.kt:31] */ class EngineProcessModel private constructor( private val _setupUtContext: RdCall, private val _createTestGenerator: RdCall, private val _isCancelled: RdCall, + private val _findTestClassName: RdCall, private val _generate: RdCall, private val _render: RdCall, - private val _stopProcess: RdCall, private val _obtainClassId: RdCall, private val _findMethodsInClassMatchingSelected: RdCall, private val _findMethodParamNames: RdCall, - private val _writeSarifReport: RdCall, - private val _generateTestReport: RdCall + private val _writeSarifReport: RdCall, + private val _generateTestReport: RdCall, + private val _perform: RdCall ) : RdExtBase() { //companion @@ -37,12 +39,14 @@ class EngineProcessModel private constructor( override fun registerSerializersCore(serializers: ISerializers) { serializers.register(JdkInfo) serializers.register(TestGeneratorParams) + serializers.register(TestClassNameParams) + serializers.register(TestClassNameResult) serializers.register(GenerateParams) serializers.register(GenerateResult) serializers.register(RenderParams) serializers.register(RenderResult) serializers.register(SetupContextParams) - serializers.register(Signature) + serializers.register(MethodDescription) serializers.register(FindMethodsInClassMatchingSelectedArguments) serializers.register(FindMethodsInClassMatchingSelectedResult) serializers.register(FindMethodParamNamesArguments) @@ -50,6 +54,7 @@ class EngineProcessModel private constructor( serializers.register(WriteSarifReportArguments) serializers.register(GenerateTestReportArgs) serializers.register(GenerateTestReportResult) + serializers.register(PerformParams) } @@ -64,16 +69,13 @@ class EngineProcessModel private constructor( @JvmStatic @Deprecated("Use protocol.engineProcessModel or revise the extension scope instead", ReplaceWith("protocol.engineProcessModel")) fun create(lifetime: Lifetime, protocol: IProtocol): EngineProcessModel { - EngineProcessProtocolRoot.register(protocol.serializers) + EngineProcessRoot.register(protocol.serializers) - return EngineProcessModel().apply { - identify(protocol.identity, RdId.Null.mix("EngineProcessModel")) - bind(lifetime, protocol, "EngineProcessModel") - } + return EngineProcessModel() } - const val serializationHash = 4674749231408610997L + const val serializationHash = 7072495177628793247L } override val serializersOwner: ISerializersOwner get() = EngineProcessModel @@ -83,42 +85,45 @@ class EngineProcessModel private constructor( val setupUtContext: RdCall get() = _setupUtContext val createTestGenerator: RdCall get() = _createTestGenerator val isCancelled: RdCall get() = _isCancelled + val findTestClassName: RdCall get() = _findTestClassName val generate: RdCall get() = _generate val render: RdCall get() = _render - val stopProcess: RdCall get() = _stopProcess val obtainClassId: RdCall get() = _obtainClassId val findMethodsInClassMatchingSelected: RdCall get() = _findMethodsInClassMatchingSelected val findMethodParamNames: RdCall get() = _findMethodParamNames - val writeSarifReport: RdCall get() = _writeSarifReport + val writeSarifReport: RdCall get() = _writeSarifReport val generateTestReport: RdCall get() = _generateTestReport + val perform: RdCall get() = _perform //methods //initializer init { _setupUtContext.async = true _createTestGenerator.async = true _isCancelled.async = true + _findTestClassName.async = true _generate.async = true _render.async = true - _stopProcess.async = true _obtainClassId.async = true _findMethodsInClassMatchingSelected.async = true _findMethodParamNames.async = true _writeSarifReport.async = true _generateTestReport.async = true + _perform.async = true } init { bindableChildren.add("setupUtContext" to _setupUtContext) bindableChildren.add("createTestGenerator" to _createTestGenerator) bindableChildren.add("isCancelled" to _isCancelled) + bindableChildren.add("findTestClassName" to _findTestClassName) bindableChildren.add("generate" to _generate) bindableChildren.add("render" to _render) - bindableChildren.add("stopProcess" to _stopProcess) bindableChildren.add("obtainClassId" to _obtainClassId) bindableChildren.add("findMethodsInClassMatchingSelected" to _findMethodsInClassMatchingSelected) bindableChildren.add("findMethodParamNames" to _findMethodParamNames) bindableChildren.add("writeSarifReport" to _writeSarifReport) bindableChildren.add("generateTestReport" to _generateTestReport) + bindableChildren.add("perform" to _perform) } //secondary constructor @@ -127,14 +132,15 @@ class EngineProcessModel private constructor( RdCall(SetupContextParams, FrameworkMarshallers.Void), RdCall(TestGeneratorParams, FrameworkMarshallers.Void), RdCall(FrameworkMarshallers.Void, FrameworkMarshallers.Bool), + RdCall(TestClassNameParams, TestClassNameResult), RdCall(GenerateParams, GenerateResult), RdCall(RenderParams, RenderResult), - RdCall(FrameworkMarshallers.Void, FrameworkMarshallers.Void), RdCall(FrameworkMarshallers.String, FrameworkMarshallers.ByteArray), RdCall(FindMethodsInClassMatchingSelectedArguments, FindMethodsInClassMatchingSelectedResult), RdCall(FindMethodParamNamesArguments, FindMethodParamNamesResult), - RdCall(WriteSarifReportArguments, FrameworkMarshallers.Void), - RdCall(GenerateTestReportArgs, GenerateTestReportResult) + RdCall(WriteSarifReportArguments, FrameworkMarshallers.String), + RdCall(GenerateTestReportArgs, GenerateTestReportResult), + RdCall(PerformParams, FrameworkMarshallers.ByteArray) ) //equals trait @@ -146,14 +152,15 @@ class EngineProcessModel private constructor( print("setupUtContext = "); _setupUtContext.print(printer); println() print("createTestGenerator = "); _createTestGenerator.print(printer); println() print("isCancelled = "); _isCancelled.print(printer); println() + print("findTestClassName = "); _findTestClassName.print(printer); println() print("generate = "); _generate.print(printer); println() print("render = "); _render.print(printer); println() - print("stopProcess = "); _stopProcess.print(printer); println() print("obtainClassId = "); _obtainClassId.print(printer); println() print("findMethodsInClassMatchingSelected = "); _findMethodsInClassMatchingSelected.print(printer); println() print("findMethodParamNames = "); _findMethodParamNames.print(printer); println() print("writeSarifReport = "); _writeSarifReport.print(printer); println() print("generateTestReport = "); _generateTestReport.print(printer); println() + print("perform = "); _perform.print(printer); println() } printer.print(")") } @@ -163,14 +170,15 @@ class EngineProcessModel private constructor( _setupUtContext.deepClonePolymorphic(), _createTestGenerator.deepClonePolymorphic(), _isCancelled.deepClonePolymorphic(), + _findTestClassName.deepClonePolymorphic(), _generate.deepClonePolymorphic(), _render.deepClonePolymorphic(), - _stopProcess.deepClonePolymorphic(), _obtainClassId.deepClonePolymorphic(), _findMethodsInClassMatchingSelected.deepClonePolymorphic(), _findMethodParamNames.deepClonePolymorphic(), _writeSarifReport.deepClonePolymorphic(), - _generateTestReport.deepClonePolymorphic() + _generateTestReport.deepClonePolymorphic(), + _perform.deepClonePolymorphic() ) } //contexts @@ -180,7 +188,7 @@ val IProtocol.engineProcessModel get() = getOrCreateExtension(EngineProcessModel /** - * #### Generated from [EngineProcessModel.kt:89] + * #### Generated from [EngineProcessModel.kt:106] */ data class FindMethodParamNamesArguments ( val classId: ByteArray, @@ -243,7 +251,7 @@ data class FindMethodParamNamesArguments ( /** - * #### Generated from [EngineProcessModel.kt:93] + * #### Generated from [EngineProcessModel.kt:110] */ data class FindMethodParamNamesResult ( val paramNames: ByteArray @@ -300,11 +308,11 @@ data class FindMethodParamNamesResult ( /** - * #### Generated from [EngineProcessModel.kt:82] + * #### Generated from [EngineProcessModel.kt:99] */ data class FindMethodsInClassMatchingSelectedArguments ( val classId: ByteArray, - val signatures: List + val methodDescriptions: List ) : IPrintable { //companion @@ -314,13 +322,13 @@ data class FindMethodsInClassMatchingSelectedArguments ( @Suppress("UNCHECKED_CAST") override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): FindMethodsInClassMatchingSelectedArguments { val classId = buffer.readByteArray() - val signatures = buffer.readList { Signature.read(ctx, buffer) } - return FindMethodsInClassMatchingSelectedArguments(classId, signatures) + val methodDescriptions = buffer.readList { MethodDescription.read(ctx, buffer) } + return FindMethodsInClassMatchingSelectedArguments(classId, methodDescriptions) } override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: FindMethodsInClassMatchingSelectedArguments) { buffer.writeByteArray(value.classId) - buffer.writeList(value.signatures) { v -> Signature.write(ctx, buffer, v) } + buffer.writeList(value.methodDescriptions) { v -> MethodDescription.write(ctx, buffer, v) } } @@ -337,7 +345,7 @@ data class FindMethodsInClassMatchingSelectedArguments ( other as FindMethodsInClassMatchingSelectedArguments if (!(classId contentEquals other.classId)) return false - if (signatures != other.signatures) return false + if (methodDescriptions != other.methodDescriptions) return false return true } @@ -345,7 +353,7 @@ data class FindMethodsInClassMatchingSelectedArguments ( override fun hashCode(): Int { var __r = 0 __r = __r*31 + classId.contentHashCode() - __r = __r*31 + signatures.hashCode() + __r = __r*31 + methodDescriptions.hashCode() return __r } //pretty print @@ -353,7 +361,7 @@ data class FindMethodsInClassMatchingSelectedArguments ( printer.println("FindMethodsInClassMatchingSelectedArguments (") printer.indent { print("classId = "); classId.print(printer); println() - print("signatures = "); signatures.print(printer); println() + print("methodDescriptions = "); methodDescriptions.print(printer); println() } printer.print(")") } @@ -363,7 +371,7 @@ data class FindMethodsInClassMatchingSelectedArguments ( /** - * #### Generated from [EngineProcessModel.kt:86] + * #### Generated from [EngineProcessModel.kt:103] */ data class FindMethodsInClassMatchingSelectedResult ( val executableIds: ByteArray @@ -420,12 +428,9 @@ data class FindMethodsInClassMatchingSelectedResult ( /** - * #### Generated from [EngineProcessModel.kt:32] + * #### Generated from [EngineProcessModel.kt:49] */ data class GenerateParams ( - val mockInstalled: Boolean, - val staticsMockingIsConfigureda: Boolean, - val conflictTriggers: ByteArray, val methods: ByteArray, val mockStrategy: String, val chosenClassesToMockAlways: ByteArray, @@ -434,7 +439,8 @@ data class GenerateParams ( val isSymbolicEngineEnabled: Boolean, val isFuzzingEnabled: Boolean, val fuzzingValue: Double, - val searchDirectory: String + val searchDirectory: String, + val taintConfigPath: String? ) : IPrintable { //companion @@ -443,9 +449,6 @@ data class GenerateParams ( @Suppress("UNCHECKED_CAST") override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): GenerateParams { - val mockInstalled = buffer.readBool() - val staticsMockingIsConfigureda = buffer.readBool() - val conflictTriggers = buffer.readByteArray() val methods = buffer.readByteArray() val mockStrategy = buffer.readString() val chosenClassesToMockAlways = buffer.readByteArray() @@ -455,13 +458,11 @@ data class GenerateParams ( val isFuzzingEnabled = buffer.readBool() val fuzzingValue = buffer.readDouble() val searchDirectory = buffer.readString() - return GenerateParams(mockInstalled, staticsMockingIsConfigureda, conflictTriggers, methods, mockStrategy, chosenClassesToMockAlways, timeout, generationTimeout, isSymbolicEngineEnabled, isFuzzingEnabled, fuzzingValue, searchDirectory) + val taintConfigPath = buffer.readNullable { buffer.readString() } + return GenerateParams(methods, mockStrategy, chosenClassesToMockAlways, timeout, generationTimeout, isSymbolicEngineEnabled, isFuzzingEnabled, fuzzingValue, searchDirectory, taintConfigPath) } override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: GenerateParams) { - buffer.writeBool(value.mockInstalled) - buffer.writeBool(value.staticsMockingIsConfigureda) - buffer.writeByteArray(value.conflictTriggers) buffer.writeByteArray(value.methods) buffer.writeString(value.mockStrategy) buffer.writeByteArray(value.chosenClassesToMockAlways) @@ -471,6 +472,7 @@ data class GenerateParams ( buffer.writeBool(value.isFuzzingEnabled) buffer.writeDouble(value.fuzzingValue) buffer.writeString(value.searchDirectory) + buffer.writeNullable(value.taintConfigPath) { buffer.writeString(it) } } @@ -486,9 +488,6 @@ data class GenerateParams ( other as GenerateParams - if (mockInstalled != other.mockInstalled) return false - if (staticsMockingIsConfigureda != other.staticsMockingIsConfigureda) return false - if (!(conflictTriggers contentEquals other.conflictTriggers)) return false if (!(methods contentEquals other.methods)) return false if (mockStrategy != other.mockStrategy) return false if (!(chosenClassesToMockAlways contentEquals other.chosenClassesToMockAlways)) return false @@ -498,15 +497,13 @@ data class GenerateParams ( if (isFuzzingEnabled != other.isFuzzingEnabled) return false if (fuzzingValue != other.fuzzingValue) return false if (searchDirectory != other.searchDirectory) return false + if (taintConfigPath != other.taintConfigPath) return false return true } //hash code trait override fun hashCode(): Int { var __r = 0 - __r = __r*31 + mockInstalled.hashCode() - __r = __r*31 + staticsMockingIsConfigureda.hashCode() - __r = __r*31 + conflictTriggers.contentHashCode() __r = __r*31 + methods.contentHashCode() __r = __r*31 + mockStrategy.hashCode() __r = __r*31 + chosenClassesToMockAlways.contentHashCode() @@ -516,15 +513,13 @@ data class GenerateParams ( __r = __r*31 + isFuzzingEnabled.hashCode() __r = __r*31 + fuzzingValue.hashCode() __r = __r*31 + searchDirectory.hashCode() + __r = __r*31 + if (taintConfigPath != null) taintConfigPath.hashCode() else 0 return __r } //pretty print override fun print(printer: PrettyPrinter) { printer.println("GenerateParams (") printer.indent { - print("mockInstalled = "); mockInstalled.print(printer); println() - print("staticsMockingIsConfigureda = "); staticsMockingIsConfigureda.print(printer); println() - print("conflictTriggers = "); conflictTriggers.print(printer); println() print("methods = "); methods.print(printer); println() print("mockStrategy = "); mockStrategy.print(printer); println() print("chosenClassesToMockAlways = "); chosenClassesToMockAlways.print(printer); println() @@ -534,6 +529,7 @@ data class GenerateParams ( print("isFuzzingEnabled = "); isFuzzingEnabled.print(printer); println() print("fuzzingValue = "); fuzzingValue.print(printer); println() print("searchDirectory = "); searchDirectory.print(printer); println() + print("taintConfigPath = "); taintConfigPath.print(printer); println() } printer.print(")") } @@ -543,7 +539,7 @@ data class GenerateParams ( /** - * #### Generated from [EngineProcessModel.kt:50] + * #### Generated from [EngineProcessModel.kt:65] */ data class GenerateResult ( val notEmptyCases: Int, @@ -606,7 +602,7 @@ data class GenerateResult ( /** - * #### Generated from [EngineProcessModel.kt:101] + * #### Generated from [EngineProcessModel.kt:118] */ data class GenerateTestReportArgs ( val eventLogMessage: String?, @@ -699,7 +695,7 @@ data class GenerateTestReportArgs ( /** - * #### Generated from [EngineProcessModel.kt:110] + * #### Generated from [EngineProcessModel.kt:127] */ data class GenerateTestReportResult ( val notifyMessage: String, @@ -768,7 +764,7 @@ data class GenerateTestReportResult ( /** - * #### Generated from [EngineProcessModel.kt:21] + * #### Generated from [EngineProcessModel.kt:32] */ data class JdkInfo ( val path: String, @@ -831,11 +827,138 @@ data class JdkInfo ( /** - * #### Generated from [EngineProcessModel.kt:54] + * #### Generated from [EngineProcessModel.kt:94] + */ +data class MethodDescription ( + val name: String, + val containingClass: String?, + val parametersTypes: List +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = MethodDescription::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): MethodDescription { + val name = buffer.readString() + val containingClass = buffer.readNullable { buffer.readString() } + val parametersTypes = buffer.readList { buffer.readNullable { buffer.readString() } } + return MethodDescription(name, containingClass, parametersTypes) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: MethodDescription) { + buffer.writeString(value.name) + buffer.writeNullable(value.containingClass) { buffer.writeString(it) } + buffer.writeList(value.parametersTypes) { v -> buffer.writeNullable(v) { buffer.writeString(it) } } + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as MethodDescription + + if (name != other.name) return false + if (containingClass != other.containingClass) return false + if (parametersTypes != other.parametersTypes) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + name.hashCode() + __r = __r*31 + if (containingClass != null) containingClass.hashCode() else 0 + __r = __r*31 + parametersTypes.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("MethodDescription (") + printer.indent { + print("name = "); name.print(printer); println() + print("containingClass = "); containingClass.print(printer); println() + print("parametersTypes = "); parametersTypes.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [EngineProcessModel.kt:132] + */ +data class PerformParams ( + val engineProcessTask: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = PerformParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): PerformParams { + val engineProcessTask = buffer.readByteArray() + return PerformParams(engineProcessTask) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: PerformParams) { + buffer.writeByteArray(value.engineProcessTask) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as PerformParams + + if (!(engineProcessTask contentEquals other.engineProcessTask)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + engineProcessTask.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("PerformParams (") + printer.indent { + print("engineProcessTask = "); engineProcessTask.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [EngineProcessModel.kt:69] */ data class RenderParams ( val testSetsId: Long, val classUnderTest: ByteArray, + val projectType: String, val paramNames: ByteArray, val generateUtilClassFile: Boolean, val testFramework: String, @@ -859,6 +982,7 @@ data class RenderParams ( override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): RenderParams { val testSetsId = buffer.readLong() val classUnderTest = buffer.readByteArray() + val projectType = buffer.readString() val paramNames = buffer.readByteArray() val generateUtilClassFile = buffer.readBool() val testFramework = buffer.readString() @@ -872,12 +996,13 @@ data class RenderParams ( val hangingTestsTimeout = buffer.readLong() val enableTestsTimeout = buffer.readBool() val testClassPackageName = buffer.readString() - return RenderParams(testSetsId, classUnderTest, paramNames, generateUtilClassFile, testFramework, mockFramework, codegenLanguage, parameterizedTestSource, staticsMocking, forceStaticMocking, generateWarningsForStaticMocking, runtimeExceptionTestsBehaviour, hangingTestsTimeout, enableTestsTimeout, testClassPackageName) + return RenderParams(testSetsId, classUnderTest, projectType, paramNames, generateUtilClassFile, testFramework, mockFramework, codegenLanguage, parameterizedTestSource, staticsMocking, forceStaticMocking, generateWarningsForStaticMocking, runtimeExceptionTestsBehaviour, hangingTestsTimeout, enableTestsTimeout, testClassPackageName) } override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: RenderParams) { buffer.writeLong(value.testSetsId) buffer.writeByteArray(value.classUnderTest) + buffer.writeString(value.projectType) buffer.writeByteArray(value.paramNames) buffer.writeBool(value.generateUtilClassFile) buffer.writeString(value.testFramework) @@ -908,6 +1033,7 @@ data class RenderParams ( if (testSetsId != other.testSetsId) return false if (!(classUnderTest contentEquals other.classUnderTest)) return false + if (projectType != other.projectType) return false if (!(paramNames contentEquals other.paramNames)) return false if (generateUtilClassFile != other.generateUtilClassFile) return false if (testFramework != other.testFramework) return false @@ -929,6 +1055,7 @@ data class RenderParams ( var __r = 0 __r = __r*31 + testSetsId.hashCode() __r = __r*31 + classUnderTest.contentHashCode() + __r = __r*31 + projectType.hashCode() __r = __r*31 + paramNames.contentHashCode() __r = __r*31 + generateUtilClassFile.hashCode() __r = __r*31 + testFramework.hashCode() @@ -950,6 +1077,7 @@ data class RenderParams ( printer.indent { print("testSetsId = "); testSetsId.print(printer); println() print("classUnderTest = "); classUnderTest.print(printer); println() + print("projectType = "); projectType.print(printer); println() print("paramNames = "); paramNames.print(printer); println() print("generateUtilClassFile = "); generateUtilClassFile.print(printer); println() print("testFramework = "); testFramework.print(printer); println() @@ -972,11 +1100,11 @@ data class RenderParams ( /** - * #### Generated from [EngineProcessModel.kt:71] + * #### Generated from [EngineProcessModel.kt:87] */ data class RenderResult ( val generatedCode: String, - val utilClassKind: ByteArray + val utilClassKind: String? ) : IPrintable { //companion @@ -986,13 +1114,13 @@ data class RenderResult ( @Suppress("UNCHECKED_CAST") override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): RenderResult { val generatedCode = buffer.readString() - val utilClassKind = buffer.readByteArray() + val utilClassKind = buffer.readNullable { buffer.readString() } return RenderResult(generatedCode, utilClassKind) } override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: RenderResult) { buffer.writeString(value.generatedCode) - buffer.writeByteArray(value.utilClassKind) + buffer.writeNullable(value.utilClassKind) { buffer.writeString(it) } } @@ -1009,7 +1137,7 @@ data class RenderResult ( other as RenderResult if (generatedCode != other.generatedCode) return false - if (!(utilClassKind contentEquals other.utilClassKind)) return false + if (utilClassKind != other.utilClassKind) return false return true } @@ -1017,7 +1145,7 @@ data class RenderResult ( override fun hashCode(): Int { var __r = 0 __r = __r*31 + generatedCode.hashCode() - __r = __r*31 + utilClassKind.contentHashCode() + __r = __r*31 + if (utilClassKind != null) utilClassKind.hashCode() else 0 return __r } //pretty print @@ -1035,7 +1163,7 @@ data class RenderResult ( /** - * #### Generated from [EngineProcessModel.kt:75] + * #### Generated from [EngineProcessModel.kt:91] */ data class SetupContextParams ( val classpathForUrlsClassloader: List @@ -1092,27 +1220,24 @@ data class SetupContextParams ( /** - * #### Generated from [EngineProcessModel.kt:78] + * #### Generated from [EngineProcessModel.kt:43] */ -data class Signature ( - val name: String, - val parametersTypes: List +data class TestClassNameParams ( + val classUnderTest: ByteArray ) : IPrintable { //companion - companion object : IMarshaller { - override val _type: KClass = Signature::class + companion object : IMarshaller { + override val _type: KClass = TestClassNameParams::class @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): Signature { - val name = buffer.readString() - val parametersTypes = buffer.readList { buffer.readNullable { buffer.readString() } } - return Signature(name, parametersTypes) + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): TestClassNameParams { + val classUnderTest = buffer.readByteArray() + return TestClassNameParams(classUnderTest) } - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: Signature) { - buffer.writeString(value.name) - buffer.writeList(value.parametersTypes) { v -> buffer.writeNullable(v) { buffer.writeString(it) } } + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: TestClassNameParams) { + buffer.writeByteArray(value.classUnderTest) } @@ -1126,26 +1251,80 @@ data class Signature ( if (this === other) return true if (other == null || other::class != this::class) return false - other as Signature + other as TestClassNameParams - if (name != other.name) return false - if (parametersTypes != other.parametersTypes) return false + if (!(classUnderTest contentEquals other.classUnderTest)) return false return true } //hash code trait override fun hashCode(): Int { var __r = 0 - __r = __r*31 + name.hashCode() - __r = __r*31 + parametersTypes.hashCode() + __r = __r*31 + classUnderTest.contentHashCode() return __r } //pretty print override fun print(printer: PrettyPrinter) { - printer.println("Signature (") + printer.println("TestClassNameParams (") printer.indent { - print("name = "); name.print(printer); println() - print("parametersTypes = "); parametersTypes.print(printer); println() + print("classUnderTest = "); classUnderTest.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [EngineProcessModel.kt:46] + */ +data class TestClassNameResult ( + val testClassName: String +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = TestClassNameResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): TestClassNameResult { + val testClassName = buffer.readString() + return TestClassNameResult(testClassName) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: TestClassNameResult) { + buffer.writeString(value.testClassName) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as TestClassNameResult + + if (testClassName != other.testClassName) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + testClassName.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("TestClassNameResult (") + printer.indent { + print("testClassName = "); testClassName.print(printer); println() } printer.print(")") } @@ -1155,13 +1334,14 @@ data class Signature ( /** - * #### Generated from [EngineProcessModel.kt:26] + * #### Generated from [EngineProcessModel.kt:36] */ data class TestGeneratorParams ( val buildDir: Array, val classpath: String?, val dependencyPaths: String, - val jdkInfo: JdkInfo + val jdkInfo: JdkInfo, + val applicationContext: ByteArray ) : IPrintable { //companion @@ -1174,7 +1354,8 @@ data class TestGeneratorParams ( val classpath = buffer.readNullable { buffer.readString() } val dependencyPaths = buffer.readString() val jdkInfo = JdkInfo.read(ctx, buffer) - return TestGeneratorParams(buildDir, classpath, dependencyPaths, jdkInfo) + val applicationContext = buffer.readByteArray() + return TestGeneratorParams(buildDir, classpath, dependencyPaths, jdkInfo, applicationContext) } override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: TestGeneratorParams) { @@ -1182,6 +1363,7 @@ data class TestGeneratorParams ( buffer.writeNullable(value.classpath) { buffer.writeString(it) } buffer.writeString(value.dependencyPaths) JdkInfo.write(ctx, buffer, value.jdkInfo) + buffer.writeByteArray(value.applicationContext) } @@ -1201,6 +1383,7 @@ data class TestGeneratorParams ( if (classpath != other.classpath) return false if (dependencyPaths != other.dependencyPaths) return false if (jdkInfo != other.jdkInfo) return false + if (!(applicationContext contentEquals other.applicationContext)) return false return true } @@ -1211,6 +1394,7 @@ data class TestGeneratorParams ( __r = __r*31 + if (classpath != null) classpath.hashCode() else 0 __r = __r*31 + dependencyPaths.hashCode() __r = __r*31 + jdkInfo.hashCode() + __r = __r*31 + applicationContext.contentHashCode() return __r } //pretty print @@ -1221,6 +1405,7 @@ data class TestGeneratorParams ( print("classpath = "); classpath.print(printer); println() print("dependencyPaths = "); dependencyPaths.print(printer); println() print("jdkInfo = "); jdkInfo.print(printer); println() + print("applicationContext = "); applicationContext.print(printer); println() } printer.print(")") } @@ -1230,7 +1415,7 @@ data class TestGeneratorParams ( /** - * #### Generated from [EngineProcessModel.kt:96] + * #### Generated from [EngineProcessModel.kt:113] */ data class WriteSarifReportArguments ( val testSetsId: Long, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessProtocolRoot.Generated.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessProtocolRoot.Generated.kt deleted file mode 100644 index dc9c7ce86b..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessProtocolRoot.Generated.kt +++ /dev/null @@ -1,59 +0,0 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") -package org.utbot.framework.process.generated - -import com.jetbrains.rd.framework.* -import com.jetbrains.rd.framework.base.* -import com.jetbrains.rd.framework.impl.* - -import com.jetbrains.rd.util.lifetime.* -import com.jetbrains.rd.util.reactive.* -import com.jetbrains.rd.util.string.* -import com.jetbrains.rd.util.* -import kotlin.reflect.KClass -import kotlin.jvm.JvmStatic - - - -/** - * #### Generated from [EngineProcessModel.kt:5] - */ -class EngineProcessProtocolRoot private constructor( -) : RdExtBase() { - //companion - - companion object : ISerializersOwner { - - override fun registerSerializersCore(serializers: ISerializers) { - EngineProcessProtocolRoot.register(serializers) - EngineProcessModel.register(serializers) - RdSourceFindingStrategy.register(serializers) - } - - - - - - const val serializationHash = -4532543668004925627L - - } - override val serializersOwner: ISerializersOwner get() = EngineProcessProtocolRoot - override val serializationHash: Long get() = EngineProcessProtocolRoot.serializationHash - - //fields - //methods - //initializer - //secondary constructor - //equals trait - //hash code trait - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("EngineProcessProtocolRoot (") - printer.print(")") - } - //deepClone - override fun deepClone(): EngineProcessProtocolRoot { - return EngineProcessProtocolRoot( - ) - } - //contexts -} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessRoot.Generated.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessRoot.Generated.kt new file mode 100644 index 0000000000..b49abf70e0 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/EngineProcessRoot.Generated.kt @@ -0,0 +1,61 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.framework.process.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [EngineProcessModel.kt:6] + */ +class EngineProcessRoot private constructor( +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + EngineProcessRoot.register(serializers) + EngineProcessModel.register(serializers) + RdInstrumenterAdapter.register(serializers) + RdSourceFindingStrategy.register(serializers) + } + + + + + + const val serializationHash = 2863869932420445069L + + } + override val serializersOwner: ISerializersOwner get() = EngineProcessRoot + override val serializationHash: Long get() = EngineProcessRoot.serializationHash + + //fields + //methods + //initializer + //secondary constructor + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("EngineProcessRoot (") + printer.print(")") + } + //deepClone + override fun deepClone(): EngineProcessRoot { + return EngineProcessRoot( + ) + } + //contexts +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/RdInstrumenterAdapter.Generated.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/RdInstrumenterAdapter.Generated.kt new file mode 100644 index 0000000000..c7ce7a8188 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/RdInstrumenterAdapter.Generated.kt @@ -0,0 +1,151 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.framework.process.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [EngineProcessModel.kt:8] + */ +class RdInstrumenterAdapter private constructor( + private val _computeSourceFileByClass: RdCall +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + serializers.register(ComputeSourceFileByClassArguments) + } + + + @JvmStatic + @JvmName("internalCreateModel") + @Deprecated("Use create instead", ReplaceWith("create(lifetime, protocol)")) + internal fun createModel(lifetime: Lifetime, protocol: IProtocol): RdInstrumenterAdapter { + @Suppress("DEPRECATION") + return create(lifetime, protocol) + } + + @JvmStatic + @Deprecated("Use protocol.rdInstrumenterAdapter or revise the extension scope instead", ReplaceWith("protocol.rdInstrumenterAdapter")) + fun create(lifetime: Lifetime, protocol: IProtocol): RdInstrumenterAdapter { + EngineProcessRoot.register(protocol.serializers) + + return RdInstrumenterAdapter() + } + + private val __StringNullableSerializer = FrameworkMarshallers.String.nullable() + + const val serializationHash = 1502978559314472937L + + } + override val serializersOwner: ISerializersOwner get() = RdInstrumenterAdapter + override val serializationHash: Long get() = RdInstrumenterAdapter.serializationHash + + //fields + val computeSourceFileByClass: RdCall get() = _computeSourceFileByClass + //methods + //initializer + init { + _computeSourceFileByClass.async = true + } + + init { + bindableChildren.add("computeSourceFileByClass" to _computeSourceFileByClass) + } + + //secondary constructor + private constructor( + ) : this( + RdCall(ComputeSourceFileByClassArguments, __StringNullableSerializer) + ) + + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("RdInstrumenterAdapter (") + printer.indent { + print("computeSourceFileByClass = "); _computeSourceFileByClass.print(printer); println() + } + printer.print(")") + } + //deepClone + override fun deepClone(): RdInstrumenterAdapter { + return RdInstrumenterAdapter( + _computeSourceFileByClass.deepClonePolymorphic() + ) + } + //contexts +} +val IProtocol.rdInstrumenterAdapter get() = getOrCreateExtension(RdInstrumenterAdapter::class) { @Suppress("DEPRECATION") RdInstrumenterAdapter.create(lifetime, this) } + + + +/** + * #### Generated from [EngineProcessModel.kt:9] + */ +data class ComputeSourceFileByClassArguments ( + val canonicalClassName: String +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = ComputeSourceFileByClassArguments::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): ComputeSourceFileByClassArguments { + val canonicalClassName = buffer.readString() + return ComputeSourceFileByClassArguments(canonicalClassName) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: ComputeSourceFileByClassArguments) { + buffer.writeString(value.canonicalClassName) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as ComputeSourceFileByClassArguments + + if (canonicalClassName != other.canonicalClassName) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + canonicalClassName.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("ComputeSourceFileByClassArguments (") + printer.indent { + print("canonicalClassName = "); canonicalClassName.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/RdSourceFindingStrategy.Generated.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/RdSourceFindingStrategy.Generated.kt index 7e053a37f8..42da5b6e9b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/RdSourceFindingStrategy.Generated.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/RdSourceFindingStrategy.Generated.kt @@ -9,25 +9,26 @@ import com.jetbrains.rd.util.lifetime.* import com.jetbrains.rd.util.reactive.* import com.jetbrains.rd.util.string.* import com.jetbrains.rd.util.* +import kotlin.time.Duration import kotlin.reflect.KClass import kotlin.jvm.JvmStatic /** - * #### Generated from [EngineProcessModel.kt:7] + * #### Generated from [EngineProcessModel.kt:17] */ class RdSourceFindingStrategy private constructor( - private val _testsRelativePath: RdCall, - private val _getSourceRelativePath: RdCall, - private val _getSourceFile: RdCall + private val _testsRelativePath: RdCall, + private val _getSourceRelativePath: RdCall, + private val _getSourceFile: RdCall ) : RdExtBase() { //companion companion object : ISerializersOwner { override fun registerSerializersCore(serializers: ISerializers) { - serializers.register(SourceStrategeMethodArgs) + serializers.register(SourceStrategyMethodArgs) } @@ -42,26 +43,23 @@ class RdSourceFindingStrategy private constructor( @JvmStatic @Deprecated("Use protocol.rdSourceFindingStrategy or revise the extension scope instead", ReplaceWith("protocol.rdSourceFindingStrategy")) fun create(lifetime: Lifetime, protocol: IProtocol): RdSourceFindingStrategy { - EngineProcessProtocolRoot.register(protocol.serializers) + EngineProcessRoot.register(protocol.serializers) - return RdSourceFindingStrategy().apply { - identify(protocol.identity, RdId.Null.mix("RdSourceFindingStrategy")) - bind(lifetime, protocol, "RdSourceFindingStrategy") - } + return RdSourceFindingStrategy() } private val __StringNullableSerializer = FrameworkMarshallers.String.nullable() - const val serializationHash = -8019839448677987345L + const val serializationHash = 3794277837200536292L } override val serializersOwner: ISerializersOwner get() = RdSourceFindingStrategy override val serializationHash: Long get() = RdSourceFindingStrategy.serializationHash //fields - val testsRelativePath: RdCall get() = _testsRelativePath - val getSourceRelativePath: RdCall get() = _getSourceRelativePath - val getSourceFile: RdCall get() = _getSourceFile + val testsRelativePath: RdCall get() = _testsRelativePath + val getSourceRelativePath: RdCall get() = _getSourceRelativePath + val getSourceFile: RdCall get() = _getSourceFile //methods //initializer init { @@ -79,9 +77,9 @@ class RdSourceFindingStrategy private constructor( //secondary constructor private constructor( ) : this( - RdCall(FrameworkMarshallers.Void, FrameworkMarshallers.String), - RdCall(SourceStrategeMethodArgs, FrameworkMarshallers.String), - RdCall(SourceStrategeMethodArgs, __StringNullableSerializer) + RdCall(FrameworkMarshallers.Long, FrameworkMarshallers.String), + RdCall(SourceStrategyMethodArgs, FrameworkMarshallers.String), + RdCall(SourceStrategyMethodArgs, __StringNullableSerializer) ) //equals trait @@ -111,25 +109,28 @@ val IProtocol.rdSourceFindingStrategy get() = getOrCreateExtension(RdSourceFindi /** - * #### Generated from [EngineProcessModel.kt:8] + * #### Generated from [EngineProcessModel.kt:18] */ -data class SourceStrategeMethodArgs ( +data class SourceStrategyMethodArgs ( + val testSetId: Long, val classFqn: String, val extension: String? ) : IPrintable { //companion - companion object : IMarshaller { - override val _type: KClass = SourceStrategeMethodArgs::class + companion object : IMarshaller { + override val _type: KClass = SourceStrategyMethodArgs::class @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): SourceStrategeMethodArgs { + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): SourceStrategyMethodArgs { + val testSetId = buffer.readLong() val classFqn = buffer.readString() val extension = buffer.readNullable { buffer.readString() } - return SourceStrategeMethodArgs(classFqn, extension) + return SourceStrategyMethodArgs(testSetId, classFqn, extension) } - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: SourceStrategeMethodArgs) { + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: SourceStrategyMethodArgs) { + buffer.writeLong(value.testSetId) buffer.writeString(value.classFqn) buffer.writeNullable(value.extension) { buffer.writeString(it) } } @@ -145,8 +146,9 @@ data class SourceStrategeMethodArgs ( if (this === other) return true if (other == null || other::class != this::class) return false - other as SourceStrategeMethodArgs + other as SourceStrategyMethodArgs + if (testSetId != other.testSetId) return false if (classFqn != other.classFqn) return false if (extension != other.extension) return false @@ -155,14 +157,16 @@ data class SourceStrategeMethodArgs ( //hash code trait override fun hashCode(): Int { var __r = 0 + __r = __r*31 + testSetId.hashCode() __r = __r*31 + classFqn.hashCode() __r = __r*31 + if (extension != null) extension.hashCode() else 0 return __r } //pretty print override fun print(printer: PrettyPrinter) { - printer.println("SourceStrategeMethodArgs (") + printer.println("SourceStrategyMethodArgs (") printer.indent { + print("testSetId = "); testSetId.print(printer); println() print("classFqn = "); classFqn.print(printer); println() print("extension = "); extension.print(printer); println() } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/SettingsProtocolRoot.Generated.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/SettingsProtocolRoot.Generated.kt deleted file mode 100644 index 62e91e16de..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/SettingsProtocolRoot.Generated.kt +++ /dev/null @@ -1,58 +0,0 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") -package org.utbot.framework.process.generated - -import com.jetbrains.rd.framework.* -import com.jetbrains.rd.framework.base.* -import com.jetbrains.rd.framework.impl.* - -import com.jetbrains.rd.util.lifetime.* -import com.jetbrains.rd.util.reactive.* -import com.jetbrains.rd.util.string.* -import com.jetbrains.rd.util.* -import kotlin.reflect.KClass -import kotlin.jvm.JvmStatic - - - -/** - * #### Generated from [SettingsModel.kt:5] - */ -class SettingsProtocolRoot private constructor( -) : RdExtBase() { - //companion - - companion object : ISerializersOwner { - - override fun registerSerializersCore(serializers: ISerializers) { - SettingsProtocolRoot.register(serializers) - SettingsModel.register(serializers) - } - - - - - - const val serializationHash = 6206621683627449183L - - } - override val serializersOwner: ISerializersOwner get() = SettingsProtocolRoot - override val serializationHash: Long get() = SettingsProtocolRoot.serializationHash - - //fields - //methods - //initializer - //secondary constructor - //equals trait - //hash code trait - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("SettingsProtocolRoot (") - printer.print(")") - } - //deepClone - override fun deepClone(): SettingsProtocolRoot { - return SettingsProtocolRoot( - ) - } - //contexts -} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/EngineUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/EngineUtils.kt index cd86c08cfe..af216dfae2 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/EngineUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/EngineUtils.kt @@ -1,74 +1,19 @@ package org.utbot.framework.util -import org.utbot.common.Reflection -import org.utbot.engine.ValueConstructor -import org.utbot.framework.plugin.api.ExecutableId +import mu.KotlinLogging +import org.utbot.common.logException +import org.utbot.framework.plugin.api.util.constructor.ValueConstructor import org.utbot.framework.plugin.api.MissingState import org.utbot.framework.plugin.api.UtSymbolicExecution import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.plugin.api.UtMethodValueTestSet import org.utbot.framework.plugin.api.UtVoidModel -import org.utbot.framework.plugin.api.classId -import org.utbot.framework.plugin.api.id -import org.utbot.framework.plugin.api.util.constructorId -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 java.util.concurrent.atomic.AtomicInteger -import soot.SootMethod -@Suppress("DEPRECATION") -val Class<*>.anyInstance: Any - get() { -// val defaultCtor = declaredConstructors.singleOrNull { it.parameterCount == 0} -// if (defaultCtor != null) { -// try { -// defaultCtor.isAccessible = true -// return defaultCtor.newInstance() -// } catch (e : Throwable) { -// logger.warn(e) { "Can't create object with default ctor. Fallback to Unsafe." } -// } -// } - return Reflection.unsafe.allocateInstance(this) +private val logger = KotlinLogging.logger { } -// val constructors = runCatching { -// arrayOf(getDeclaredConstructor()) -// }.getOrElse { declaredConstructors } -// -// return constructors.asSequence().mapNotNull { constructor -> -// runCatching { -// val parameters = constructor.parameterTypes.map { defaultParameterValue(it) } -// val isAccessible = constructor.isAccessible -// try { -// constructor.isAccessible = true -// constructor.newInstance(*parameters.toTypedArray()) -// } finally { -// constructor.isAccessible = isAccessible -// } -// }.getOrNull() -// }.firstOrNull() ?: error("Failed to create instance of $this") - } - -/** - * Gets method or constructor id of SootMethod. - */ -val SootMethod.executableId: ExecutableId - get() = when { - isConstructor -> constructorId( - classId = declaringClass.id, - arguments = parameterTypes.map { it.classId }.toTypedArray() - ) - else -> methodId( - classId = declaringClass.id, - name = name, - returnType = returnType.classId, - arguments = parameterTypes.map { it.classId }.toTypedArray() - ) - } - -val modelIdCounter = AtomicInteger(0) val instanceCounter = AtomicInteger(0) fun nextModelName(base: String): String = "$base${instanceCounter.incrementAndGet()}" @@ -88,7 +33,5 @@ fun UtSymbolicExecution.hasThisInstance(): Boolean = when { // An execution must either have this instance or not. // This instance cannot be absent before execution and appear after // as well as it cannot be present before execution and disappear after. - else -> error("Execution configuration must not change between states") -} - -internal fun valueToClassId(value: Any?) = value?.let { it::class.java.id } ?: objectClassId \ No newline at end of file + else -> logger.logException { error("Execution configuration must not change between states") } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/ImplicitlyDeclaredMethods.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/ImplicitlyDeclaredMethods.kt new file mode 100644 index 0000000000..0b9b85225b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/ImplicitlyDeclaredMethods.kt @@ -0,0 +1,50 @@ +package org.utbot.framework.util + +import org.utbot.framework.codegen.util.fieldThatIsGotWith +import org.utbot.framework.codegen.util.fieldThatIsSetWith +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.util.isData +import org.utbot.framework.plugin.api.util.isEnum +import org.utbot.framework.plugin.api.util.isFromKotlin +import org.utbot.framework.plugin.api.util.isKotlinFile + +/** + * Returns whether this method could be implicitly generated by compiler, or not. + * + * Note that here we can only judge this by method name and kind of class (data class, enum, etc). + * There seems to be no (at least, easy) way to check from bytecode if this method was actually overridden by user, + * so this function will return true even if the matching method is not autogenerated but written explicitly by user. + */ +val ExecutableId.isKnownImplicitlyDeclaredMethod: Boolean + get() = + when { + // this check is needed because reflection is not fully supported for file facades -- see KT-16479, + // by now we assume that such classes can't contain autogenerated methods + classId.isKotlinFile -> false + isKotlinGetterOrSetter -> true + classId.isEnum -> name in KnownImplicitlyDeclaredMethods.enumImplicitlyDeclaredMethodNames + classId.isData -> KnownImplicitlyDeclaredMethods.dataClassImplicitlyDeclaredMethodNameRegexps.any { it.matches(name) } + else -> false + } + +internal val ExecutableId.isKotlinGetterOrSetter: Boolean + get() = classId.isFromKotlin && + (classId.fieldThatIsGotWith(this) != null || classId.fieldThatIsSetWith(this) != null) + +/** + * Contains names of methods that are always autogenerated by compiler and thus it is unlikely that + * one would want to generate tests for them. + */ +private object KnownImplicitlyDeclaredMethods { + /** List with names of enum methods that are generated by compiler */ + val enumImplicitlyDeclaredMethodNames = listOf("values", "valueOf") + + /** List with regexps that match names of methods that are generated by Kotlin compiler for data classes */ + val dataClassImplicitlyDeclaredMethodNameRegexps = listOf( + "equals", + "hashCode", + "toString", + "copy", + "component[1-9][0-9]*" + ).map { it.toRegex() } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/ReflectionUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/ReflectionUtils.kt deleted file mode 100644 index 9eb6108f81..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/ReflectionUtils.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.utbot.framework.util - -import org.utbot.framework.plugin.api.FieldId -import soot.RefType - -/** - * Several fields are inaccessible in runtime even via reflection - */ -val FieldId.isInaccessibleViaReflection: Boolean - get() { - val declaringClassName = declaringClass.name - return declaringClassName in inaccessibleViaReflectionClasses || - (name to declaringClassName) in inaccessibleViaReflectionFields - } - -val RefType.isInaccessibleViaReflection: Boolean - get() { - return className in inaccessibleViaReflectionClasses - } - -private val inaccessibleViaReflectionClasses = setOf( - "jdk.internal.reflect.ReflectionFactory", - "jdk.internal.reflect.Reflection", - "jdk.internal.loader.ClassLoaderValue", - "sun.reflect.Reflection", -) - -private val inaccessibleViaReflectionFields = setOf( - "security" to "java.lang.System", -) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/SizeUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/SizeUtils.kt new file mode 100644 index 0000000000..d6da8d7f25 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/SizeUtils.kt @@ -0,0 +1,66 @@ +package org.utbot.framework.util + +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtClassRefModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtCustomModel +import org.utbot.framework.plugin.api.UtDirectGetFieldModel +import org.utbot.framework.plugin.api.UtDirectSetFieldModel +import org.utbot.framework.plugin.api.UtEnumConstantModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtLambdaModel +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.UtReferenceModel +import org.utbot.framework.plugin.api.UtStatementModel +import org.utbot.framework.plugin.api.UtVoidModel + +fun EnvironmentModels.calculateSize(): Int { + val thisInstanceSize = thisInstance?.calculateSize() ?: 0 + val parametersSize = parameters.sumOf { it.calculateSize() } + val staticsSize = statics.values.sumOf { it.calculateSize() } + + return thisInstanceSize + parametersSize + staticsSize +} + +/** + * We assume that "size" for "common" models is 1, 0 for [UtVoidModel] (as they do not return anything) and + * [UtPrimitiveModel] and 2 for [UtNullModel] (we use them as literals in codegen), summarising for + * all statements for [UtAssembleModel] and summarising for all fields and mocks for [UtCompositeModel]. + * + * As [UtReferenceModel] could be recursive, we need to store it in [used]. Moreover, if we have already + * calculated the size for [this] model and [this] is [UtReferenceModel], then in codegen we would have already + * created variable for [this] model and do not need to create it again, so size should be equal to 0. + */ +fun UtModel.calculateSize(used: MutableSet = mutableSetOf()): Int { + if (this in used) return 0 + + if (this is UtReferenceModel) + used += this + + return when (this) { + // `null` is assigned size of `2` to encourage use of empty mocks which have size of `1` over `null`s + is UtNullModel -> 2 + is UtPrimitiveModel, UtVoidModel -> 0 + is UtClassRefModel, is UtEnumConstantModel, is UtArrayModel, is UtCustomModel -> 1 + is UtAssembleModel -> { + 1 + instantiationCall.calculateSize(used) + modificationsChain.sumOf { it.calculateSize(used) } + } + is UtCompositeModel -> 1 + (fields.values + mocks.values.flatten()).sumOf { it.calculateSize(used) } + is UtLambdaModel -> 1 + capturedValues.sumOf { it.calculateSize(used) } + // PythonModel, JsUtModel, UtSpringContextModel may be here + else -> 0 + } +} + +private fun UtStatementModel.calculateSize(used: MutableSet = mutableSetOf()): Int = + when (this) { + is UtExecutableCallModel -> 1 + params.sumOf { it.calculateSize(used) } + (instance?.calculateSize(used) ?: 0) + is UtDirectSetFieldModel -> 1 + fieldModel.calculateSize(used) + instance.calculateSize(used) + + // -2 is added to encourage use of non-hardcoded values (including compensation for one extra `UtAssembleModel`) + is UtDirectGetFieldModel -> (-2 + instance.calculateSize(used)).coerceAtLeast(0) + } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/SootUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/SootUtils.kt index e92dadf74f..dcb18951e4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/SootUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/SootUtils.kt @@ -3,6 +3,7 @@ package org.utbot.framework.util import org.utbot.common.FileUtil import org.utbot.engine.jimpleBody import org.utbot.engine.pureJavaSignature +import org.utbot.framework.UtSettings import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.services.JdkInfo import soot.G @@ -15,6 +16,7 @@ import soot.options.Options import soot.toolkits.graph.ExceptionalUnitGraph import java.io.File import java.nio.file.Path +import java.util.function.Consumer object SootUtils { /** @@ -52,6 +54,11 @@ object SootUtils { private var previousClassPath: String? = null } +/** + * This option is only needed to fast changing from other tools. + */ +var sootOptionConfiguration: Consumer = Consumer { _ -> } + /** * Convert code to Jimple */ @@ -66,6 +73,7 @@ private fun initSoot(buildDirs: List, classpath: String?, jdkInfo: JdkInfo // set true to debug. Disabled because of a bug when two different variables // from the source code have the same name in the jimple body. setPhaseOption("jb", "use-original-names:false") + sootOptionConfiguration.accept(this) set_soot_classpath( FileUtil.isolateClassFiles(*classesToLoad).absolutePath + if (!classpath.isNullOrEmpty()) File.pathSeparator + "$classpath" else "" @@ -88,21 +96,51 @@ private fun initSoot(buildDirs: List, classpath: String?, jdkInfo: JdkInfo Scene.v().loadNecessaryClasses() PackManager.v().runPacks() + // these options are moved out from forEach loop because they are slow due to rd communication + val removeUtBotClassesFromHierarchy = UtSettings.removeUtBotClassesFromHierarchy + val removeSootClassesFromHierarchy = UtSettings.removeSootClassesFromHierarchy // we need this to create hierarchy of classes - Scene.v().classes.forEach { - if (it.resolvingLevel() < SootClass.HIERARCHY) + Scene.v().classes.toList().forEach { + val isUtBotPackage = it.packageName.startsWith(UTBOT_PACKAGE_PREFIX) + + // remove our own classes from the soot scene + if (removeUtBotClassesFromHierarchy && isUtBotPackage) { + val isOverriddenPackage = it.packageName.startsWith(UTBOT_OVERRIDDEN_PACKAGE_PREFIX) + val isExamplesPackage = it.packageName.startsWith(UTBOT_EXAMPLES_PACKAGE_PREFIX) + val isApiPackage = it.packageName.startsWith(UTBOT_API_PACKAGE_PREFIX) + val isVisiblePackage = it.packageName.startsWith(UTBOT_FRAMEWORK_API_VISIBLE_PACKAGE) + + // remove if it is not a part of the examples (CUT), not a part of our API, not an override and not from visible for soot + if (!isOverriddenPackage && !isExamplesPackage && !isApiPackage && !isVisiblePackage) { + Scene.v().removeClass(it) + return@forEach + } + } + + // remove soot's classes from the scene, because we don't wont to analyze them + if (removeSootClassesFromHierarchy && it.packageName.startsWith(SOOT_PACKAGE_PREFIX)) { + Scene.v().removeClass(it) + return@forEach + } + + if (it.resolvingLevel() < SootClass.HIERARCHY) { it.setResolvingLevel(SootClass.HIERARCHY) + } } } fun JimpleBody.graph() = ExceptionalUnitGraph(this) val ExecutableId.sootMethod: SootMethod + get() = sootMethodOrNull ?: error("Class contains not only one method with the required signature.") + +val ExecutableId.sootMethodOrNull: SootMethod? get() { val clazz = Scene.v().getSootClass(classId.name) - return clazz.methods.single { it.pureJavaSignature == signature } + return clazz.methods.singleOrNull { it.pureJavaSignature == signature } } + fun jimpleBody(method: ExecutableId): JimpleBody = method.sootMethod.jimpleBody() @@ -127,16 +165,20 @@ private val classesToLoad = arrayOf( org.utbot.engine.overrides.Long::class, org.utbot.engine.overrides.Short::class, org.utbot.engine.overrides.System::class, + org.utbot.engine.overrides.Throwable::class, + org.utbot.engine.overrides.AutoCloseable::class, + org.utbot.engine.overrides.AccessController::class, org.utbot.engine.overrides.collections.UtOptional::class, org.utbot.engine.overrides.collections.UtOptionalInt::class, org.utbot.engine.overrides.collections.UtOptionalLong::class, org.utbot.engine.overrides.collections.UtOptionalDouble::class, org.utbot.engine.overrides.collections.UtArrayList::class, + org.utbot.engine.overrides.collections.UtArrayList.UtArrayListSimpleIterator::class, org.utbot.engine.overrides.collections.UtArrayList.UtArrayListIterator::class, org.utbot.engine.overrides.collections.UtLinkedList::class, org.utbot.engine.overrides.collections.UtLinkedListWithNullableCheck::class, org.utbot.engine.overrides.collections.UtLinkedList.UtLinkedListIterator::class, - org.utbot.engine.overrides.collections.UtLinkedList.ReverseIteratorWrapper::class, + org.utbot.engine.overrides.collections.UtLinkedList.UtReverseIterator::class, org.utbot.engine.overrides.collections.UtHashSet::class, org.utbot.engine.overrides.collections.UtHashSet.UtHashSetIterator::class, org.utbot.engine.overrides.collections.UtHashMap::class, @@ -153,14 +195,21 @@ private val classesToLoad = arrayOf( org.utbot.engine.overrides.collections.UtGenericStorage::class, org.utbot.engine.overrides.collections.UtGenericAssociative::class, org.utbot.engine.overrides.PrintStream::class, - org.utbot.engine.UtNativeStringWrapper::class, org.utbot.engine.overrides.strings.UtString::class, org.utbot.engine.overrides.strings.UtStringBuilder::class, org.utbot.engine.overrides.strings.UtStringBuffer::class, + org.utbot.engine.overrides.threads.UtThread::class, + org.utbot.engine.overrides.threads.UtThreadGroup::class, + org.utbot.engine.overrides.threads.UtCompletableFuture::class, + org.utbot.engine.overrides.threads.CompletableFuture::class, + org.utbot.engine.overrides.threads.Executors::class, + org.utbot.engine.overrides.threads.UtExecutorService::class, + org.utbot.engine.overrides.threads.UtCountDownLatch::class, org.utbot.engine.overrides.stream.Stream::class, org.utbot.engine.overrides.stream.Arrays::class, org.utbot.engine.overrides.collections.Collection::class, org.utbot.engine.overrides.collections.List::class, + org.utbot.framework.plugin.api.visible.UtStreamConsumingException::class, org.utbot.engine.overrides.stream.UtStream::class, org.utbot.engine.overrides.stream.UtIntStream::class, org.utbot.engine.overrides.stream.UtLongStream::class, @@ -172,4 +221,13 @@ private val classesToLoad = arrayOf( org.utbot.engine.overrides.stream.IntStream::class, org.utbot.engine.overrides.stream.LongStream::class, org.utbot.engine.overrides.stream.DoubleStream::class, -).map { it.java }.toTypedArray() \ No newline at end of file + org.utbot.framework.plugin.api.OverflowDetectionError::class, + org.utbot.framework.plugin.api.TaintAnalysisError::class +).map { it.java }.toTypedArray() + +private const val UTBOT_PACKAGE_PREFIX = "org.utbot" +private const val UTBOT_EXAMPLES_PACKAGE_PREFIX = "$UTBOT_PACKAGE_PREFIX.examples" +private const val UTBOT_API_PACKAGE_PREFIX = "$UTBOT_PACKAGE_PREFIX.api" +private const val UTBOT_OVERRIDDEN_PACKAGE_PREFIX = "$UTBOT_PACKAGE_PREFIX.engine.overrides" +internal const val UTBOT_FRAMEWORK_API_VISIBLE_PACKAGE = "$UTBOT_PACKAGE_PREFIX.framework.plugin.api.visible" +private const val SOOT_PACKAGE_PREFIX = "soot." \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt deleted file mode 100644 index ce66f79400..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/SyntheticMethods.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.utbot.framework.util - -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.util.humanReadableName -import org.utbot.framework.plugin.api.util.isEnum - -fun isKnownSyntheticMethod(method: ExecutableId): Boolean = - if (method.classId.isEnum) - method.humanReadableName.substringBefore('(') in KnownSyntheticMethodNames.enumSyntheticMethodNames - else - false - -/** - * Contains names of methods that are always autogenerated and thus it is unlikely that - * one would want to generate tests for them. - */ -private object KnownSyntheticMethodNames { - /** List with names of enum methods that are autogenerated */ - val enumSyntheticMethodNames = listOf("values", "valueOf") -} \ 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 e4e0436ebd..7c95e724f2 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 @@ -153,9 +153,13 @@ class ConflictTriggers( Conflict.values().forEach { conflict -> map[conflict] = false } } ) : MutableMap by triggers { - val triggered: Boolean + val anyTriggered: Boolean get() = triggers.values.any { it } + fun triggered(conflict: Conflict): Boolean { + return triggers[conflict] ?: false + } + fun reset(vararg conflicts: Conflict) { for (conflict in conflicts) { triggers[conflict] = false diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtConcreteExecutionResultUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtConcreteExecutionResultUtils.kt new file mode 100644 index 0000000000..a0e00573e4 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtConcreteExecutionResultUtils.kt @@ -0,0 +1,61 @@ +package org.utbot.framework.util + +import org.utbot.framework.assemble.AssembleModelGenerator +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.MissingState +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtModel +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import java.util.IdentityHashMap + +/** + * Tries to convert all models from [UtExecutionResult] to [UtAssembleModel] if possible. + * + * @return [UtConcreteExecutionResult] with converted models. + */ +fun UtConcreteExecutionResult.convertToAssemble(packageName: String): UtConcreteExecutionResult { + val allModels = collectAllModels() + + val modelsToAssembleModels = AssembleModelGenerator(packageName).createAssembleModels(allModels) + return updateWithAssembleModels(modelsToAssembleModels) +} + +private fun UtConcreteExecutionResult.updateWithAssembleModels( + assembledUtModels: IdentityHashMap +): UtConcreteExecutionResult { + val toAssemble: (UtModel) -> UtModel = { assembledUtModels.getOrDefault(it, it) } + + val resolvedStateBefore = stateBefore.resolveState(toAssemble) + val resolvedStateAfter = stateAfter.resolveState(toAssemble) + val resolvedResult = (result as? UtExecutionSuccess)?.model?.let { UtExecutionSuccess(toAssemble(it)) } ?: result + + return copy( + stateBefore = resolvedStateBefore, + stateAfter = resolvedStateAfter, + result = resolvedResult, + ) +} + +private fun EnvironmentModels.resolveState(toAssemble: (UtModel) -> UtModel): EnvironmentModels = + if (this is MissingState) { + MissingState + } else { + copy( + thisInstance = thisInstance?.let { toAssemble(it) }, + parameters = parameters.map { toAssemble(it) }, + statics = statics.mapValues { toAssemble(it.value) }, + ) + } + +private fun UtConcreteExecutionResult.collectAllModels(): List { + val allModels = mutableListOf() + + allModels += stateBefore.utModels + allModels += stateAfter.utModels + allModels += listOfNotNull((result as? UtExecutionSuccess)?.model) + + return allModels +} + +private val EnvironmentModels.utModels: List + get() = listOfNotNull(thisInstance) + parameters + statics.values \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtModelVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtModelVisitor.kt index 093992d1b7..e4621e64c9 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtModelVisitor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtModelVisitor.kt @@ -4,6 +4,7 @@ import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtClassRefModel import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtCustomModel import org.utbot.framework.plugin.api.UtEnumConstantModel import org.utbot.framework.plugin.api.UtLambdaModel import org.utbot.framework.plugin.api.UtModel @@ -35,6 +36,7 @@ abstract class UtModelVisitor { is UtAssembleModel -> visit(element, data) is UtCompositeModel -> visit(element, data) is UtLambdaModel -> visit(element, data) + is UtCustomModel -> visit(element, data) } } @@ -44,6 +46,7 @@ abstract class UtModelVisitor { protected abstract fun visit(element: UtAssembleModel, data: D) protected abstract fun visit(element: UtCompositeModel, data: D) protected abstract fun visit(element: UtLambdaModel, data: D) + protected abstract fun visit(element: UtCustomModel, data: D) /** * Returns true when we can traverse the given model. diff --git a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FallbackModelProvider.kt b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FallbackModelProvider.kt deleted file mode 100644 index 9ca0140c57..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FallbackModelProvider.kt +++ /dev/null @@ -1,113 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.common.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.isClassType -import org.utbot.framework.plugin.api.util.isEnum -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: IdGenerator -): 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.isEnum || classId.isClassType -> - classId.defaultValueModel() - classId.isArray -> - UtArrayModel( - id = idGenerator.createId(), - 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 when { - kclass.isAbstract -> { - // sealed class is abstract by itself - UtNullModel(kclass.java.id) - - } - kclass.java.isEnum || kclass == java.lang.Class::class -> { - // No sensible fallback solution for these classes except returning default `null` value - UtNullModel(kclass.java.id) - } - defaultConstructor != null -> { - UtAssembleModel( - id = idGenerator.createId(), - kclass.id, - kclass.id.toString(), - UtExecutableCallModel(instance = null, defaultConstructor.executableId, listOf()) - ) - } - else -> { - UtCompositeModel( - id = idGenerator.createId(), - 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 index 656b0c1655..d925ae8428 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt @@ -1,19 +1,8 @@ package org.utbot.fuzzer import mu.KotlinLogging -import org.utbot.framework.plugin.api.ClassId 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.id -import org.utbot.framework.plugin.api.util.intClassId -import org.utbot.framework.plugin.api.util.longClassId -import org.utbot.framework.plugin.api.util.objectClassId -import org.utbot.framework.plugin.api.util.shortClassId -import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.framework.plugin.api.util.* import org.utbot.framework.util.executableId import soot.BooleanType import soot.ByteType @@ -31,29 +20,8 @@ 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.JInvokeStmt -import soot.jimple.internal.JLeExpr -import soot.jimple.internal.JLookupSwitchStmt -import soot.jimple.internal.JLtExpr -import soot.jimple.internal.JNeExpr -import soot.jimple.internal.JSpecialInvokeExpr -import soot.jimple.internal.JStaticInvokeExpr -import soot.jimple.internal.JTableSwitchStmt -import soot.jimple.internal.JVirtualInvokeExpr +import soot.jimple.internal.* import soot.toolkits.graph.ExceptionalUnitGraph -import java.lang.reflect.GenericArrayType -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.lang.reflect.TypeVariable -import java.lang.reflect.WildcardType private val logger = KotlinLogging.logger {} @@ -82,7 +50,10 @@ fun collectConstantsForFuzzer(graph: ExceptionalUnitGraph): Set @@ -112,6 +83,10 @@ private object ConstantsFromIfStatement: ConstantsFinder { // 1. compare (result = local compare constant) // 2. if result if (unit is JAssignStmt) { + // Accepts only those assignments statements that uses compare operation on the right + if (unit.rightOp !is JCmpExpr) { + return emptyList() + } useBoxes = unit.rightOp.useBoxes.mapNotNull { (it as? ImmediateBox)?.value } ifStatement = nextDirectUnit(graph, unit) as? JIfStmt } @@ -161,16 +136,20 @@ private object ConstantsFromCast: ConstantsFinder { val const = next.useBoxes.findFirstInstanceOf() if (const != null) { val op = (nextDirectUnit(graph, next) as? JIfStmt)?.let(::sootIfToFuzzedOp) ?: FuzzedContext.Unknown - 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 + when (val exactValue = const.plainValue) { + is 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 + } + ) + is String -> { + return listOfNotNull(FuzzedConcreteValue(stringClassId, exactValue, op)) } - ) + } } } return emptyList() @@ -299,24 +278,4 @@ private fun sootIfToFuzzedOp(unit: JIfStmt) = when (unit.condition) { else -> FuzzedContext.Unknown } -private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first() - -fun toFuzzerType(type: Type): FuzzedType { - return when (type) { - is WildcardType -> type.upperBounds.firstOrNull()?.let(::toFuzzerType) ?: FuzzedType(objectClassId) - is TypeVariable<*> -> type.bounds.firstOrNull()?.let(::toFuzzerType) ?: FuzzedType(objectClassId) - is ParameterizedType -> FuzzedType((type.rawType as Class<*>).id, type.actualTypeArguments.map { toFuzzerType(it) }) - is GenericArrayType -> { - val genericComponentType = type.genericComponentType - val fuzzerType = toFuzzerType(genericComponentType) - val classId = if (genericComponentType !is GenericArrayType) { - ClassId("[L${fuzzerType.classId.name};", fuzzerType.classId) - } else { - ClassId("[" + fuzzerType.classId.name, fuzzerType.classId) - } - FuzzedType(classId) - } - is Class<*> -> FuzzedType(type.id) - else -> error("Unknown type: $type") - } -} \ No newline at end of file +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/DataClasses.kt b/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt index 5ecae62040..9731658270 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/sarif/DataClasses.kt @@ -1,7 +1,13 @@ package org.utbot.sarif -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.annotation.* +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue /** * Useful links: @@ -14,6 +20,12 @@ data class Sarif( val runs: List ) { companion object { + + private val jsonMapper = jacksonObjectMapper() + .registerModule(SimpleModule() + .addDeserializer(SarifLocationWrapper::class.java, SarifLocationWrapperDeserializer()) + ) + private const val defaultSchema = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json" private const val defaultVersion = @@ -24,7 +36,23 @@ data class Sarif( fun fromRun(run: SarifRun) = Sarif(defaultSchema, defaultVersion, listOf(run)) + + fun fromJson(reportInJson: String): Sarif = + jsonMapper.readValue(reportInJson) } + + fun toJson(): String = + jsonMapper + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(this) + + operator fun plus(other: Sarif): Sarif = + this.copy(runs = this.runs + other.runs) + + @JsonIgnore + fun getAllResults(): List = + runs.flatMap { it.results } } /** @@ -96,7 +124,7 @@ data class SarifResult( val ruleId: String, val level: Level, val message: Message, - val locations: List = listOf(), + val locations: List = listOf(), val relatedLocations: List = listOf(), val codeFlows: List = listOf() ) { @@ -104,8 +132,8 @@ data class SarifResult( * Returns the total number of locations in all [codeFlows]. */ fun totalCodeFlowLocations() = - codeFlows.sumBy { codeFlow -> - codeFlow.threadFlows.sumBy { threadFlow -> + codeFlows.sumOf { codeFlow -> + codeFlow.threadFlows.sumOf { threadFlow -> threadFlow.locations.size } } @@ -130,14 +158,41 @@ data class Message( val markdown: String? = null ) -// physical location +// location /** * [Documentation](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#location-object) */ +sealed class SarifLocationWrapper + data class SarifPhysicalLocationWrapper( - val physicalLocation: SarifPhysicalLocation, -) + val physicalLocation: SarifPhysicalLocation // this name used in the SarifLocationWrapperDeserializer +) : SarifLocationWrapper() + +data class SarifLogicalLocationsWrapper( + val logicalLocations: List // this name used in the SarifLocationWrapperDeserializer +) : SarifLocationWrapper() + +/** + * Custom JSON deserializer for sealed class [SarifLocationWrapper]. + * Returns [SarifPhysicalLocationWrapper] or [SarifLogicalLocationsWrapper]. + */ +class SarifLocationWrapperDeserializer : JsonDeserializer() { + override fun deserialize(jp: JsonParser, context: DeserializationContext?): SarifLocationWrapper { + val node: JsonNode = jp.codec.readTree(jp) + val isPhysicalLocation = node.get("physicalLocation") != null // field name + val isLogicalLocations = node.get("logicalLocations") != null // field name + return when { + isPhysicalLocation -> { + jacksonObjectMapper().readValue(node.toString()) + } + isLogicalLocations -> { + return jacksonObjectMapper().readValue(node.toString()) + } + else -> error("SarifLocationWrapperDeserializer: Cannot parse ${node.toPrettyString()}") + } + } +} /** * [Documentation](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#physicallocation-object) @@ -152,7 +207,9 @@ data class SarifArtifact( val uriBaseId: String = "%SRCROOT%" ) -// all fields should be one-based +/** + * All fields should be one-based. + */ data class SarifRegion( val startLine: Int, val endLine: Int? = null, @@ -163,17 +220,33 @@ data class SarifRegion( /** * Makes [startColumn] the first non-whitespace character in [startLine] in the [text]. * If the [text] contains less than [startLine] lines, [startColumn] == null. + * @param startLine should be one-based */ fun withStartLine(text: String, startLine: Int): SarifRegion { val neededLine = text.split('\n').getOrNull(startLine - 1) // to zero-based val startColumn = neededLine?.run { takeWhile { it.toString().isBlank() }.length + 1 // to one-based } - return SarifRegion(startLine = startLine, startColumn = startColumn) + val safeStartLine = if (startLine < 1) { + logger.warn { "For some reason startLine < 1, so now it is equal to 1" } + 1 // we don't want to fail, so just set the line number to 1 + } else { + startLine + } + return SarifRegion(startLine = safeStartLine, startColumn = startColumn) } } } +// logical locations + +/** + * [Documentation](https://github.com/microsoft/sarif-tutorials/blob/main/docs/2-Basics.md#physical-and-logical-locations) + */ +data class SarifLogicalLocation( + val fullyQualifiedName: String +) + // related locations /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/sarif/RdSourceFindingStrategy.kt b/utbot-framework/src/main/kotlin/org/utbot/sarif/RdSourceFindingStrategy.kt index 68adbb1295..5b38c1e18c 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/sarif/RdSourceFindingStrategy.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/sarif/RdSourceFindingStrategy.kt @@ -2,19 +2,22 @@ package org.utbot.sarif import kotlinx.coroutines.runBlocking import org.utbot.framework.process.generated.RdSourceFindingStrategy -import org.utbot.framework.process.generated.SourceStrategeMethodArgs +import org.utbot.framework.process.generated.SourceStrategyMethodArgs import java.io.File -class RdSourceFindingStrategyFacade(private val realStrategy: RdSourceFindingStrategy): SourceFindingStrategy() { +class RdSourceFindingStrategyFacade( + private val testSetsId: Long, + private val realStrategy: RdSourceFindingStrategy +) : SourceFindingStrategy() { override val testsRelativePath: String - get() = runBlocking { realStrategy.testsRelativePath.startSuspending(Unit) } + get() = runBlocking { realStrategy.testsRelativePath.startSuspending(testSetsId) } override fun getSourceRelativePath(classFqn: String, extension: String?): String = runBlocking { - realStrategy.getSourceRelativePath.startSuspending(SourceStrategeMethodArgs(classFqn, extension)) + realStrategy.getSourceRelativePath.startSuspending(SourceStrategyMethodArgs(testSetsId, classFqn, extension)) } override fun getSourceFile(classFqn: String, extension: String?): File? = runBlocking { - realStrategy.getSourceFile.startSuspending(SourceStrategeMethodArgs(classFqn, extension))?.let { + realStrategy.getSourceFile.startSuspending(SourceStrategyMethodArgs(testSetsId, classFqn, extension))?.let { File(it) } } 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 2166156bac..0d0d8d7121 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt @@ -1,12 +1,15 @@ package org.utbot.sarif -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue +import mu.KotlinLogging import org.utbot.common.PathUtil.fileExtension import org.utbot.common.PathUtil.toPath import org.utbot.framework.UtSettings import org.utbot.framework.plugin.api.* +import org.utbot.instrumentation.process.InstrumentedProcessMain +import java.nio.file.Path +import kotlin.io.path.nameWithoutExtension + +internal val logger = KotlinLogging.logger {} /** * Used for the SARIF report creation by given test cases and generated tests code. @@ -21,45 +24,55 @@ class SarifReport( private val generatedTestsCode: String, private val sourceFinding: SourceFindingStrategy ) { - companion object { - /** * Merges several SARIF reports given as JSON-strings into one */ fun mergeReports(reports: List): String = reports.fold(Sarif.empty()) { sarif: Sarif, report: String -> - sarif.copy(runs = sarif.runs + report.jsonToSarif().runs) - }.sarifToJson() - - // internal + sarif + Sarif.fromJson(report) + }.toJson() - private fun String.jsonToSarif(): Sarif = - jacksonObjectMapper().readValue(this) - - private fun Sarif.sarifToJson(): String = - jacksonObjectMapper() - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .writerWithDefaultPrettyPrinter() - .writeValueAsString(this) + /** + * Minimizes SARIF results between several reports. + * + * More complex version of the [SarifReport.minimizeResults]. + */ + fun minimizeSarifResults(srcPathToSarif: MutableMap): MutableMap { + val pathToSarifResult = srcPathToSarif.entries.flatMap { (path, sarif) -> + sarif.getAllResults().map { sarifResult -> + path to sarifResult + } + } + val groupedResults = pathToSarifResult.groupBy { (_, sarifResult) -> + sarifResult.ruleId to sarifResult.locations + } + val minimizedResults = groupedResults.map { (_, sarifResultsGroup) -> + sarifResultsGroup.minByOrNull { (_, sarifResult) -> + sarifResult.totalCodeFlowLocations() + }!! + } + val groupedByPath = minimizedResults + .groupBy { (path, _) -> path } + .mapValues { (_, resultsWithPath) -> + resultsWithPath.map { (_, sarifResult) -> sarifResult } // remove redundant path + } + val pathToSarifTool = srcPathToSarif.mapValues { (_, sarif) -> + sarif.runs.first().tool + } + val paths = pathToSarifTool.keys intersect groupedByPath.keys + return paths.associateWith { path -> + val sarifTool = pathToSarifTool[path]!! + val sarifResults = groupedByPath[path]!! + Sarif.fromRun(SarifRun(sarifTool, sarifResults)) + }.toMutableMap() + } } /** - * Creates a SARIF report and returns it as string + * Creates a SARIF report. */ - fun createReport(): String = - constructSarif().sarifToJson() - - // internal - - private val defaultLineNumber = 1 // if the line in the source code where the exception is thrown is unknown - - /** - * [Read more about links to locations](https://github.com/microsoft/sarif-tutorials/blob/main/docs/3-Beyond-basics.md#msg-links-location) - */ - private val relatedLocationId = 1 // for attaching link to generated test in related locations - - private fun constructSarif(): Sarif { + fun createReport(): Sarif { val sarifResults = mutableListOf() val sarifRules = mutableSetOf() @@ -85,6 +98,17 @@ class SarifReport( ) } + // internal + + private val defaultLineNumber = 1 // if the line in the source code where the exception is thrown is unknown + + /** + * [Read more about links to locations](https://github.com/microsoft/sarif-tutorials/blob/main/docs/3-Beyond-basics.md#msg-links-location) + */ + private val relatedLocationId = 1 // for attaching link to generated test in related locations + + private val stackTraceLengthForStackOverflow = 50 // stack overflow error may have too many elements + /** * Minimizes detected errors and removes duplicates. * @@ -133,17 +157,35 @@ class SarifReport( val methodArguments = utExecution.stateBefore.parameters .joinToString(prefix = "", separator = ", ", postfix = "") { it.preview() } + val errorMessage = when (executionFailure) { + is UtTimeoutException -> { + "Unexpected behavior: ${executionFailure.exception.message}" + } + is UtTaintAnalysisFailure -> { + executionFailure.exception.message + } + else -> { + val exceptionSimpleName = executionFailure.exception::class.java.simpleName + val exceptionMessage = executionFailure.exception.message + if (exceptionMessage == null) { + "Unexpected $exceptionSimpleName" + } else { + "Unexpected $exceptionSimpleName: $exceptionMessage" + } + } + } + val sarifResult = SarifResult( ruleId, Level.Error, Message( text = """ - Unexpected exception: ${executionFailure.exception}. + $errorMessage. Test case: `$methodName($methodArguments)` [Generated test for this case]($relatedLocationId) """.trimIndent() ), - getLocations(utExecution, classFqn), + getLocations(method, utExecution, classFqn), getRelatedLocations(utExecution), getCodeFlows(method, utExecution, executionFailure) ) @@ -164,16 +206,23 @@ class SarifReport( return Pair(sarifResult, sarifRule) } - private fun getLocations(utExecution: UtExecution, classFqn: String?): List { + private fun getLocations( + method: ExecutableId, + utExecution: UtExecution, + classFqn: String? + ): List { if (classFqn == null) return listOf() - val sourceRelativePath = sourceFinding.getSourceRelativePath(classFqn) - val startLine = getLastLineNumber(utExecution) ?: defaultLineNumber - val sourceCode = sourceFinding.getSourceFile(classFqn)?.readText() ?: "" + val (startLine, classWithErrorFqn) = getLastLineNumberWithClassFqn(method, utExecution, classFqn) + val sourceCode = sourceFinding.getSourceFile(classWithErrorFqn)?.readText() ?: "" val sourceRegion = SarifRegion.withStartLine(sourceCode, startLine) + val sourceRelativePath = sourceFinding.getSourceRelativePath(classWithErrorFqn) return listOf( SarifPhysicalLocationWrapper( SarifPhysicalLocation(SarifArtifact(sourceRelativePath), sourceRegion) + ), + SarifLogicalLocationsWrapper( + listOf(SarifLogicalLocation(classWithErrorFqn)) // class name without method name ) ) } @@ -199,38 +248,30 @@ class SarifReport( utExecution: UtExecution, executionFailure: UtExecutionFailure ): List { - /* Example of a typical stack trace: - - java.lang.Math.multiplyExact(Math.java:867) - - com.abc.Util.multiply(Util.java:10) - - com.abc.Util.multiply(Util.java:6) - - com.abc.Main.example(Main.java:11) // <- `lastMethodCallIndex` - - sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - - ... - */ - val stackTrace = executionFailure.exception.stackTrace + val stackTraceResolved = filterStackTrace(method, executionFailure) + .mapNotNull { findStackTraceElementLocation(it) } + .toMutableList() - val lastMethodCallIndex = stackTrace.indexOfLast { - it.className == method.classId.name && it.methodName == method.name + if (stackTraceResolved.isEmpty() && utExecution is UtSymbolicExecution) { + stackTraceResolved += stackTraceFromSymbolicSteps(utExecution) // fallback logic } - if (lastMethodCallIndex == -1) - return listOf() - // taking all elements before the last `method` call - val stackTraceFiltered = stackTrace.take(lastMethodCallIndex + 1) - val stackTraceResolved = stackTraceFiltered.mapNotNull { - findStackTraceElementLocation(it) - }.toMutableList() - if (stackTraceResolved.isEmpty()) - return listOf() // empty stack trace is not shown + if (stackTraceResolved.isEmpty()) { // still empty + stackTraceResolved += stackTraceFromCoverage(utExecution) // fallback logic (2) + } // prepending stack trace by `method` call in generated tests val methodCallLocation: SarifPhysicalLocation? = - findMethodCallInTestBody(utExecution.testMethodName, method.name) + findMethodCallInTestBody(utExecution.testMethodName, method.name, utExecution) if (methodCallLocation != null) { + val testFileName = sourceFinding.testsRelativePath.toPath().fileName + val testClassName = testFileName.nameWithoutExtension + val testMethodName = utExecution.testMethodName + val methodCallLineNumber = methodCallLocation.region.startLine val methodCallLocationWrapper = SarifFlowLocationWrapper( SarifFlowLocation( message = Message( - text = "${sourceFinding.testsRelativePath.toPath().fileName}:${methodCallLocation.region.startLine}" + text = "$testClassName.$testMethodName($testFileName:$methodCallLineNumber)" ), physicalLocation = methodCallLocation ) @@ -245,6 +286,115 @@ class SarifReport( ) } + private fun filterStackTrace( + method: ExecutableId, + executionFailure: UtExecutionFailure + ): List { + /* Example of a typical stack trace: + - java.lang.Math.multiplyExact(Math.java:867) + - com.abc.Util.multiply(Util.java:10) + - com.abc.Util.multiply(Util.java:6) + - com.abc.Main.example(Main.java:11) // <- `lastMethodCallIndex` + - sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + - ... + */ + var stackTrace = executionFailure.exception.stackTrace.toList() + + val lastMethodCallIndex = stackTrace.indexOfLast { + it.className == method.classId.name && it.methodName == method.name + } + if (lastMethodCallIndex != -1) { + // taking all elements before the last `method` call + stackTrace = stackTrace.take(lastMethodCallIndex + 1) + } else { // no `method` call in the stack trace + if (executionFailure.exception !is StackOverflowError) { + stackTrace = listOf() // (likely) the stack trace contains only our internal calls + } + } + + if (executionFailure.exception is StackOverflowError) { + stackTrace = stackTrace.takeLast(stackTraceLengthForStackOverflow) + } + + val stackTraceFiltered = stackTrace.filter { + // filter all internal calls related to the instrumentation process + val forbiddenPackage = InstrumentedProcessMain::class.java.`package`.name + !it.className.startsWith(forbiddenPackage) + } + + return stackTraceFiltered + } + + /** + * Constructs the stack trace from the symbolic path. + */ + private fun stackTraceFromSymbolicSteps(utExecution: UtSymbolicExecution): List { + val depth2LastStep = mutableListOf() + for (step in utExecution.symbolicSteps) { + if (step.callDepth >= depth2LastStep.size) { + depth2LastStep.add(step) + } else if (step.callDepth >= 0) { + depth2LastStep[step.callDepth] = step + while (step.callDepth != depth2LastStep.size - 1) { + depth2LastStep.removeLast() + } + } + } + + val sarifExecutionTrace = depth2LastStep.map { step -> + resolveStackTraceElementByNames( + classFqn = step.method.declaringClass.name, + methodName = step.method.name, + lineNumber = step.lineNumber, + ) + } + + return sarifExecutionTrace.reversed() // to stack trace + } + + /** + * Constructs the stack trace from the list of covered instructions. + */ + private fun stackTraceFromCoverage(utExecution: UtExecution): List { + val coveredInstructions = utExecution.coverage?.coveredInstructions + ?: return listOf() + + val executionTrace = coveredInstructions.groupBy { instruction -> + instruction.internalName to instruction.methodSignature // group by method + }.map { (_, instructionsForOneMethod) -> + instructionsForOneMethod.last() // we need only last to construct the stack trace + } + + val sarifExecutionTrace = executionTrace.map { instruction -> + resolveStackTraceElementByNames( + classFqn = instruction.className, + methodName = instruction.methodSignature.substringBefore('('), + lineNumber = instruction.lineNumber + ) + } + + return sarifExecutionTrace.reversed() // to stack trace + } + + private fun resolveStackTraceElementByNames( + classFqn: String, + methodName: String, + lineNumber: Int + ): SarifFlowLocationWrapper { + val sourceFilePath = sourceFinding.getSourceRelativePath(classFqn) + val sourceFileName = sourceFilePath.substringAfterLast('/') + + return SarifFlowLocationWrapper( + SarifFlowLocation( + message = Message("$classFqn.$methodName($sourceFileName:$lineNumber)"), + physicalLocation = SarifPhysicalLocation( + SarifArtifact(uri = sourceFilePath), + SarifRegion(startLine = lineNumber) // lineNumber is one-based + ) + ) + ) + } + private fun findStackTraceElementLocation(stackTraceElement: StackTraceElement): SarifFlowLocationWrapper? { val lineNumber = stackTraceElement.lineNumber if (lineNumber < 1) @@ -269,9 +419,15 @@ class SarifReport( generatedTestsCode.split('\n') } - private fun findMethodCallInTestBody(testMethodName: String?, methodName: String): SarifPhysicalLocation? { + private fun findMethodCallInTestBody( + testMethodName: String?, + methodName: String, + utExecution: UtExecution, + ): SarifPhysicalLocation? { if (testMethodName == null) return null + if (utExecution.result is UtSandboxFailure) // if there is no method call in test + return getRelatedLocations(utExecution).firstOrNull()?.physicalLocation // searching needed test val testMethodStartsAt = testsBodyLines.indexOfFirst { line -> @@ -331,32 +487,62 @@ class SarifReport( } /** - * Returns the number of the last line in the execution path. + * Returns the number of the last line in the execution path + * And the name of the class in which it is located. */ - private fun getLastLineNumber(utExecution: UtExecution): Int? { - // if for some reason we can't extract the last line from the path - val lastCoveredInstruction = - utExecution.coverage?.coveredInstructions?.lastOrNull()?.lineNumber - - return if (utExecution is UtSymbolicExecution) { - val lastPathLine = try { - // path/fullPath might be empty when engine executes in another process - - // soot entities cannot be passed to the main process because kryo cannot deserialize them - utExecution.path.lastOrNull()?.stmt?.javaSourceStartLineNumber - } catch (t: Throwable) { - null - } + private fun getLastLineNumberWithClassFqn( + method: ExecutableId, + utExecution: UtExecution, + defaultClassFqn: String + ): Pair { + val lastSymbolicStep = (utExecution as? UtSymbolicExecution)?.symbolicSteps?.lastOrNull() + if (lastSymbolicStep != null) { + return Pair( + lastSymbolicStep.lineNumber, + lastSymbolicStep.method.declaringClass.name + ) + } + + val coveredInstructions = utExecution.coverage?.coveredInstructions + val lastCoveredInstruction = coveredInstructions?.lastOrNull() + if (lastCoveredInstruction != null) { + return Pair( + lastCoveredInstruction.lineNumber, // .lineNumber is one-based + lastCoveredInstruction.className + ) + } + + // if for some reason we can't extract the last line from the coverage + val lastPathElementLineNumber = try { + // path/fullPath might be empty when engine executes in another process - + // soot entities cannot be passed to the main process because kryo cannot deserialize them + (utExecution as? UtSymbolicExecution)?.path?.lastOrNull()?.stmt?.javaSourceStartLineNumber // one-based + } catch (t: Throwable) { + null + } + if (lastPathElementLineNumber != null) { + return Pair(lastPathElementLineNumber, defaultClassFqn) + } + + val methodDefinitionLine = getMethodDefinitionLineNumber(method) + return Pair(methodDefinitionLine ?: defaultLineNumber, defaultClassFqn) + } - lastPathLine ?: lastCoveredInstruction - } else { - lastCoveredInstruction + private fun getMethodDefinitionLineNumber(method: ExecutableId): Int? { + val sourceFile = sourceFinding.getSourceFile(method.classId.canonicalName) + val lineNumber = sourceFile?.readLines()?.indexOfFirst { line -> + line.contains(" ${method.name}(") // method definition } + return if (lineNumber == null || lineNumber == -1) null else lineNumber + 1 // to one-based } private fun shouldProcessExecutionResult(result: UtExecutionResult): Boolean { val implicitlyThrown = result is UtImplicitlyThrownException val overflowFailure = result is UtOverflowFailure && UtSettings.treatOverflowAsError + val taintAnalysisFailure = result is UtTaintAnalysisFailure && UtSettings.useTaintAnalysis val assertionError = result is UtExplicitlyThrownException && result.exception is AssertionError - return implicitlyThrown || overflowFailure || assertionError + val sandboxFailure = result is UtSandboxFailure + val timeoutException = result is UtTimeoutException + return implicitlyThrown || overflowFailure || taintAnalysisFailure || assertionError || sandboxFailure || timeoutException } } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/TaintConfigurationProvider.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintConfigurationProvider.kt new file mode 100644 index 0000000000..e0e9b101a4 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintConfigurationProvider.kt @@ -0,0 +1,158 @@ +package org.utbot.taint + +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.internal.synchronized +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.encodeToByteArray +import mu.KotlinLogging +import org.utbot.common.PathUtil.toPath +import org.utbot.common.utBotTempDirectory +import org.utbot.taint.model.TaintConfiguration +import org.utbot.taint.parser.yaml.TaintYamlParser +import java.io.File + +const val MODIFICATION_TIME_INVALID = 0L +const val TAINT_CONFIGURATION_RESOURCES_PATH = "taint/config.yaml" +const val TAINT_CONFIGURATION_CACHED_DEFAULT_NAME = "taint-config-cached" + +val lockResourcesCachedFile = Object() + +private val logger = KotlinLogging.logger {} + +/** + * Provide the [TaintConfiguration]. + */ +interface TaintConfigurationProvider { + + /** + * Returns the time that the configuration was last modified. + * + * Uses the same format as the [java.io.File.lastModified]. + */ + fun lastModified(): Long + + /** + * Provides [TaintConfiguration]. + */ + fun getConfiguration(): TaintConfiguration +} + +/** + * Provides an empty [TaintConfiguration]. + */ +object TaintConfigurationProviderEmpty : TaintConfigurationProvider { + override fun lastModified() = MODIFICATION_TIME_INVALID + override fun getConfiguration() = TaintConfiguration() // no rules +} + +/** + * Reads and parses [TaintConfiguration] from the [configPath]. + * + * @param configPath relative path to the .yaml file in resources + */ +class TaintConfigurationProviderResources( + private val configPath: String = TAINT_CONFIGURATION_RESOURCES_PATH +) : TaintConfigurationProvider { + + override fun lastModified(): Long { + val selfJarFile = File(javaClass.protectionDomain.codeSource.location.toURI().path) + return selfJarFile.lastModified() + } + + override fun getConfiguration(): TaintConfiguration { + val configUrls = javaClass.classLoader.getResources(configPath).toList() + val configUrl = when (configUrls.size) { + 0 -> return TaintConfiguration() + 1 -> configUrls.single() + else -> error("Cannot choose between several taint configurations: $configUrls") + } + val yamlInput = configUrl.readText() + val yamlConfig = TaintYamlParser.parse(yamlInput) + return YamlTaintConfigurationAdapter.convert(yamlConfig) + } +} + +/** + * Reads and parses [TaintConfiguration] from the [configPath]. + * + * @param configPath relative path to the .yaml file in the user's project. + */ +class TaintConfigurationProviderUserRules(private val configPath: String) : TaintConfigurationProvider { + + override fun lastModified(): Long { + val configFile = configPath.toPath().toFile() + if (!configFile.exists()) { + return MODIFICATION_TIME_INVALID + } + return configFile.lastModified() + } + + override fun getConfiguration(): TaintConfiguration { + val configFile = configPath.toPath().toFile() + if (!configFile.exists()) { + logger.warn { "Taint analysis configuration not found: file $configPath doesn't exist." } + return TaintConfiguration() + } else { + logger.info { "Used taint analysis configuration from file $configPath." } + } + val yamlInput = configFile.readText() + val yamlConfig = TaintYamlParser.parse(yamlInput) + return YamlTaintConfigurationAdapter.convert(yamlConfig) + } +} + +/** + * Combines taint configurations from several providers. + */ +class TaintConfigurationProviderCombiner(private val inners: List) : TaintConfigurationProvider { + + override fun lastModified(): Long { + return inners.maxOfOrNull { it.lastModified() } ?: MODIFICATION_TIME_INVALID + } + + override fun getConfiguration(): TaintConfiguration = + inners.fold(TaintConfiguration()) { resultConfig, configProvider -> + resultConfig + configProvider.getConfiguration() + } +} + +/** + * Stores binary configuration file to the utbot temp directory to reduce parsing time in the future. + * + * @param nameSuffix the cached file will have "-$nameSuffix" suffix + * @param inner provider to cache + */ +class TaintConfigurationProviderCached( + nameSuffix: String, + private val inner: TaintConfigurationProvider, +) : TaintConfigurationProvider { + + private val cachedConfigName = "$TAINT_CONFIGURATION_CACHED_DEFAULT_NAME-$nameSuffix" + + private val cachedConfigFile = utBotTempDirectory.resolve(cachedConfigName).toFile() + + override fun lastModified(): Long { + if (!cachedConfigFile.exists()) { + return MODIFICATION_TIME_INVALID + } + return cachedConfigFile.lastModified() + } + + @OptIn(InternalCoroutinesApi::class, ExperimentalSerializationApi::class) + override fun getConfiguration(): TaintConfiguration = + if (!cachedConfigFile.exists() || cachedConfigFile.lastModified() < inner.lastModified()) { + val config = inner.getConfiguration() + val bytes = Cbor.encodeToByteArray(config) + synchronized(lockResourcesCachedFile) { + cachedConfigFile.writeBytes(bytes) + } + config + } else { + val bytes = synchronized(lockResourcesCachedFile) { + cachedConfigFile.readBytes() + } + Cbor.decodeFromByteArray(bytes) + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/TaintContext.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintContext.kt new file mode 100644 index 0000000000..2ba3be0187 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintContext.kt @@ -0,0 +1,11 @@ +package org.utbot.taint + +import org.utbot.taint.model.TaintConfiguration + +/** + * Mutable state that is modified during the taint analyzer work. + */ +class TaintContext( + val markManager: TaintMarkManager, + val configuration: TaintConfiguration, +) diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/TaintMarkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintMarkManager.kt new file mode 100644 index 0000000000..d1144af812 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintMarkManager.kt @@ -0,0 +1,144 @@ +package org.utbot.taint + +import kotlinx.collections.immutable.persistentListOf +import org.utbot.engine.* +import org.utbot.engine.pc.* +import org.utbot.engine.symbolic.SymbolicStateUpdate +import org.utbot.taint.TaintUtil.isEmpty +import org.utbot.taint.model.TaintMark +import org.utbot.taint.model.TaintMarks +import org.utbot.taint.model.TaintMarksAll +import org.utbot.taint.model.TaintMarksSet + +/** + * Manages taint vectors. + */ +class TaintMarkManager(private val markRegistry: TaintMarkRegistry) { + + /** + * Returns true if taint vector at address [addr] in [memory] contains taint [mark]. + */ + fun containsMark(memory: Memory, addr: UtAddrExpression, mark: TaintMark): UtBoolExpression { + val taintVector = memory.taintVector(addr).toLongValue() + val taintMarkId = markRegistry.idByMark(mark) + val taintMarkVector = mkLong(taintMarkId).toLongValue() + return mkNot(mkEq(And(taintVector, taintMarkVector), emptyTaintVector)) + } + + /** + * Returns true if taint vector at address [addr] in [memory] contains any taint mark. + */ + fun containsAnyMark(memory: Memory, addr: UtAddrExpression): UtBoolExpression { + val taintVector = memory.taintVector(addr).toLongValue() + return mkNot(mkEq(taintVector, emptyTaintVector.toLongValue())) + } + + /** + * Replaces the taint vector at address [addr] with a new taint vector constructed from [marks]. + * Replaces only if [condition] is true. + */ + fun setMarks( + memory: Memory, + addr: UtAddrExpression, + marks: TaintMarks, + condition: UtBoolExpression + ): SymbolicStateUpdate { + if (marks.isEmpty()) { + return SymbolicStateUpdate() + } + + val newTaintVector = constructTaintVector(marks) + val oldTaintVector = memory.taintVector(addr) + + val taintUpdateExpr = UtIteExpression( + condition, + thenExpr = newTaintVector, + elseExpr = oldTaintVector + ) + + return updateAddr(addr, taintUpdateExpr) + } + + /** + * Removes taint [marks] from the taint vector at address [addr] in [memory] if [condition] is true. + */ + fun clearMarks( + memory: Memory, + addr: UtAddrExpression, + marks: TaintMarks, + condition: UtBoolExpression + ): SymbolicStateUpdate { + if (marks.isEmpty()) { + return SymbolicStateUpdate() + } + + val taintMarks = constructTaintVector(marks).toLongValue() + val taintMarksNegated = UtBvNotExpression(taintMarks).toLongValue() + val oldTaintVectorExpr = memory.taintVector(addr) + val newTaintVectorExpr = And(taintMarksNegated, oldTaintVectorExpr.toLongValue()) + + val taintUpdateExpr = UtIteExpression( + condition, + thenExpr = newTaintVectorExpr, + elseExpr = oldTaintVectorExpr + ) + + return updateAddr(addr, taintUpdateExpr) + } + + /** + * Passes taint [marks] contained in the taint vector at [fromAddr] + * from [fromAddr] to [toAddr] in [memory] if [condition] is true. + */ + fun passMarks( + memory: Memory, + fromAddr: UtAddrExpression, + toAddr: UtAddrExpression, + marks: TaintMarks, + condition: UtBoolExpression + ): SymbolicStateUpdate { + if (marks.isEmpty()) { + return SymbolicStateUpdate() + } + + val taintMarks = constructTaintVector(marks).toLongValue() + val oldTaintVectorFrom = memory.taintVector(fromAddr).toLongValue() + val intersection = And(taintMarks, oldTaintVectorFrom) + val hasAtLeastOneMark = mkNot(mkEq(intersection, emptyTaintVector)) + + val oldTaintVectorExprTo = memory.taintVector(toAddr) + val newTaintVectorExprTo = Or(oldTaintVectorExprTo.toLongValue(), intersection.toLongValue()) + + val taintUpdateExpr = UtIteExpression( + mkAnd(condition, hasAtLeastOneMark), + thenExpr = newTaintVectorExprTo, + elseExpr = oldTaintVectorExprTo + ) + + return updateAddr(toAddr, taintUpdateExpr) + } + + // internal + + private val emptyTaintVector = mkLong(0L) + + /** + * Returns taint vector that represents [marks]. + */ + private fun constructTaintVector(marks: TaintMarks): UtBvExpression { + val taintedLongValue = when (marks) { + TaintMarksAll -> -1L // 0b11..1111 + is TaintMarksSet -> marks.marks.fold(initial = 0L) { acc, mark -> + acc or markRegistry.idByMark(mark) + } + } + return mkLong(taintedLongValue) + } + + private fun updateAddr(addr: UtAddrExpression, updateTaintVectorExpr: UtExpression): SymbolicStateUpdate = + SymbolicStateUpdate( + memoryUpdates = MemoryUpdate( + taintArrayUpdate = persistentListOf(addr to updateTaintVectorExpr) + ) + ) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/TaintMarkRegistry.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintMarkRegistry.kt new file mode 100644 index 0000000000..2c6f00c678 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintMarkRegistry.kt @@ -0,0 +1,29 @@ +package org.utbot.taint + +import com.google.common.collect.BiMap +import com.google.common.collect.HashBiMap +import org.utbot.taint.model.TaintMark + +/** + * Conversion between the taint mark name and 64-bit taint vector (some power of 2). + * Can contain a maximum of 64 different marks. + */ +class TaintMarkRegistry { + + fun idByMark(mark: TaintMark): Long = + taintMarkToIdBiMap.getOrPut(mark) { + nextId.also { nextId *= 2 } + } + + fun containsMark(mark: TaintMark): Boolean = + mark in taintMarkToIdBiMap + + fun markById(id: Long): TaintMark = // TODO: return null? + taintMarkToIdBiMap.inverse()[id] ?: error("Unknown mark id: $id") + + // internal + + private var nextId: Long = 1 // 2 ** 0 + + private val taintMarkToIdBiMap: BiMap = HashBiMap.create() +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/TaintUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintUtil.kt new file mode 100644 index 0000000000..ca88ec1e6b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/TaintUtil.kt @@ -0,0 +1,25 @@ +package org.utbot.taint + +import org.utbot.taint.model.* + +object TaintUtil { + + /** + * Chooses base, argN or result corresponding to [this] entity. + */ + fun TaintEntity.chooseRelatedValue(base: T, args: List, result: T): T? = + when (this) { + TaintEntityThis -> base + is TaintEntityArgument -> args.elementAtOrNull(index.toInt() - 1) + TaintEntityReturn -> result + } + + /** + * Check if [this] does not contain marks. + */ + fun TaintMarks.isEmpty(): Boolean = + when (this) { + TaintMarksAll -> false + is TaintMarksSet -> marks.isEmpty() + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/YamlTaintConfigurationAdapter.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/YamlTaintConfigurationAdapter.kt new file mode 100644 index 0000000000..7fab87f974 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/YamlTaintConfigurationAdapter.kt @@ -0,0 +1,132 @@ +package org.utbot.taint + +import org.utbot.taint.model.* +import org.utbot.taint.parser.yaml.* + +/** + * Converts objects from .yaml config to model data classes. + */ +object YamlTaintConfigurationAdapter { + + fun convert(configuration: YamlTaintConfiguration) = + TaintConfiguration( + configuration.sources.map { it.convert() }, + configuration.passes.map { it.convert() }, + configuration.cleaners.map { it.convert() }, + configuration.sinks.map { it.convert() }, + ) + + // internal + + private fun YamlTaintSource.convert() = + TaintSource( + methodFqn.convert(), + addTo.convert(), + marks.convert(), + signature.convert(), + conditions.convert() + ) + + private fun YamlTaintPass.convert() = + TaintPass( + methodFqn.convert(), + getFrom.convert(), + addTo.convert(), + marks.convert(), + signature.convert(), + conditions.convert() + ) + + private fun YamlTaintCleaner.convert() = + TaintCleaner( + methodFqn.convert(), + removeFrom.convert(), + marks.convert(), + signature.convert(), + conditions.convert() + ) + + private fun YamlTaintSink.convert() = + TaintSink( + methodFqn.convert(), + check.convert(), + marks.convert(), + signature.convert(), + conditions.convert() + ) + + private fun YamlTaintSignature.convert(): TaintSignature = + when (this) { + YamlTaintSignatureAny -> TaintSignatureAny + is YamlTaintSignatureList -> TaintSignatureList( + argumentTypes.map { it.convert() } + ) + } + + private fun YamlTaintConditions.convert(): TaintCondition = + when (this) { + YamlNoTaintConditions -> ConditionTrue + is YamlTaintConditionsMap -> ConditionAnd( + entityToCondition.map { (entity, condition) -> + condition.convert(entity) + } + ) + } + + private fun YamlTaintCondition.convert(entity: YamlTaintEntity): TaintCondition = + when (this) { + is YamlTaintConditionEqualValue -> { + ConditionEqualValue(entity.convert(), argumentValue.convert()) + } + is YamlTaintConditionIsType -> { + ConditionIsType(entity.convert(), argumentType.convert()) + } + is YamlTaintConditionNot -> { + ConditionNot(inner.convert(entity)) + } + is YamlTaintConditionOr -> { + ConditionOr(inners.map { it.convert(entity) }) + } + } + + private fun YamlMethodFqn.convert() = + MethodFqnValue(packageNames, className, methodName) + + private fun YamlTaintEntities.convert(): TaintEntities = + when (this) { + is YamlTaintEntitiesSet -> { + TaintEntitiesSet(entities.map { it.convert() }.toSet()) + } + } + + private fun YamlTaintMarks.convert(): TaintMarks = + when (this) { + YamlTaintMarksAll -> TaintMarksAll + is YamlTaintMarksSet -> TaintMarksSet(marks.map { it.convert() }.toSet()) + } + + private fun YamlTaintEntity.convert(): TaintEntity = + when (this) { + is YamlTaintEntityArgument -> TaintEntityArgument(index) + YamlTaintEntityReturn -> TaintEntityReturn + YamlTaintEntityThis -> TaintEntityThis + } + + private fun YamlTaintMark.convert() = + TaintMark(name) + + private fun YamlArgumentType.convert(): ArgumentType = + when (this) { + YamlArgumentTypeAny -> ArgumentTypeAny + is YamlArgumentTypeString -> ArgumentTypeString(typeFqn) + } + + private fun YamlArgumentValue.convert(): ArgumentValue = + when (this) { + YamlArgumentValueNull -> ArgumentValueNull + is YamlArgumentValueBoolean -> ArgumentValueBoolean(value) + is YamlArgumentValueLong -> ArgumentValueLong(value) + is YamlArgumentValueDouble -> ArgumentValueDouble(value) + is YamlArgumentValueString -> ArgumentValueString(value) + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/model/DataClasses.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/model/DataClasses.kt new file mode 100644 index 0000000000..85166eb920 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/model/DataClasses.kt @@ -0,0 +1,47 @@ +package org.utbot.taint.model + +import kotlinx.serialization.Serializable + +// Data classes corresponding to the parsed objects. +// See [org.utbot.taint.parser] for more details. + +@Serializable sealed interface MethodFqn +@Serializable data class MethodFqnValue( + val packageNames: List, + val className: String, + val methodName: String +) : MethodFqn +@Serializable data class MethodFqnRegex(val regex: String) : MethodFqn + +@Serializable +sealed interface TaintEntities { + val entities: Collection +} +@Serializable data class TaintEntitiesSet(override val entities: Set) : TaintEntities + +@Serializable sealed interface TaintMarks +@Serializable object TaintMarksAll : TaintMarks +@Serializable data class TaintMarksSet(val marks: Set) : TaintMarks + +@Serializable sealed interface TaintEntity +@Serializable object TaintEntityThis : TaintEntity +@Serializable object TaintEntityReturn : TaintEntity +@Serializable data class TaintEntityArgument(/** one-based */ val index: UInt) : TaintEntity + +@Serializable data class TaintMark(val name: String) + +@Serializable sealed interface ArgumentValue +@Serializable object ArgumentValueNull : ArgumentValue +@Serializable data class ArgumentValueBoolean(val value: Boolean) : ArgumentValue +@Serializable data class ArgumentValueLong(val value: Long) : ArgumentValue +@Serializable data class ArgumentValueDouble(val value: Double) : ArgumentValue +@Serializable data class ArgumentValueString(val value: String) : ArgumentValue + +@Serializable sealed interface ArgumentType +@Serializable object ArgumentTypeAny : ArgumentType +@Serializable data class ArgumentTypeString(val typeFqn: String) : ArgumentType +@Serializable data class ArgumentTypeRegex(val typeFqnRegex: String) : ArgumentType + +@Serializable sealed interface TaintSignature +@Serializable object TaintSignatureAny : TaintSignature +@Serializable data class TaintSignatureList(val argumentTypes: List) : TaintSignature diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/model/SymbolicMethodData.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/model/SymbolicMethodData.kt new file mode 100644 index 0000000000..b1ecd766ff --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/model/SymbolicMethodData.kt @@ -0,0 +1,26 @@ +package org.utbot.taint.model + +import org.utbot.engine.SymbolicValue +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.taint.TaintUtil.chooseRelatedValue + +/** + * Contains symbolic values for [methodId]. + */ +data class SymbolicMethodData( + val methodId: ExecutableId, + val base: SymbolicValue?, + val args: List, + val result: SymbolicValue? +) { + /** + * Returns symbolic value (base, argN or result) corresponding to the [entity]. + */ + fun choose(entity: TaintEntity): SymbolicValue? = + entity.chooseRelatedValue(base, args, result) + + companion object { + fun constructInvalid(methodId: ExecutableId): SymbolicMethodData = + SymbolicMethodData(methodId, base = null, args = listOf(), result = null) + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/model/TaintCondition.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/model/TaintCondition.kt new file mode 100644 index 0000000000..bce7ce2e09 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/model/TaintCondition.kt @@ -0,0 +1,130 @@ +package org.utbot.taint.model + +import kotlinx.serialization.Serializable +import org.utbot.engine.* +import org.utbot.engine.pc.* +import soot.Scene + +/** + * Condition that imposed on the taint rule. It must be met to trigger the rule. + */ +@Serializable +sealed interface TaintCondition { + fun toBoolExpr(traverser: Traverser, methodData: SymbolicMethodData): UtBoolExpression +} + +/** + * Returns always true. + */ +@Serializable +object ConditionTrue : TaintCondition { + override fun toBoolExpr(traverser: Traverser, methodData: SymbolicMethodData): UtBoolExpression = + UtTrue +} + +/** + * The [entity] must be equal to [argumentValue]. + */ +@Serializable +class ConditionEqualValue( + private val entity: TaintEntity, + private val argumentValue: ArgumentValue +) : TaintCondition { + override fun toBoolExpr(traverser: Traverser, methodData: SymbolicMethodData): UtBoolExpression { + val symbolicValue = methodData.choose(entity) ?: return UtFalse + // TODO: support java.lang.Boolean, java.lang.Integer, etc. + return when (argumentValue) { + ArgumentValueNull -> { + val referenceValue = symbolicValue as? ReferenceValue + ?: return UtFalse + addrEq(referenceValue.addr, nullObjectAddr) + } + is ArgumentValueBoolean -> { + val primitiveValue = symbolicValue as? PrimitiveValue + ?: return UtFalse + mkEq(primitiveValue, mkBool(argumentValue.value).toBoolValue()) + } + is ArgumentValueLong -> { + val primitiveValue = symbolicValue as? PrimitiveValue + ?: return UtFalse + // conversions like int -> long will be made automatically by utbot + mkEq(primitiveValue, mkLong(argumentValue.value).toLongValue()) + } + is ArgumentValueDouble -> { + val primitiveValue = symbolicValue as? PrimitiveValue + ?: return UtFalse + // conversion float -> double will be made automatically by utbot + mkEq(primitiveValue, mkDouble(argumentValue.value).toDoubleValue()) + } + is ArgumentValueString -> { + val objectValue = symbolicValue as? ObjectValue + ?: return UtFalse + // TODO: compare not only length + val symbolicLength = traverser.getIntFieldValue(objectValue, STRING_LENGTH) + mkEq(symbolicLength, mkInt(argumentValue.value.length)) + } + } + } +} + +/** + * The [entity] must be [argumentType] at the runtime. + */ +@Serializable +class ConditionIsType( + private val entity: TaintEntity, + private val argumentType: ArgumentType +) : TaintCondition { + override fun toBoolExpr(traverser: Traverser, methodData: SymbolicMethodData): UtBoolExpression { + val symbolicValue = methodData.choose(entity) ?: return UtFalse + return when (argumentType) { + ArgumentTypeAny -> UtTrue + is ArgumentTypeString -> { + when (symbolicValue) { + is PrimitiveValue -> { + // If the method receives and the user calls it with the argument, + // utbot will automatically create a symbolic value with the type, + // so there is no need to handle conversions like int -> long here. + val argumentType = Scene.v().getTypeUnsafe(argumentType.typeFqn) + mkBool(symbolicValue.type == argumentType) + } + is ReferenceValue -> { + val argumentRefType = Scene.v().getRefTypeUnsafe(argumentType.typeFqn) + ?: return UtFalse + val typeStorage = traverser.typeResolver.constructTypeStorage(argumentRefType, useConcreteType = false) + // not `.isConstraintOrNull()` because null objects are not allowed (like java `instanceof`) + traverser.typeRegistry.typeConstraint(symbolicValue.addr, typeStorage).isConstraint() + } + } + } + is ArgumentTypeRegex -> UtTrue // TODO: support regex type constraints + } + } +} + +/** + * Negates [inner]. + */ +@Serializable +class ConditionNot(private val inner: TaintCondition) : TaintCondition { + override fun toBoolExpr(traverser: Traverser, methodData: SymbolicMethodData): UtBoolExpression = + mkNot(inner.toBoolExpr(traverser, methodData)) +} + +/** + * Combines [inners] with `or` operator. + */ +@Serializable +class ConditionOr(private val inners: List) : TaintCondition { + override fun toBoolExpr(traverser: Traverser, methodData: SymbolicMethodData): UtBoolExpression = + mkOr(inners.map { it.toBoolExpr(traverser, methodData) }) +} + +/** + * Combines [inners] with `and` operator. + */ +@Serializable +class ConditionAnd(private val inners: List) : TaintCondition { + override fun toBoolExpr(traverser: Traverser, methodData: SymbolicMethodData): UtBoolExpression = + mkAnd(inners.map { it.toBoolExpr(traverser, methodData) }) +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/model/TaintConfiguration.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/model/TaintConfiguration.kt new file mode 100644 index 0000000000..5ed0e8478a --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/model/TaintConfiguration.kt @@ -0,0 +1,155 @@ +package org.utbot.taint.model + +import kotlinx.serialization.Serializable +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId + +/** + * Storage for all taint rules. + */ +@Serializable +data class TaintConfiguration( + private val sources: List = listOf(), + private val passes: List = listOf(), + private val cleaners: List = listOf(), + private val sinks: List = listOf(), +) { + + fun getSourcesBy(executableId: ExecutableId): List = + sources.filter { + equalParameters(executableId, it.signature) && equalMethodNames(executableId, it.methodFqn) + } + + fun getPassesBy(executableId: ExecutableId): List = + passes.filter { + equalParameters(executableId, it.signature) && equalMethodNames(executableId, it.methodFqn) + } + + fun getCleanersBy(executableId: ExecutableId): List = + cleaners.filter { + equalParameters(executableId, it.signature) && equalMethodNames(executableId, it.methodFqn) + } + + fun getSinksBy(executableId: ExecutableId): List = + sinks.filter { + equalParameters(executableId, it.signature) && equalMethodNames(executableId, it.methodFqn) + } + + operator fun plus(other: TaintConfiguration): TaintConfiguration = + TaintConfiguration( + sources = sources + other.sources, + passes = passes + other.passes, + cleaners = cleaners + other.cleaners, + sinks = sinks + other.sinks + ) + + // internal + + private fun equalParameters(executableId: ExecutableId, signature: TaintSignature): Boolean = + when (signature) { + is TaintSignatureAny -> true + is TaintSignatureList -> { + val equalSizes = signature.argumentTypes.size == executableId.parameters.size + val equalElementwise = (signature.argumentTypes zip executableId.parameters).all { (type, parameter) -> + equalTypes(parameter, type) + } + equalSizes && equalElementwise + } + } + + private fun equalTypes(classId: ClassId, argumentType: ArgumentType): Boolean = + when (argumentType) { + ArgumentTypeAny -> true + is ArgumentTypeString -> argumentType.typeFqn == classId.name + // TODO: constructing Regex many times can lead to a decrease in performance + is ArgumentTypeRegex -> classId.name.matches(Regex(argumentType.typeFqnRegex)) + } + + private fun equalMethodNames(executableId: ExecutableId, methodFqn: MethodFqn): Boolean = + when (methodFqn) { + is MethodFqnRegex -> { + val executableFqn = executableId.getMethodFqn()?.run { + packageNames.plus(className).plus(methodName).joinToString(".") + } + // TODO: constructing Regex many times can lead to a decrease in performance + executableFqn?.matches(Regex(methodFqn.regex)) ?: false + } + is MethodFqnValue -> methodFqn == executableId.getMethodFqn() + } + + private fun ExecutableId.getMethodFqn(): MethodFqnValue? = + runCatching { + val packages = if (classId.packageName == "") + listOf() + else + classId.packageName.split('.') + MethodFqnValue(packages, classId.simpleName, name) + }.getOrNull() +} + +/** + * @param methodFqn method fully qualified name + * @param addTo objects to be marked + * @param marks marks that should be added to the objects from the [addTo] list + * @param signature list of argument types (at compile time) + * @param condition condition that must be met to trigger this rule + */ +@Serializable +data class TaintSource( + val methodFqn: MethodFqn, + val addTo: TaintEntities, + val marks: TaintMarks, + val signature: TaintSignature, + val condition: TaintCondition, +) + +/** + * @param methodFqn method fully qualified name + * @param getFrom the sources of marks + * @param addTo objects that will be marked if any element from [getFrom] has any mark + * @param marks actual marks that can be passed from one object to another + * @param signature list of argument types (at compile time) + * @param condition condition that must be met to trigger this rule + */ +@Serializable +data class TaintPass( + val methodFqn: MethodFqn, + val getFrom: TaintEntities, + val addTo: TaintEntities, + val marks: TaintMarks, + val signature: TaintSignature, + val condition: TaintCondition, +) + +/** + * @param methodFqn method fully qualified name + * @param removeFrom objects from which marks should be removed + * @param marks marks to be removed + * @param signature list of argument types (at compile time) + * @param condition condition that must be met to trigger this rule + */ +@Serializable +data class TaintCleaner( + val methodFqn: MethodFqn, + val removeFrom: TaintEntities, + val marks: TaintMarks, + val signature: TaintSignature, + val condition: TaintCondition, +) + +/** + * @param methodFqn method fully qualified name + * @param check objects that will be checked for marks + * @param marks when one of the [marks] is found in one of the objects from the [check], + * the analysis will report the problem found + * @param signature list of argument types (at compile time) + * @param condition condition that must be met to trigger this rule + */ +@Serializable +data class TaintSink( + val methodFqn: MethodFqn, + val check: TaintEntities, + val marks: TaintMarks, + val signature: TaintSignature, + val condition: TaintCondition, +) \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/Constants.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/Constants.kt new file mode 100644 index 0000000000..b177c97ca1 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/Constants.kt @@ -0,0 +1,30 @@ +package org.utbot.taint.parser.yaml + +/** + * String constants in YAML configuration file. + */ +object Constants { + const val KEY_SOURCES = "sources" + const val KEY_PASSES = "passes" + const val KEY_CLEANERS = "cleaners" + const val KEY_SINKS = "sinks" + + const val KEY_ADD_TO = "add-to" + const val KEY_GET_FROM = "get-from" + const val KEY_REMOVE_FROM = "remove-from" + const val KEY_CHECK = "check" + const val KEY_MARKS = "marks" + + const val KEY_SIGNATURE = "signature" + const val KEY_CONDITIONS = "conditions" + + const val KEY_CONDITION_NOT = "not" + + const val KEY_THIS = "this" + const val KEY_RETURN = "return" + const val KEY_ARG = "arg" + + const val ARGUMENT_TYPE_PREFIX = "<" + const val ARGUMENT_TYPE_SUFFIX = ">" + const val ARGUMENT_TYPE_UNDERSCORE = "_" +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/MethodArgumentParser.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/MethodArgumentParser.kt new file mode 100644 index 0000000000..90ff757ad2 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/MethodArgumentParser.kt @@ -0,0 +1,76 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.YamlNode +import com.charleskorn.kaml.YamlNull +import com.charleskorn.kaml.YamlScalar +import com.charleskorn.kaml.YamlScalarFormatException +import org.utbot.taint.parser.yaml.Constants.ARGUMENT_TYPE_PREFIX +import org.utbot.taint.parser.yaml.Constants.ARGUMENT_TYPE_SUFFIX +import org.utbot.taint.parser.yaml.Constants.ARGUMENT_TYPE_UNDERSCORE +import kotlin.contracts.ExperimentalContracts + +@OptIn(ExperimentalContracts::class) +object MethodArgumentParser { + + /** + * This method is expected to be called only if the [isArgumentType] method returned `true`. + */ + fun parseArgumentType(node: YamlNode): YamlArgumentType { + validate(node is YamlScalar, "The argument type node should be a scalar", node) + return when (val typeDescription = node.content) { + ARGUMENT_TYPE_UNDERSCORE -> YamlArgumentTypeAny + else -> { + val typeFqn = typeDescription.removeSurrounding(ARGUMENT_TYPE_PREFIX, ARGUMENT_TYPE_SUFFIX) + YamlArgumentTypeString(typeFqn) + } + } + } + + /** + * This method is expected to be called only if the [isArgumentValue] method returned `true`. + */ + fun parseArgumentValue(node: YamlNode): YamlArgumentValue { + return when (node) { + is YamlNull -> YamlArgumentValueNull + is YamlScalar -> { + val conversions: List<(YamlScalar) -> YamlArgumentValue> = listOf( + { YamlArgumentValueBoolean(it.toBoolean()) }, + { YamlArgumentValueLong(it.toLong()) }, + { YamlArgumentValueDouble(it.toDouble()) }, + { YamlArgumentValueString(it.content) }, + ) + + for (conversion in conversions) { + try { + return conversion(node) + } catch (_: YamlScalarFormatException) { + continue + } + } + throw TaintParseError("All conversions failed for the argument value node", node) + } + + else -> { + throw TaintParseError("The argument value node should be a null or a scalar", node) + } + } + } + + /** + * Checks that the [node] can be parsed to [YamlArgumentType]. + */ + fun isArgumentType(node: YamlNode): Boolean { + val content = (node as? YamlScalar)?.content ?: return false + + val isUnderscore = content == ARGUMENT_TYPE_UNDERSCORE + val isInBrackets = content.startsWith(ARGUMENT_TYPE_PREFIX) && content.endsWith(ARGUMENT_TYPE_SUFFIX) + + return isUnderscore || isInBrackets + } + + /** + * Checks that the [node] can be parsed to [YamlArgumentValue]. + */ + fun isArgumentValue(node: YamlNode): Boolean = + (node is YamlNull) || (node is YamlScalar) && !isArgumentType(node) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/Model.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/Model.kt new file mode 100644 index 0000000000..95b5953668 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/Model.kt @@ -0,0 +1,90 @@ +package org.utbot.taint.parser.yaml + +/** + * See "docs/TaintAnalysis.md" for configuration format. + */ +data class YamlTaintConfiguration( + val sources: List, + val passes: List, + val cleaners: List, + val sinks: List +) + +data class YamlTaintSource( + val methodFqn: YamlMethodFqn, + val addTo: YamlTaintEntities, + val marks: YamlTaintMarks, + val signature: YamlTaintSignature = YamlTaintSignatureAny, + val conditions: YamlTaintConditions = YamlNoTaintConditions, +) + +data class YamlTaintPass( + val methodFqn: YamlMethodFqn, + val getFrom: YamlTaintEntities, + val addTo: YamlTaintEntities, + val marks: YamlTaintMarks, + val signature: YamlTaintSignature = YamlTaintSignatureAny, + val conditions: YamlTaintConditions = YamlNoTaintConditions, +) + +data class YamlTaintCleaner( + val methodFqn: YamlMethodFqn, + val removeFrom: YamlTaintEntities, + val marks: YamlTaintMarks, + val signature: YamlTaintSignature = YamlTaintSignatureAny, + val conditions: YamlTaintConditions = YamlNoTaintConditions, +) + +data class YamlTaintSink( + val methodFqn: YamlMethodFqn, + val check: YamlTaintEntities, + val marks: YamlTaintMarks, + val signature: YamlTaintSignature = YamlTaintSignatureAny, + val conditions: YamlTaintConditions = YamlNoTaintConditions, +) + +data class YamlMethodFqn( + val packageNames: List, + val className: String, + val methodName: String +) + +sealed interface YamlTaintEntities +data class YamlTaintEntitiesSet(val entities: Set) : YamlTaintEntities + +sealed interface YamlTaintMarks +object YamlTaintMarksAll : YamlTaintMarks +data class YamlTaintMarksSet(val marks: Set) : YamlTaintMarks + +sealed interface YamlTaintSignature +object YamlTaintSignatureAny : YamlTaintSignature +data class YamlTaintSignatureList(val argumentTypes: List) : YamlTaintSignature + +sealed interface YamlTaintConditions +object YamlNoTaintConditions : YamlTaintConditions +data class YamlTaintConditionsMap(val entityToCondition: Map) : YamlTaintConditions + +sealed interface YamlTaintEntity +object YamlTaintEntityThis : YamlTaintEntity +data class YamlTaintEntityArgument(/** one-based */ val index: UInt) : YamlTaintEntity + +object YamlTaintEntityReturn : YamlTaintEntity + +data class YamlTaintMark(val name: String) + +sealed interface YamlTaintCondition +data class YamlTaintConditionEqualValue(val argumentValue: YamlArgumentValue) : YamlTaintCondition +data class YamlTaintConditionIsType(val argumentType: YamlArgumentType) : YamlTaintCondition +data class YamlTaintConditionNot(val inner: YamlTaintCondition) : YamlTaintCondition +data class YamlTaintConditionOr(val inners: List) : YamlTaintCondition + +sealed interface YamlArgumentValue +object YamlArgumentValueNull : YamlArgumentValue +data class YamlArgumentValueBoolean(val value: Boolean) : YamlArgumentValue +data class YamlArgumentValueLong(val value: Long) : YamlArgumentValue +data class YamlArgumentValueDouble(val value: Double) : YamlArgumentValue +data class YamlArgumentValueString(val value: String) : YamlArgumentValue + +sealed interface YamlArgumentType +object YamlArgumentTypeAny : YamlArgumentType +data class YamlArgumentTypeString(val typeFqn: String) : YamlArgumentType diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintConditionParser.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintConditionParser.kt new file mode 100644 index 0000000000..9e88e3cfe6 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintConditionParser.kt @@ -0,0 +1,92 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.* +import org.utbot.taint.parser.yaml.MethodArgumentParser.isArgumentType +import org.utbot.taint.parser.yaml.MethodArgumentParser.isArgumentValue +import org.utbot.taint.parser.yaml.MethodArgumentParser.parseArgumentType +import org.utbot.taint.parser.yaml.MethodArgumentParser.parseArgumentValue +import org.utbot.taint.parser.yaml.TaintEntityParser.taintEntityByName +import kotlin.contracts.ExperimentalContracts + +@OptIn(ExperimentalContracts::class) +object TaintConditionParser { + + /** + * Expects a [YamlMap] with (or without) a key [Constants.KEY_CONDITIONS]. + * + * __Input example:__ + * + * ```yaml + * conditions: + * this: + * arg1: [ "in", "out" ] + * arg2: 227 + * return: [ "", { not: } ] + * ``` + */ + fun parseConditionsKey(ruleMap: YamlMap): YamlTaintConditions = + ruleMap.get(Constants.KEY_CONDITIONS)?.let(TaintConditionParser::parseConditions) + ?: YamlNoTaintConditions + + /** + * Expects a [YamlMap] with taint entities as keys. + * + * __Input example:__ + * + * ```yaml + * this: + * arg1: [ "in", "out" ] + * arg2: 227 + * return: [ "", { not: } ] + * ``` + */ + fun parseConditions(node: YamlNode): YamlTaintConditions { + validate(node is YamlMap, "The conditions node should be a map", node) + return if (node.entries.isEmpty()) { + YamlNoTaintConditions + } else { + val entityToCondition = node.entries.map { (taintEntityNameScalar, conditionNode) -> + taintEntityByName(taintEntityNameScalar.content) to parseCondition(conditionNode) + }.toMap() + YamlTaintConditionsMap(entityToCondition) + } + } + + /** + * Expects a [YamlNode] that describes one condition. + */ + fun parseCondition(node: YamlNode): YamlTaintCondition = + when (node) { + // example: `null` + is YamlNull -> { + YamlTaintConditionEqualValue(parseArgumentValue(node)) + } + + // examples: `227`, `"some string"`, `` + is YamlScalar -> { + when { + isArgumentType(node) -> YamlTaintConditionIsType(parseArgumentType(node)) + isArgumentValue(node) -> YamlTaintConditionEqualValue(parseArgumentValue(node)) + else -> throw TaintParseError("The condition scalar should be a type or a value", node) + } + } + + // examples: `[ true, 1, "yes" ]`, `[ "", { not: } ]` + is YamlList -> { + val innerConditions = node.items.map(TaintConditionParser::parseCondition) + YamlTaintConditionOr(innerConditions) + } + + // examples: `{ not: null }`, `{ not: [1, 2, 3] }` + is YamlMap -> { + validateYamlMapKeys(node, setOf(Constants.KEY_CONDITION_NOT)) + val innerNode = node.get(Constants.KEY_CONDITION_NOT)!! + val innerCondition = parseCondition(innerNode) + YamlTaintConditionNot(innerCondition) + } + + else -> { + throw TaintParseError("The condition node has an unknown type", node) + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintConfigurationParser.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintConfigurationParser.kt new file mode 100644 index 0000000000..bddd1ddfe7 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintConfigurationParser.kt @@ -0,0 +1,42 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.YamlMap +import com.charleskorn.kaml.YamlNode +import org.utbot.taint.parser.yaml.Constants.KEY_CLEANERS +import org.utbot.taint.parser.yaml.Constants.KEY_PASSES +import org.utbot.taint.parser.yaml.Constants.KEY_SINKS +import org.utbot.taint.parser.yaml.Constants.KEY_SOURCES +import kotlin.contracts.ExperimentalContracts + +@OptIn(ExperimentalContracts::class) +object TaintConfigurationParser { + + /** + * Expects a [YamlMap] with keys [KEY_SOURCES], [KEY_PASSES], [KEY_CLEANERS] and [KEY_SINKS]. + * + * __Input example:__ + * + * ```yaml + * sources: [ ... ] + * passes: [ ... ] + * cleaners: [ ... ] + * sinks: [ ... ] + * ``` + */ + fun parseConfiguration(node: YamlNode): YamlTaintConfiguration { + validate(node is YamlMap, "The root node should be a map", node) + validateYamlMapKeys(node, setOf(KEY_SOURCES, KEY_PASSES, KEY_CLEANERS, KEY_SINKS)) + + val sourcesNode = node.get(KEY_SOURCES) + val passesNode = node.get(KEY_PASSES) + val cleanersNode = node.get(KEY_CLEANERS) + val sinksNode = node.get(KEY_SINKS) + + val sources = sourcesNode?.let(TaintRuleParser::parseSources) ?: listOf() + val passes = passesNode?.let(TaintRuleParser::parsePasses) ?: listOf() + val cleaners = cleanersNode?.let(TaintRuleParser::parseCleaners) ?: listOf() + val sinks = sinksNode?.let(TaintRuleParser::parseSinks) ?: listOf() + + return YamlTaintConfiguration(sources, passes, cleaners, sinks) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintEntityParser.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintEntityParser.kt new file mode 100644 index 0000000000..a0d8866325 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintEntityParser.kt @@ -0,0 +1,52 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.YamlList +import com.charleskorn.kaml.YamlNode +import com.charleskorn.kaml.YamlScalar +import kotlin.contracts.ExperimentalContracts + +@OptIn(ExperimentalContracts::class) +object TaintEntityParser { + + /** + * Expects a [YamlScalar] (single taint entity) or a [YamlList] (several taint entities). + * + * __Input example:__ + * + * `[ this, arg1, arg7, return ]` + */ + fun parseTaintEntities(node: YamlNode): YamlTaintEntities = + when (node) { + is YamlScalar -> { + YamlTaintEntitiesSet(setOf(taintEntityByName(node.content))) + } + + is YamlList -> { + validate(node.items.isNotEmpty(), "The taint entities set should contain at least one value", node) + val entities = node.items.map { innerNode -> + validate(innerNode is YamlScalar, "The taint entity name should be a scalar", node) + taintEntityByName(innerNode.content) + } + YamlTaintEntitiesSet(entities.toSet()) + } + + else -> { + throw TaintParseError("The taint-entities node should be a scalar or a list", node) + } + } + + /** + * Constructs [YamlTaintEntity] by the given [name] &ndash "this", "return" or "argN". + */ + fun taintEntityByName(name: String): YamlTaintEntity = + when (name) { + Constants.KEY_THIS -> YamlTaintEntityThis + Constants.KEY_RETURN -> YamlTaintEntityReturn + else -> { + val index = name.removePrefix(Constants.KEY_ARG).toUIntOrNull() + ?: throw TaintParseError("Method argument should be like `arg` + index, but is `$name`") + validate(index >= 1u, "Method arguments indexes are numbered from one, but index = `$index`") + YamlTaintEntityArgument(index) + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintMarkParser.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintMarkParser.kt new file mode 100644 index 0000000000..ec5449826e --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintMarkParser.kt @@ -0,0 +1,40 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.YamlList +import com.charleskorn.kaml.YamlNode +import com.charleskorn.kaml.YamlScalar +import kotlin.contracts.ExperimentalContracts + +@OptIn(ExperimentalContracts::class) +object TaintMarkParser { + + /** + * Expects a [YamlScalar] (single mark) or a [YamlList] (several marks). + * + * __Input example:__ + * + * `[ sensitive-data, xss ]` + */ + fun parseTaintMarks(node: YamlNode): YamlTaintMarks = + when (node) { + is YamlScalar -> { + YamlTaintMarksSet(setOf(YamlTaintMark(node.content))) + } + + is YamlList -> { + if (node.items.isEmpty()) { + YamlTaintMarksAll + } else { + val marks = node.items.map { innerNode -> + validate(innerNode is YamlScalar, "The mark name should be a scalar", innerNode) + YamlTaintMark(innerNode.content) + } + YamlTaintMarksSet(marks.toSet()) + } + } + + else -> { + throw TaintParseError("The marks node should be a scalar or a list", node) + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintRuleParser.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintRuleParser.kt new file mode 100644 index 0000000000..6909c32eaf --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintRuleParser.kt @@ -0,0 +1,242 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.YamlList +import com.charleskorn.kaml.YamlMap +import com.charleskorn.kaml.YamlNode +import org.utbot.taint.parser.yaml.TaintConditionParser.parseConditionsKey +import org.utbot.taint.parser.yaml.TaintEntityParser.parseTaintEntities +import org.utbot.taint.parser.yaml.TaintMarkParser.parseTaintMarks +import org.utbot.taint.parser.yaml.TaintSignatureParser.parseSignatureKey +import kotlin.contracts.ExperimentalContracts + +@OptIn(ExperimentalContracts::class) +object TaintRuleParser { + + /** + * Recursively parses source rules. + * @see parseRules + */ + fun parseSources(node: YamlNode): List = + parseRules(node, TaintRuleParser::isSourceRule, TaintRuleParser::parseSourceRule) + + /** + * Recursively parses pass-through rules. + * @see parseRules + */ + fun parsePasses(node: YamlNode): List = + parseRules(node, TaintRuleParser::isPassRule, TaintRuleParser::parsePassRule) + + /** + * Recursively parses cleaner rules. + * @see parseRules + */ + fun parseCleaners(node: YamlNode): List = + parseRules(node, TaintRuleParser::isCleanerRule, TaintRuleParser::parseCleanerRule) + + /** + * Recursively parses sink rules. + * @see parseRules + */ + fun parseSinks(node: YamlNode): List = + parseRules(node, TaintRuleParser::isSinkRule, TaintRuleParser::parseSinkRule) + + /** + * Recursively parses rules (sources/passes/cleaners/sinks). + * + * Expects a [YamlMap] (single rule) or a YamlList (several rules). + * + * __Input example:__ + * + * ```yaml + * - java.lang.String: + * - isEmpty: { ... } + * - concat: { ... } + * ``` + */ + private fun parseRules( + node: YamlNode, + isRule: YamlNode.() -> Boolean, + parseRule: (YamlNode, List) -> Rule, + currentPath: List = listOf() + ): List { + if (node.isRule()) { + val rule = parseRule(node, currentPath) + return listOf(rule) + } + + validate(node is YamlList, "The rules should be stored as a list", node) + return node.items.flatMap { innerNode -> + validate(innerNode is YamlMap, "The rule node should be a map with the key `method name part`", innerNode) + validate(innerNode.entries.size == 1, "The rule map should contain only one key", innerNode) + val (nextNamePart, nextNode) = innerNode.entries.toList().first() + parseRules(nextNode, isRule, parseRule, currentPath = currentPath + nextNamePart.content) + } + } + + + /** + * Checks that the [node] can be parsed to [YamlTaintSource]. + */ + fun isSourceRule(node: YamlNode): Boolean = + node.containsKey(Constants.KEY_ADD_TO) && node.containsKey(Constants.KEY_MARKS) + + /** + * Checks that the [node] can be parsed to [YamlTaintPass]. + */ + fun isPassRule(node: YamlNode): Boolean = + node.containsKey(Constants.KEY_GET_FROM) && node.containsKey(Constants.KEY_ADD_TO) && node.containsKey(Constants.KEY_MARKS) + + /** + * Checks that the [node] can be parsed to [YamlTaintCleaner]. + */ + fun isCleanerRule(node: YamlNode): Boolean = + node.containsKey(Constants.KEY_REMOVE_FROM) && node.containsKey(Constants.KEY_MARKS) + + /** + * Checks that the [node] can be parsed to [YamlTaintSink]. + */ + fun isSinkRule(node: YamlNode): Boolean = + node.containsKey(Constants.KEY_CHECK) && node.containsKey(Constants.KEY_MARKS) + + /** + * Checks that [this] is [YamlMap] and contains [key] as a key. + */ + private fun YamlNode.containsKey(key: String): Boolean = + (this as? YamlMap)?.get(key) != null + + + /** + * This method is expected to be called only if the [isSourceRule] method returned `true`. + * + * __Input example:__ + * + * ```yaml + * signature: [ ... ] + * conditions: [ ... ] + * add-to: ... + * marks: ... + * ``` + */ + fun parseSourceRule(node: YamlNode, methodNameParts: List): YamlTaintSource { + validate(node is YamlMap, "The source-rule node should be a map", node) + validateYamlMapKeys( + node, + setOf(Constants.KEY_SIGNATURE, Constants.KEY_CONDITIONS, Constants.KEY_ADD_TO, Constants.KEY_MARKS) + ) + + val methodFqn = getMethodFqnFromParts(methodNameParts) + val signature = parseSignatureKey(node) + val conditions = parseConditionsKey(node) + val addTo = parseTaintEntities(node[Constants.KEY_ADD_TO]!!) + val marks = parseTaintMarks(node[Constants.KEY_MARKS]!!) + + return YamlTaintSource(methodFqn, addTo, marks, signature, conditions) + } + + /** + * This method is expected to be called only if the [isPassRule] method returned `true`. + * + * __Input example:__ + * + * ```yaml + * signature: [ ... ] + * conditions: [ ... ] + * get-from: [ ... ] + * add-to: ... + * marks: ... + * ``` + */ + fun parsePassRule(node: YamlNode, methodNameParts: List): YamlTaintPass { + validate(node is YamlMap, "The pass-rule node should be a map", node) + validateYamlMapKeys( + node, + setOf( + Constants.KEY_SIGNATURE, + Constants.KEY_CONDITIONS, + Constants.KEY_GET_FROM, + Constants.KEY_ADD_TO, + Constants.KEY_MARKS + ) + ) + + val methodFqn = getMethodFqnFromParts(methodNameParts) + val signature = parseSignatureKey(node) + val conditions = parseConditionsKey(node) + val getFrom = parseTaintEntities(node[Constants.KEY_GET_FROM]!!) + val addTo = parseTaintEntities(node[Constants.KEY_ADD_TO]!!) + val marks = parseTaintMarks(node[Constants.KEY_MARKS]!!) + + return YamlTaintPass(methodFqn, getFrom, addTo, marks, signature, conditions) + } + + /** + * This method is expected to be called only if the [isCleanerRule] method returned `true`. + * + * __Input example:__ + * + * ```yaml + * signature: [ ... ] + * conditions: [ ... ] + * remove-from: ... + * marks: ... + * ``` + */ + fun parseCleanerRule(node: YamlNode, methodNameParts: List): YamlTaintCleaner { + validate(node is YamlMap, "The cleaner-rule node should be a map", node) + validateYamlMapKeys( + node, + setOf(Constants.KEY_SIGNATURE, Constants.KEY_CONDITIONS, Constants.KEY_REMOVE_FROM, Constants.KEY_MARKS) + ) + + val methodFqn = getMethodFqnFromParts(methodNameParts) + val signature = parseSignatureKey(node) + val conditions = parseConditionsKey(node) + val removeFrom = parseTaintEntities(node[Constants.KEY_REMOVE_FROM]!!) + val marks = parseTaintMarks(node[Constants.KEY_MARKS]!!) + + return YamlTaintCleaner(methodFqn, removeFrom, marks, signature, conditions) + } + + /** + * This method is expected to be called only if the [isSinkRule] method returned `true`. + * + * __Input example:__ + * + * ```yaml + * signature: [ ... ] + * conditions: [ ... ] + * check: ... + * marks: ... + * ``` + */ + fun parseSinkRule(node: YamlNode, methodNameParts: List): YamlTaintSink { + validate(node is YamlMap, "The sink-rule node should be a map", node) + validateYamlMapKeys( + node, + setOf(Constants.KEY_SIGNATURE, Constants.KEY_CONDITIONS, Constants.KEY_CHECK, Constants.KEY_MARKS) + ) + + val methodFqn = getMethodFqnFromParts(methodNameParts) + val signature = parseSignatureKey(node) + val conditions = parseConditionsKey(node) + val check = parseTaintEntities(node[Constants.KEY_CHECK]!!) + val marks = parseTaintMarks(node[Constants.KEY_MARKS]!!) + + return YamlTaintSink(methodFqn, check, marks, signature, conditions) + } + + /** + * Constructs [YamlMethodFqn] from the given [methodNameParts]. + * + * __Input example:__ + * + * `["org.example", "server", "Controller", "start"]` + */ + private fun getMethodFqnFromParts(methodNameParts: List): YamlMethodFqn { + val parts = methodNameParts.flatMap { it.split('.') } + validate(parts.size >= 2, "Method fqn should contain at least the class name and the method name") + val packageNames = parts.dropLast(2) + val (className, methodName) = parts.takeLast(2) + return YamlMethodFqn(packageNames, className, methodName) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintSignatureParser.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintSignatureParser.kt new file mode 100644 index 0000000000..8e5b082f4b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintSignatureParser.kt @@ -0,0 +1,37 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.YamlList +import com.charleskorn.kaml.YamlMap +import com.charleskorn.kaml.YamlNode +import org.utbot.taint.parser.yaml.MethodArgumentParser.isArgumentType +import org.utbot.taint.parser.yaml.MethodArgumentParser.parseArgumentType +import kotlin.contracts.ExperimentalContracts + +@OptIn(ExperimentalContracts::class) +object TaintSignatureParser { + + /** + * Expects a [YamlMap] with (or without) a key [Constants.KEY_SIGNATURE]. + * + * __Input example:__ + * + * `signature: [ _, _, ]` + */ + fun parseSignatureKey(ruleMap: YamlMap): YamlTaintSignature = + ruleMap.get(Constants.KEY_SIGNATURE)?.let(TaintSignatureParser::parseSignature) + ?: YamlTaintSignatureAny + + /** + * Expects a [YamlList] with argument types as keys. + * + * __Input example:__ + * + * `[ _, _, ]` + */ + fun parseSignature(node: YamlNode): YamlTaintSignature { + validate(node is YamlList, "The signature node should be a list", node) + validate(node.items.all(::isArgumentType), "All items should be argument types", node) + val argumentTypes = node.items.map(::parseArgumentType) + return YamlTaintSignatureList(argumentTypes) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintYamlParser.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintYamlParser.kt new file mode 100644 index 0000000000..ed8ef9faba --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/TaintYamlParser.kt @@ -0,0 +1,20 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.Yaml +import com.charleskorn.kaml.YamlException + +/** + * YAML configuration file parser. + */ +object TaintYamlParser { + /** + * Parses the YAML configuration file to our data classes. + * + * @throws YamlException + * @throws TaintParseError + */ + fun parse(yamlInput: String): YamlTaintConfiguration { + val rootNode = Yaml.default.parseToYamlNode(yamlInput) + return TaintConfigurationParser.parseConfiguration(rootNode) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/Validation.kt b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/Validation.kt new file mode 100644 index 0000000000..4595dd9b31 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/taint/parser/yaml/Validation.kt @@ -0,0 +1,34 @@ +package org.utbot.taint.parser.yaml + +import com.charleskorn.kaml.YamlMap +import com.charleskorn.kaml.YamlNode +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +class TaintParseError( + message: String, + node: YamlNode? = null +) : RuntimeException(message + (if (node != null) "\nYaml node: ${node.contentToString()}" else "")) + +@ExperimentalContracts +fun validate(condition: Boolean, reason: String, node: YamlNode? = null) { + contract { + returns() implies condition + } + if (!condition) { + throw TaintParseError(reason, node) + } +} + +/** + * Validates that the [node] contains keys only from [allowedKeys]. + * + * Does not require the presence of all [allowedKeys] in the [node]. + */ +fun validateYamlMapKeys(node: YamlMap, allowedKeys: Set) { + val actualKeys = node.entries.keys.map { it.content }.toSet() + val unknownKeys = actualKeys - allowedKeys + if (unknownKeys.isNotEmpty()) { + throw TaintParseError("Unknown keys was encountered: $unknownKeys", node) + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CodeGenerationIntegrationTest.kt b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CodeGenerationIntegrationTest.kt deleted file mode 100644 index cdc4dce755..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CodeGenerationIntegrationTest.kt +++ /dev/null @@ -1,194 +0,0 @@ -package org.utbot.tests.infrastructure - -import org.utbot.common.FileUtil -import org.utbot.common.withAccessibility -import org.utbot.framework.UtSettings -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.UtMethodTestSet -import org.utbot.instrumentation.ConcreteExecutor -import kotlin.reflect.KClass -import mu.KotlinLogging -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.MethodOrderer -import org.junit.jupiter.api.Order -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInfo -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.TestMethodOrder -import org.junit.jupiter.api.extension.BeforeAllCallback -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.api.fail -import org.junit.jupiter.engine.descriptor.ClassTestDescriptor -import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor -import java.nio.file.Path - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -@ExtendWith(CodeGenerationIntegrationTest.Companion.ReadRunningTestsNumberBeforeAllTestsCallback::class) -abstract class CodeGenerationIntegrationTest( - private val testClass: KClass<*>, - private var testCodeGeneration: Boolean = true, - private val languagesLastStages: List = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN) - ) -) { - private val testSets: MutableList = arrayListOf() - - data class CodeGenerationLanguageLastStage(val language: CodegenLanguage, val lastStage: Stage = TestExecution) - - fun processTestCase(testSet: UtMethodTestSet) { - if (testCodeGeneration) testSets += testSet - } - - 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) { - packageResult.getOrPut(pkg) { mutableListOf() } += CodeGenerationTestCases( - testClass, - testSets, - languagesLastStages - ) - } - } - - private fun cleanAfterProcessingPackage(pkg: Package) { - // clean test cases after cur package processing - packageResult[pkg]?.clear() - - // clean cur package test classes info - testClassesAmountByPackage[pkg] = 0 - processedTestClassesAmountByPackage[pkg] = 0 - } - - @Test - @Order(Int.MAX_VALUE) - fun processTestCases(testInfo: TestInfo) { - val pkg = testInfo.testClass.get().`package` - addTestCase(pkg) - - // check if tests are inside package and current test is not the last one - if (runningTestsNumber > 1 && !isPackageFullyProcessed(testInfo.testClass.get())) { - logger.info("Package $pkg is not fully processed yet, code generation will be tested later") - return - } - ConcreteExecutor.defaultPool.close() - - FileUtil.clearTempDirectory(UtSettings.daysLimitForTempFiles) - - val result = packageResult[pkg] ?: return - try { - val pipelineErrors = mutableListOf() - languages.map { language -> - try { - // choose all test cases that should be tested with current language - val resultsWithCurLanguage = result.filter { codeGenerationTestCases -> - codeGenerationTestCases.languagePipelines.any { it.language == language } - } - - // for each test class choose code generation pipeline stages - val classStages = resultsWithCurLanguage.map { codeGenerationTestCases -> - ClassStages( - codeGenerationTestCases.testClass, - StageStatusCheck( - firstStage = CodeGeneration, - lastStage = codeGenerationTestCases.languagePipelines.single { it.language == language }.lastStage, - status = ExecutionStatus.SUCCESS - ), - codeGenerationTestCases.testSets - ) - } - - val config = TestCodeGeneratorPipeline.defaultTestFrameworkConfiguration(language) - TestCodeGeneratorPipeline(config).runClassesCodeGenerationTests(classStages) - } catch (e: RuntimeException) { - pipelineErrors.add(e.message) - } - } - - if (pipelineErrors.isNotEmpty()) - fail { pipelineErrors.joinToString(System.lineSeparator()) } - } finally { - cleanAfterProcessingPackage(pkg) - } - } - - companion object { - - init { - // trigger old temporary file deletion - FileUtil.OldTempFileDeleter - } - - private val packageResult: MutableMap> = mutableMapOf() - - private var allRunningTestClasses: List = mutableListOf() - - private val languages = listOf(CodegenLanguage.JAVA, CodegenLanguage.KOTLIN) - - data class CodeGenerationTestCases( - val testClass: KClass<*>, - val testSets: List, - val languagePipelines: List - ) - - class ReadRunningTestsNumberBeforeAllTestsCallback : BeforeAllCallback { - override fun beforeAll(extensionContext: ExtensionContext) { - val clazz = Class.forName("org.junit.jupiter.engine.descriptor.AbstractExtensionContext") - val field = clazz.getDeclaredField("testDescriptor") - runningTestsNumber = field.withAccessibility { - val testDescriptor = field.get(extensionContext.parent.get()) - // get all running tests and filter disabled - allRunningTestClasses = (testDescriptor as JupiterEngineDescriptor).children - .map { it as ClassTestDescriptor } - .filter { it.testClass.getAnnotation(Disabled::class.java) == null } - .toList() - allRunningTestClasses.size - } - } - } - - private var processedTestClassesAmountByPackage: MutableMap = mutableMapOf() - private var testClassesAmountByPackage: MutableMap = mutableMapOf() - - private var runningTestsNumber: Int = 0 - - internal val logger = KotlinLogging.logger { } - - @JvmStatic - protected val testCaseGeneratorCache = mutableMapOf() - data class BuildInfo(val buildDir: Path, val dependencyPath: String?) - - private fun getTestPackageSize(packageName: String): Int = - // filter all not disabled tests classes - allRunningTestClasses - .filter { it.testClass.`package`.name == packageName } - .distinctBy { it.testClass.name.substringBeforeLast("Kt") } - .size - - private fun isPackageFullyProcessed(testClass: Class<*>): Boolean { - val currentPackage = testClass.`package` - - if (currentPackage !in testClassesAmountByPackage) - testClassesAmountByPackage[currentPackage] = getTestPackageSize(currentPackage.name) - - processedTestClassesAmountByPackage.merge(currentPackage, 1, Int::plus) - - val processed = processedTestClassesAmountByPackage[currentPackage]!! - val total = testClassesAmountByPackage[currentPackage]!! - return processed == total - } - } -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt deleted file mode 100644 index fcf65e23b6..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt +++ /dev/null @@ -1,438 +0,0 @@ -package org.utbot.tests.infrastructure - -import mu.KotlinLogging -import org.junit.jupiter.api.Assertions.assertTrue -import org.utbot.common.FileUtil -import org.utbot.common.bracket -import org.utbot.common.info -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.model.CodeGenerator -import org.utbot.framework.codegen.model.CodeGeneratorResult -import org.utbot.framework.codegen.model.UtilClassKind -import org.utbot.framework.codegen.model.UtilClassKind.Companion.UT_UTILS_CLASS_NAME -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.MockFramework -import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.framework.plugin.api.UtMethodTestSet -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.framework.plugin.api.util.description -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.withUtContext -import java.io.File -import java.nio.file.Path -import kotlin.reflect.KClass - -private val logger = KotlinLogging.logger {} - -class TestCodeGeneratorPipeline(private val testFrameworkConfiguration: TestFrameworkConfiguration) { - - fun runClassesCodeGenerationTests(classesStages: List) { - val pipelines = classesStages.map { - with(it) { ClassPipeline(StageContext(testClass, testSets, testSets.size), check) } - } - - if (pipelines.isEmpty()) return - - checkPipelinesResults(pipelines) - } - - private fun runPipelinesStages(classesPipelines: List): List { - val classesPipelinesNames = classesPipelines.joinToString(" ") { - val classUnderTest = it.stageContext.classUnderTest - classUnderTest.qualifiedName ?: error("${classUnderTest.simpleName} doesn't have a fqn name") - } - - logger - .info() - .bracket("Executing code generation tests for [$classesPipelinesNames]") { - CodeGeneration.filterPipelines(classesPipelines).forEach { - withUtContext(UtContext(it.stageContext.classUnderTest.java.classLoader)) { - processCodeGenerationStage(it) - } - } - - Compilation.filterPipelines(classesPipelines).let { - if (it.isNotEmpty()) processCompilationStages(it) - } - - TestExecution.filterPipelines(classesPipelines).let { - if (it.isNotEmpty()) processTestExecutionStages(it) - } - - return classesPipelines.map { - with(it.stageContext) { - CodeGenerationResult(classUnderTest, numberOfTestCases, stages) - } - } - } - } - - @Suppress("UNCHECKED_CAST") - private fun processCodeGenerationStage(classPipeline: ClassPipeline) { - with(classPipeline.stageContext) { - val information = StageExecutionInformation(CodeGeneration) - val testSets = data as List - - val codegenLanguage = testFrameworkConfiguration.codegenLanguage - val parametrizedTestSource = testFrameworkConfiguration.parametrizedTestSource - - val codeGenerationResult = callToCodeGenerator(testSets, classUnderTest) - val testClass = codeGenerationResult.generatedCode - - // actual number of the tests in the generated testClass - val generatedMethodsCount = testClass - .lines() - .count { - val trimmedLine = it.trimStart() - val prefix = when (codegenLanguage) { - CodegenLanguage.JAVA -> - when (parametrizedTestSource) { - ParametrizedTestSource.DO_NOT_PARAMETRIZE -> "public void " - ParametrizedTestSource.PARAMETRIZE -> "public void parameterizedTestsFor" - } - - CodegenLanguage.KOTLIN -> - when (parametrizedTestSource) { - ParametrizedTestSource.DO_NOT_PARAMETRIZE -> "fun " - ParametrizedTestSource.PARAMETRIZE -> "fun parameterizedTestsFor" - } - } - trimmedLine.startsWith(prefix) - } - // expected number of the tests in the generated testClass - val expectedNumberOfGeneratedMethods = - when (parametrizedTestSource) { - ParametrizedTestSource.DO_NOT_PARAMETRIZE -> testSets.sumOf { it.executions.size } - ParametrizedTestSource.PARAMETRIZE -> testSets.filter { it.executions.isNotEmpty() }.size - } - - // check for error in the generated file - runCatching { - val separator = System.lineSeparator() - require(ERROR_REGION_BEGINNING !in testClass) { - val lines = testClass.lines().withIndex().toList() - - val errorsRegionsBeginningIndices = lines - .filter { it.value.trimStart().startsWith(ERROR_REGION_BEGINNING) } - - val errorsRegionsEndIndices = lines - .filter { it.value.trimStart().startsWith(ERROR_REGION_END) } - - val errorRegions = errorsRegionsBeginningIndices.map { beginning -> - val endIndex = errorsRegionsEndIndices.indexOfFirst { it.index > beginning.index } - lines.subList(beginning.index + 1, errorsRegionsEndIndices[endIndex].index).map { it.value } - } - - val errorText = errorRegions.joinToString(separator, separator, separator) { errorRegion -> - val text = errorRegion.joinToString(separator = separator) - "Error region in ${classUnderTest.simpleName}: $text" - } - - logger.error(errorText) - - "Errors regions has been generated: $errorText" - } - - // for now, we skip a comparing of generated and expected test methods - // in parametrized test generation mode - // because there are problems with determining expected number of methods, - // due to a feature that generates several separated parametrized tests - // when we have several executions with different result type - if (parametrizedTestSource != ParametrizedTestSource.PARAMETRIZE) { - require(generatedMethodsCount == expectedNumberOfGeneratedMethods) { - "Something went wrong during the code generation for ${classUnderTest.simpleName}. " + - "Expected to generate $expectedNumberOfGeneratedMethods test methods, " + - "but got only $generatedMethodsCount" - } - } - }.onFailure { - val classes = listOf(classPipeline).retrieveClasses() - val buildDirectory = classes.createBuildDirectory() - - val testClassName = classPipeline.retrieveTestClassName("BrokenGeneratedTest") - val generatedTestFile = writeTest(testClass, testClassName, buildDirectory, codegenLanguage) - val generatedUtilClassFile = codeGenerationResult.utilClassKind?.writeUtilClassToFile(buildDirectory, codegenLanguage) - - logger.error("Broken test has been written to the file: [$generatedTestFile]") - if (generatedUtilClassFile != null) { - logger.error("Util class for the broken test has been written to the file: [$generatedUtilClassFile]") - } - logger.error("Failed configuration: $testFrameworkConfiguration") - - throw it - } - - classPipeline.stageContext = copy(data = codeGenerationResult, stages = stages + information.completeStage()) - } - } - - private fun UtilClassKind.writeUtilClassToFile(buildDirectory: Path, language: CodegenLanguage): File { - val utilClassFile = File(buildDirectory.toFile(), "$UT_UTILS_CLASS_NAME${language.extension}") - val utilClassText = getUtilClassText(language) - return writeFile(utilClassText, utilClassFile) - } - - private data class GeneratedTestClassInfo( - val testClassName: String, - val generatedTestFile: File, - val generatedUtilClassFile: File? - ) - - @Suppress("UNCHECKED_CAST") - private fun processCompilationStages(classesPipelines: List) { - val information = StageExecutionInformation(Compilation) - val classes = classesPipelines.retrieveClasses() - val buildDirectory = classes.createBuildDirectory() - - val codegenLanguage = testFrameworkConfiguration.codegenLanguage - - val testClassesNamesToTestGeneratedTests = classesPipelines.map { classPipeline -> - val codeGeneratorResult = classPipeline.stageContext.data as CodeGeneratorResult//String - val testClass = codeGeneratorResult.generatedCode - - val testClassName = classPipeline.retrieveTestClassName("GeneratedTest") - val generatedTestFile = writeTest(testClass, testClassName, buildDirectory, codegenLanguage) - val generatedUtilClassFile = codeGeneratorResult.utilClassKind?.writeUtilClassToFile(buildDirectory, codegenLanguage) - - logger.info("Test has been written to the file: [$generatedTestFile]") - if (generatedUtilClassFile != null) { - logger.info("Util class for the test has been written to the file: [$generatedUtilClassFile]") - } - - GeneratedTestClassInfo(testClassName, generatedTestFile, generatedUtilClassFile) - } - - val sourceFiles = mutableListOf().apply { - this += testClassesNamesToTestGeneratedTests.map { it.generatedTestFile.absolutePath } - this += testClassesNamesToTestGeneratedTests.mapNotNull { it.generatedUtilClassFile?.absolutePath } - } - compileTests( - "$buildDirectory", - sourceFiles, - codegenLanguage - ) - - testClassesNamesToTestGeneratedTests.zip(classesPipelines) { generatedTestClassInfo, classPipeline -> - classPipeline.stageContext = classPipeline.stageContext.copy( - data = CompilationResult("$buildDirectory", generatedTestClassInfo.testClassName), - stages = classPipeline.stageContext.stages + information.completeStage() - ) - } - } - - /** - * For simple CUT equals to its fqn + [testSuffix] suffix, - * for nested CUT is its package + dot + its simple name + [testSuffix] suffix (to avoid outer class mention). - */ - private fun ClassPipeline.retrieveTestClassName(testSuffix: String): String = - stageContext.classUnderTest.let { "${it.java.`package`.name}.${it.simpleName}" } + testSuffix - - private fun List>.createBuildDirectory() = - FileUtil.isolateClassFiles(*toTypedArray()).toPath() - - private fun List.retrieveClasses() = map { it.stageContext.classUnderTest.java } - - @Suppress("UNCHECKED_CAST") - private fun processTestExecutionStages(classesPipelines: List) { - val information = StageExecutionInformation(TestExecution) - val buildDirectory = (classesPipelines.first().stageContext.data as CompilationResult).buildDirectory - val testClassesNames = classesPipelines.map { classPipeline -> - (classPipeline.stageContext.data as CompilationResult).testClassName - } - with(testFrameworkConfiguration) { - runTests(buildDirectory, testClassesNames, testFramework, codegenLanguage) - } - classesPipelines.forEach { - it.stageContext = it.stageContext.copy( - data = Unit, - stages = it.stageContext.stages + information.completeStage() - ) - } - } - - private fun callToCodeGenerator( - testSets: List, - classUnderTest: KClass<*> - ): CodeGeneratorResult { - val params = mutableMapOf>() - - val codeGenerator = with(testFrameworkConfiguration) { - CodeGenerator( - classUnderTest.id, - generateUtilClassFile = generateUtilClassFile, - paramNames = params, - testFramework = testFramework, - staticsMocking = staticsMocking, - forceStaticMocking = forceStaticMocking, - generateWarningsForStaticMocking = false, - codegenLanguage = codegenLanguage, - parameterizedTestSource = parametrizedTestSource, - runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, - enableTestsTimeout = enableTestsTimeout - ) - } - val testClassCustomName = "${classUnderTest.java.simpleName}GeneratedTest" - - return codeGenerator.generateAsStringWithTestReport(testSets, testClassCustomName) - } - - private fun checkPipelinesResults(classesPipelines: List) { - val results = runPipelinesStages(classesPipelines) - val classesChecks = classesPipelines.map { it.stageContext.classUnderTest to listOf(it.check) } - processResults(results, classesChecks) - } - - private fun processResults( - results: List, - classesChecks: List, List>> - ) { - val transformedResults: List, List>> = - results.zip(classesChecks) { result, classChecks -> - val stageStatusChecks = classChecks.second.mapNotNull { check -> - runCatching { check(result) } - .fold( - onSuccess = { if (it) null else check.description }, - onFailure = { it.description } - ) - } - result.classUnderTest to stageStatusChecks - } - val failedResults = transformedResults.filter { it.second.isNotEmpty() } - - assertTrue(failedResults.isEmpty()) { - val separator = "\n\t\t" - val failedResultsConcatenated = failedResults.joinToString(separator, prefix = separator) { - "${it.first.simpleName} : ${it.second.joinToString()}" - } - - "There are failed checks: $failedResultsConcatenated" - } - } - - companion object { - var currentTestFrameworkConfiguration = defaultTestFrameworkConfiguration() - - fun defaultTestFrameworkConfiguration(language: CodegenLanguage = CodegenLanguage.JAVA) = TestFrameworkConfiguration( - testFramework = TestFramework.defaultItem, - codegenLanguage = language, - mockFramework = MockFramework.defaultItem, - mockStrategy = MockStrategyApi.defaultItem, - staticsMocking = StaticsMocking.defaultItem, - parametrizedTestSource = ParametrizedTestSource.defaultItem, - forceStaticMocking = ForceStaticMocking.defaultItem, - generateUtilClassFile = false - ) - - private const val ERROR_REGION_BEGINNING = "///region Errors" - private const val ERROR_REGION_END = "///endregion" - } -} - -enum class ExecutionStatus { - IN_PROCESS, FAILED, SUCCESS -} - -sealed class Stage(private val name: String, val nextStage: Stage?) { - override fun toString() = name - - fun filterPipelines(classesPipelines: List): List = - classesPipelines.filter { - it.check.firstStage <= this && it.check.lastStage >= this - } - - abstract operator fun compareTo(stage: Stage): Int -} - -object CodeGeneration : Stage("Code Generation", Compilation) { - override fun compareTo(stage: Stage): Int = if (stage is CodeGeneration) 0 else -1 -} - -object Compilation : Stage("Compilation", TestExecution) { - override fun compareTo(stage: Stage): Int = - when (stage) { - is CodeGeneration -> 1 - is Compilation -> 0 - else -> -1 - } -} - -object TestExecution : Stage("Test Execution", null) { - override fun compareTo(stage: Stage): Int = if (stage is TestExecution) 0 else 1 -} - -private fun pipeline(firstStage: Stage = CodeGeneration, lastStage: Stage = TestExecution): Sequence = - generateSequence(firstStage) { if (it == lastStage) null else it.nextStage } - -data class StageExecutionInformation( - val stage: Stage, - val status: ExecutionStatus = ExecutionStatus.IN_PROCESS -) { - fun completeStage(status: ExecutionStatus = ExecutionStatus.SUCCESS) = copy(status = status) -} - -data class CodeGenerationResult( - val classUnderTest: KClass<*>, - val numberOfTestCases: Int, - val stageStatisticInformation: List -) - -sealed class PipelineResultCheck( - val description: String, - private val check: (CodeGenerationResult) -> Boolean -) { - open operator fun invoke(codeGenerationResult: CodeGenerationResult) = check(codeGenerationResult) -} - -/** - * Checks that stage failed and all previous stages are successfully processed. - */ -class StageStatusCheck( - val firstStage: Stage = CodeGeneration, - val lastStage: Stage, - status: ExecutionStatus, -) : PipelineResultCheck( - description = "Expect [$lastStage] to be in [$status] status", - check = constructPipelineResultCheck(firstStage, lastStage, status) -) - -private fun constructPipelineResultCheck( - firstStage: Stage, - lastStage: Stage, - status: ExecutionStatus -): (CodeGenerationResult) -> Boolean = - { result -> - val statuses = result.stageStatisticInformation.associate { it.stage to it.status } - val failedPrevStage = pipeline(firstStage, lastStage) - .takeWhile { it != lastStage } - .firstOrNull { statuses[it] != ExecutionStatus.SUCCESS } - - if (failedPrevStage != null) error("[$lastStage] is not started cause $failedPrevStage has failed") - - statuses[lastStage] == status - } - -data class CompilationResult(val buildDirectory: String, val testClassName: String) - -/** - * Context to run Stage. Contains class under test, data (input of current stage), number of analyzed test cases and - * stage execution information. - */ -data class StageContext( - val classUnderTest: KClass<*>, - val data: Any = Unit, - val numberOfTestCases: Int = 0, - val stages: List = emptyList(), - val status: ExecutionStatus = ExecutionStatus.SUCCESS -) - -data class ClassStages( - val testClass: KClass<*>, - val check: StageStatusCheck, - val testSets: List = emptyList() -) - -data class ClassPipeline(var stageContext: StageContext, val check: StageStatusCheck) \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestSpecificTestCaseGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestSpecificTestCaseGenerator.kt deleted file mode 100644 index 0f90157486..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestSpecificTestCaseGenerator.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.utbot.tests.infrastructure - -import kotlinx.coroutines.launch -import mu.KotlinLogging -import org.utbot.common.runBlockingWithCancellationPredicate -import org.utbot.common.runIgnoringCancellationException -import org.utbot.engine.EngineController -import org.utbot.engine.Mocker -import org.utbot.engine.UtBotSymbolicEngine -import org.utbot.engine.util.mockListeners.ForceMockListener -import org.utbot.engine.util.mockListeners.ForceStaticMockListener -import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.framework.plugin.api.TestCaseGenerator -import org.utbot.framework.plugin.api.UtError -import org.utbot.framework.plugin.api.UtExecution -import org.utbot.framework.plugin.api.UtMethodTestSet -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.services.JdkInfoDefaultProvider -import org.utbot.framework.util.jimpleBody -import java.nio.file.Path - -/** - * Special [UtMethodTestSet] generator for test methods that has a correct - * wrapper for suspend function [TestCaseGenerator.generateAsync]. - */ -class TestSpecificTestCaseGenerator( - buildDir: Path, - classpath: String?, - dependencyPaths: String, - engineActions: MutableList<(UtBotSymbolicEngine) -> Unit> = mutableListOf(), - isCanceled: () -> Boolean = { false }, -): TestCaseGenerator( - listOf(buildDir), - classpath, - dependencyPaths, - JdkInfoDefaultProvider().info, - engineActions, - isCanceled, - forceSootReload = false -) { - - private val logger = KotlinLogging.logger {} - - fun generate(method: ExecutableId, mockStrategy: MockStrategyApi): UtMethodTestSet { - if (isCanceled()) { - return UtMethodTestSet(method) - } - - logger.trace { "UtSettings:${System.lineSeparator()}" + UtSettings.toString() } - - val executions = mutableListOf() - val errors = mutableMapOf() - - val mockAlwaysDefaults = Mocker.javaDefaultClasses.mapTo(mutableSetOf()) { it.id } - val defaultTimeEstimator = ExecutionTimeEstimator(UtSettings.utBotGenerationTimeoutInMillis, 1) - - val config = TestCodeGeneratorPipeline.currentTestFrameworkConfiguration - var forceMockListener: ForceMockListener? = null - var forceStaticMockListener: ForceStaticMockListener? = null - - if (config.parametrizedTestSource == ParametrizedTestSource.PARAMETRIZE) { - forceMockListener = ForceMockListener.create(this, conflictTriggers) - forceStaticMockListener = ForceStaticMockListener.create(this, conflictTriggers) - } - - runIgnoringCancellationException { - runBlockingWithCancellationPredicate(isCanceled) { - val controller = EngineController() - controller.job = launch { - super - .generateAsync(controller, method, mockStrategy, mockAlwaysDefaults, defaultTimeEstimator) - .collect { - when (it) { - is UtExecution -> executions += it - is UtError -> errors.merge(it.description, 1, Int::plus) - } - } - } - } - } - - forceMockListener?.detach(this, forceMockListener) - forceStaticMockListener?.detach(this, forceStaticMockListener) - - val minimizedExecutions = super.minimizeExecutions(executions) - return UtMethodTestSet(method, minimizedExecutions, jimpleBody(method), errors) - } -} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/UtValueTestCaseChecker.kt b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/UtValueTestCaseChecker.kt deleted file mode 100644 index 6e321c4cf2..0000000000 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/UtValueTestCaseChecker.kt +++ /dev/null @@ -1,2872 +0,0 @@ -@file:Suppress("NestedLambdaShadowedImplicitParameter") - -package org.utbot.tests.infrastructure - -import org.junit.jupiter.api.Assertions.assertTrue -import org.utbot.common.ClassLocation -import org.utbot.common.FileUtil.clearTempDirectory -import org.utbot.common.FileUtil.findPathToClassFiles -import org.utbot.common.FileUtil.locateClass -import org.utbot.engine.prettify -import org.utbot.framework.UtSettings -import org.utbot.framework.UtSettings.checkCoverageInCodeGenerationTests -import org.utbot.framework.UtSettings.daysLimitForTempFiles -import org.utbot.framework.UtSettings.testDisplayName -import org.utbot.framework.UtSettings.testName -import org.utbot.framework.UtSettings.testSummary -import org.utbot.framework.coverage.Coverage -import org.utbot.framework.coverage.counters -import org.utbot.framework.coverage.methodCoverage -import org.utbot.framework.coverage.toAtLeast -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.DocClassLinkStmt -import org.utbot.framework.plugin.api.DocCodeStmt -import org.utbot.framework.plugin.api.DocCustomTagStatement -import org.utbot.framework.plugin.api.DocMethodLinkStmt -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.FieldMockTarget -import org.utbot.framework.plugin.api.MockId -import org.utbot.framework.plugin.api.MockInfo -import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.framework.plugin.api.MockStrategyApi.NO_MOCKS -import org.utbot.framework.plugin.api.ObjectMockTarget -import org.utbot.framework.plugin.api.ParameterMockTarget -import org.utbot.framework.plugin.api.UtCompositeModel -import org.utbot.framework.plugin.api.UtConcreteValue -import org.utbot.framework.plugin.api.UtInstrumentation -import org.utbot.framework.plugin.api.UtMethodTestSet -import org.utbot.framework.plugin.api.UtMockValue -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.UtValueExecution -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.framework.plugin.api.util.declaringClazz -import org.utbot.framework.plugin.api.util.enclosingClass -import org.utbot.framework.plugin.api.util.executableId -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.kClass -import org.utbot.framework.plugin.api.util.withUtContext -import org.utbot.framework.util.Conflict -import org.utbot.framework.util.toValueTestCase -import org.utbot.summary.summarize -import org.utbot.testcheckers.ExecutionsNumberMatcher -import java.io.File -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.reflect.KClass -import kotlin.reflect.KFunction -import kotlin.reflect.KFunction0 -import kotlin.reflect.KFunction1 -import kotlin.reflect.KFunction2 -import kotlin.reflect.KFunction3 -import kotlin.reflect.KFunction4 -import kotlin.reflect.KFunction5 - -abstract class UtValueTestCaseChecker( - testClass: KClass<*>, - testCodeGeneration: Boolean = true, - languagePipelines: List = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN) - ) -) : CodeGenerationIntegrationTest(testClass, testCodeGeneration, languagePipelines) { - // contains already analyzed by the engine methods - private val analyzedMethods: MutableMap = mutableMapOf() - - val searchDirectory: Path = Paths.get("../utbot-sample/src/main/java") - - init { - UtSettings.checkSolverTimeoutMillis = 0 - UtSettings.checkNpeInNestedMethods = true - UtSettings.checkNpeInNestedNotPrivateMethods = true - UtSettings.substituteStaticsWithSymbolicVariable = true - UtSettings.useAssembleModelGenerator = true - UtSettings.saveRemainingStatesForConcreteExecution = false - UtSettings.useFuzzing = false - UtSettings.useCustomJavaDocTags = false - } - - // checks paramsBefore and result - protected inline fun check( - method: KFunction1<*, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun check( - method: KFunction2<*, T, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, 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, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun check( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, 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, T1::class, T2::class, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun check( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, 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, T1::class, T2::class, T3::class, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun check( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, 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, T1::class, T2::class, T3::class, T4::class, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // check paramsBefore and Result, suitable to check exceptions - protected inline fun checkWithException( - method: KFunction1<*, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (Result) -> 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, - arguments = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithException( - method: KFunction2<*, T, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, Result) -> 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 = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithException( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, Result) -> 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, T1::class, T2::class, - arguments = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithException( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, Result) -> 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, T1::class, T2::class, T3::class, - arguments = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithException( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, Result) -> 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // check this, paramsBefore and result value - protected inline fun checkWithThis( - method: KFunction1, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, 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 = ::withThisAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithThis( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, 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 = ::withThisAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithThis( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, 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 = ::withThisAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithThis( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, T3, 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 = ::withThisAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithThis( - method: KFunction5, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, T3, T4, 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 = ::withThisAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithThisAndException( - method: KFunction1, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, Result) -> 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 = ::withThisAndException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithThisAndException( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, Result) -> 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 = ::withThisAndException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithThisAndException( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, Result) -> 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 = ::withThisAndException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithThisAndException( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, T3, Result) -> 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 = ::withThisAndException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithThisAndException( - method: KFunction5, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, T3, T4, Result) -> 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 = ::withThisAndException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // checks paramsBefore, mocks and result value - protected inline fun checkMocksInStaticMethod( - method: KFunction0, - branches: ExecutionsNumberMatcher, - vararg matchers: (Mocks, 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, - arguments = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksInStaticMethod( - method: KFunction1, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, Mocks, 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 = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksInStaticMethod( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, Mocks, 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, T1::class, T2::class, - arguments = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksInStaticMethod( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, Mocks, 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, T1::class, T2::class, T3::class, - arguments = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksInStaticMethod( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, Mocks, 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // checks paramsBefore, mocks and result value - protected inline fun checkMocks( - method: KFunction1<*, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (Mocks, 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, - arguments = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocks( - method: KFunction2<*, T, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, Mocks, 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 = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocks( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, Mocks, 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, T1::class, T2::class, - arguments = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocks( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, Mocks, 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, T1::class, T2::class, T3::class, - arguments = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocks( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, Mocks, 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withMocks, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // check paramsBefore, mocks and instrumentation and result value - protected inline fun checkMocksAndInstrumentation( - method: KFunction1<*, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (Mocks, Instrumentation, 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, - arguments = ::withMocksAndInstrumentation, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksAndInstrumentation( - method: KFunction2<*, T, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, Mocks, Instrumentation, 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 = ::withMocksAndInstrumentation, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksAndInstrumentation( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, Mocks, Instrumentation, 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, T1::class, T2::class, - arguments = ::withMocksAndInstrumentation, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksAndInstrumentation( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, Mocks, Instrumentation, 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, T1::class, T2::class, T3::class, - arguments = ::withMocksAndInstrumentation, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksAndInstrumentation( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, Mocks, Instrumentation, 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withMocksAndInstrumentation, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // check this, paramsBefore, mocks, instrumentation and return value - protected inline fun checkMocksInstrumentationAndThis( - method: KFunction1, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, Mocks, Instrumentation, 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 = ::withMocksInstrumentationAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksInstrumentationAndThis( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, Mocks, Instrumentation, 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 = ::withMocksInstrumentationAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksInstrumentationAndThis( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, Mocks, Instrumentation, 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 = ::withMocksInstrumentationAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksInstrumentationAndThis( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, T3, Mocks, Instrumentation, 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 = ::withMocksInstrumentationAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMocksInstrumentationAndThis( - method: KFunction5, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, T3, T4, Mocks, Instrumentation, 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 = ::withMocksInstrumentationAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // checks paramsBefore and return value for static methods - protected inline fun checkStaticMethod( - method: KFunction0, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethod( - method: KFunction1, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, 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, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethod( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, 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, T1::class, T2::class, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethod( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, 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, T1::class, T2::class, T3::class, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethod( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, 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, T1::class, T2::class, T3::class, T4::class, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // checks paramsBefore and Result, suitable for exceptions check - protected inline fun checkStaticMethodWithException( - method: KFunction0, - branches: ExecutionsNumberMatcher, - vararg matchers: (Result) -> 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, - arguments = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodWithException( - method: KFunction1, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, Result) -> 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 = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodWithException( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, Result) -> 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, T1::class, T2::class, - arguments = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodWithException( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, Result) -> 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, T1::class, T2::class, T3::class, - arguments = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodWithException( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, Result) -> 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withException, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // check arguments, statics and return value - protected inline fun checkStatics( - method: KFunction1<*, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, - arguments = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStatics( - method: KFunction2<*, T, R>, - 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 = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStatics( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, T1::class, T2::class, - arguments = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStatics( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, T1::class, T2::class, T3::class, - arguments = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStatics( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // check arguments, statics and Result for exceptions check - protected inline fun checkStaticsAndException( - method: KFunction1<*, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (StaticsType, Result) -> 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, - arguments = ::withStaticsBeforeAndExceptions, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsAndException( - method: KFunction2<*, T, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, StaticsType, Result) -> 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 = ::withStaticsBeforeAndExceptions, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsAndException( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, StaticsType, Result) -> 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, T1::class, T2::class, - arguments = ::withStaticsBeforeAndExceptions, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsAndException( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, StaticsType, Result) -> 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, T1::class, T2::class, T3::class, - arguments = ::withStaticsBeforeAndExceptions, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsAndException( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, StaticsType, Result) -> 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withStaticsBeforeAndExceptions, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsAfter( - method: KFunction1<*, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, - arguments = ::withStaticsAfter, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsAfter( - method: KFunction2<*, T, R>, - 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 = ::withStaticsAfter, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsAfter( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, T1::class, T2::class, - arguments = ::withStaticsAfter, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsAfter( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, T1::class, T2::class, T3::class, - arguments = ::withStaticsAfter, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsAfter( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withStaticsAfter, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - 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, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, - arguments = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsInStaticMethod( - 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 = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsInStaticMethod( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, T1::class, T2::class, - arguments = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsInStaticMethod( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, T1::class, T2::class, T3::class, - arguments = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsInStaticMethod( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsInStaticMethod( - method: KFunction5, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, T5, 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withStaticsBefore, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // check this, arguments and result value - protected inline fun checkStaticsWithThis( - 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 = ::withThisStaticsBeforeAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsWithThis( - 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 = ::withThisStaticsBeforeAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsWithThis( - 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 = ::withThisStaticsBeforeAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsWithThis( - 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 = ::withThisStaticsBeforeAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticsWithThis( - 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 = ::withThisStaticsBeforeAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkParamsMutationsAndResult( - method: KFunction2<*, T, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T, 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 = ::withParamsMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkParamsMutationsAndResult( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T1, T2, 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, T1::class, T2::class, - arguments = ::withParamsMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkParamsMutationsAndResult( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T1, T2, T3, 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, T1::class, T2::class, T3::class, - arguments = ::withParamsMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkParamsMutationsAndResult( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, T1, T2, T3, T4, 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withParamsMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // checks mutations in the parameters - protected inline fun checkParamsMutations( - method: KFunction2<*, T, *>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T) -> 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 = ::withParamsMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkParamsMutations( - method: KFunction3<*, T1, T2, *>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T1, T2) -> 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, T1::class, T2::class, - arguments = ::withParamsMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkParamsMutations( - method: KFunction4<*, T1, T2, T3, *>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T1, T2, T3) -> 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, T1::class, T2::class, T3::class, - arguments = ::withParamsMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkParamsMutations( - method: KFunction5<*, T1, T2, T3, T4, *>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, T1, T2, T3, T4) -> 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withParamsMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // checks mutations in the parameters and statics for static method - protected fun checkStaticMethodMutation( - method: KFunction0<*>, - branches: ExecutionsNumberMatcher, - vararg matchers: (StaticsType, StaticsType) -> 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, - arguments = ::withMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodMutation( - method: KFunction1, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, StaticsType, T, StaticsType) -> 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 = ::withMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodMutation( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, StaticsType, T1, T2, StaticsType) -> 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, T1::class, T2::class, - arguments = ::withMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodMutation( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, StaticsType, T1, T2, T3, StaticsType) -> 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, T1::class, T2::class, T3::class, - arguments = ::withMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodMutation( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, StaticsType, T1, T2, T3, T4, StaticsType) -> 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodMutationAndResult( - method: KFunction0, - branches: ExecutionsNumberMatcher, - vararg matchers: (StaticsType, 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, - arguments = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodMutationAndResult( - method: KFunction1, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, StaticsType, 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 = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodMutationAndResult( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, StaticsType, 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, T1::class, T2::class, - arguments = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodMutationAndResult( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, StaticsType, 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, T1::class, T2::class, T3::class, - arguments = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkStaticMethodMutationAndResult( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, StaticsType, 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - - // checks mutations in the parameters and statics - protected inline fun checkMutations( - method: KFunction2<*, T, *>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, StaticsType, T, StaticsType) -> 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 = ::withMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMutations( - method: KFunction3<*, T1, T2, *>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, StaticsType, T1, T2, StaticsType) -> 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, T1::class, T2::class, - arguments = ::withMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMutations( - method: KFunction4<*, T1, T2, T3, *>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, StaticsType, T1, T2, T3, StaticsType) -> 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, T1::class, T2::class, T3::class, - arguments = ::withMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMutations( - method: KFunction5<*, T1, T2, T3, T4, *>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, StaticsType, T1, T2, T3, T4, StaticsType) -> 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withMutations, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // checks mutations in the parameters and statics - protected inline fun checkMutationsAndResult( - method: KFunction1<*, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (StaticsType, 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, - arguments = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMutationsAndResult( - method: KFunction2<*, T, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, StaticsType, 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 = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMutationsAndResult( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, StaticsType, 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, T1::class, T2::class, - arguments = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMutationsAndResult( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, StaticsType, 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, T1::class, T2::class, T3::class, - arguments = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkMutationsAndResult( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, StaticsType, 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, T1::class, T2::class, T3::class, T4::class, - arguments = ::withMutationsAndResult, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - // checks mutations in this, parameters and statics - protected inline fun checkAllMutationsWithThis( - method: KFunction1, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, StaticsType, T, StaticsType) -> 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 = ::withMutationsAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkAllMutationsWithThis( - method: KFunction2, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, StaticsType, T, T1, StaticsType) -> 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 = ::withMutationsAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkAllMutationsWithThis( - method: KFunction3, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, StaticsType, T, T1, T2, StaticsType) -> 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 = ::withMutationsAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkAllMutationsWithThis( - method: KFunction4, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, T3, StaticsType, T, T1, T2, T3, StaticsType) -> 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 = ::withMutationsAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkAllMutationsWithThis( - method: KFunction5, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, T1, T2, T3, T4, StaticsType, T, T1, T2, T3, T4, StaticsType) -> 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 = ::withMutationsAndThis, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - //region checks substituting statics with symbolic variable or not - protected inline fun checkWithoutStaticsSubstitution( - method: KFunction1<*, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (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, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithoutStaticsSubstitution( - method: KFunction2<*, T, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T, 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, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithoutStaticsSubstitution( - method: KFunction3<*, T1, T2, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, 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, T1::class, T2::class, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithoutStaticsSubstitution( - method: KFunction4<*, T1, T2, T3, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, 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, T1::class, T2::class, T3::class, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - protected inline fun checkWithoutStaticsSubstitution( - method: KFunction5<*, T1, T2, T3, T4, R>, - branches: ExecutionsNumberMatcher, - vararg matchers: (T1, T2, T3, T4, 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, T1::class, T2::class, T3::class, T4::class, - additionalDependencies = additionalDependencies, - summaryTextChecks = summaryTextChecks, - summaryNameChecks = summaryNameChecks, - summaryDisplayNameChecks = summaryDisplayNameChecks - ) - - //endregion - - /** - * @param method method under test - * @param generateWithNested a flag indicating if we need to generate nested test classes - * or just generate one top-level test class - * @see [ClassWithStaticAndInnerClassesTest] - */ - fun checkAllCombinations(method: KFunction<*>, generateWithNested: Boolean = false) { - val failed = mutableListOf() - val succeeded = mutableListOf() - - allTestFrameworkConfigurations - .filterNot { it.isDisabled } - .forEach { config -> - runCatching { - internalCheckForCodeGeneration(method, config, generateWithNested) - }.onFailure { - failed += config - }.onSuccess { - succeeded += config - } - } - - // TODO check that all generated classes have different content JIRA:1415 - - logger.info { "Total configurations: ${succeeded.size + failed.size}. Failed: ${failed.size}." } - - require(failed.isEmpty()) { - val separator = System.lineSeparator() - val failedConfigurations = failed.joinToString(prefix = separator, separator = separator) - - "Failed configurations: $failedConfigurations" - } - } - - @Suppress("ControlFlowWithEmptyBody", "UNUSED_VARIABLE") - private fun internalCheckForCodeGeneration( - method: KFunction<*>, - testFrameworkConfiguration: TestFrameworkConfiguration, - generateWithNested: Boolean - ) { - withSettingsFromTestFrameworkConfiguration(testFrameworkConfiguration) { - with(testFrameworkConfiguration) { - - val executableId = method.executableId - computeAdditionalDependenciesClasspathAndBuildDir(method.declaringClazz, emptyArray()) - val utContext = UtContext(method.declaringClazz.classLoader) - - clearTempDirectory(daysLimitForTempFiles) - - withUtContext(utContext) { - val methodWithStrategy = - MethodWithMockStrategy(executableId, mockStrategy, resetNonFinalFieldsAfterClinit) - - val (testSet, coverage) = analyzedMethods.getOrPut(methodWithStrategy) { - walk(executableId, mockStrategy) - } - - // if force mocking took place in parametrized test generation, - // we do not need to process this [testSet] - if (TestCodeGeneratorPipeline.currentTestFrameworkConfiguration.isParametrizedAndMocked) { - conflictTriggers.reset(Conflict.ForceMockHappened, Conflict.ForceStaticMockHappened) - return - } - - if (checkCoverageInCodeGenerationTests) { - // TODO JIRA:1407 - } - - val methodUnderTestOwnerId = testSet.method.classId - val classUnderTest = if (generateWithNested) { - generateSequence(methodUnderTestOwnerId) { clazz -> clazz.enclosingClass }.last().kClass - } else { - methodUnderTestOwnerId.kClass - } - - val stageStatusCheck = StageStatusCheck( - firstStage = CodeGeneration, - lastStage = TestExecution, - status = ExecutionStatus.SUCCESS - ) - val classStages = listOf(ClassStages(classUnderTest, stageStatusCheck, listOf(testSet))) - - TestCodeGeneratorPipeline(testFrameworkConfiguration).runClassesCodeGenerationTests(classStages) - } - } - } - } - - inline fun internalCheck( - method: KFunction, - mockStrategy: MockStrategyApi, - branches: ExecutionsNumberMatcher, - matchers: Array>, - coverageMatcher: CoverageMatcher, - vararg classes: KClass<*>, - noinline arguments: (UtValueExecution<*>) -> List = ::withResult, - additionalDependencies: Array> = emptyArray(), - summaryTextChecks: List<(List?) -> Boolean> = listOf(), - summaryNameChecks: List<(String?) -> Boolean> = listOf(), - summaryDisplayNameChecks: List<(String?) -> Boolean> = listOf() - ) { - if (UtSettings.checkAllCombinationsForEveryTestInSamples) { - checkAllCombinations(method) - } - - val executableId = method.executableId - - withUtContext(UtContext(method.declaringClazz.classLoader)) { - val additionalDependenciesClassPath = - computeAdditionalDependenciesClasspathAndBuildDir(executableId.classId.jClass, additionalDependencies) - - val (testSet, coverage) = if (coverageMatcher is DoNotCalculate) { - MethodResult(executions(executableId, mockStrategy, additionalDependenciesClassPath), Coverage()) - } else { - walk(executableId, mockStrategy, additionalDependenciesClassPath) - } - testSet.summarize(searchDirectory) - val valueTestCase = testSet.toValueTestCase() - - assertTrue(testSet.errors.isEmpty()) { - "We have errors: ${ - testSet.errors.entries.map { "${it.value}: ${it.key}" }.prettify() - }" - } - - // if force mocking took place in parametrized test generation, - // we do not need to process this [testSet] - if (TestCodeGeneratorPipeline.currentTestFrameworkConfiguration.isParametrizedAndMocked) { - conflictTriggers.reset(Conflict.ForceMockHappened, Conflict.ForceStaticMockHappened) - return - } - - val valueExecutions = valueTestCase.executions - assertTrue(branches(valueExecutions.size)) { - "Branch count matcher '$branches' fails for ${valueExecutions.size}: ${valueExecutions.prettify()}" - } - - valueExecutions.checkTypes(R::class, classes.toList()) - - if (testSummary) { - valueExecutions.checkSummaryMatchers(summaryTextChecks) - // todo: Ask Zarina to take a look (Java 11 transition) - // valueExecutions.checkCommentsForBasicErrors() - } - if (testName) { - valueExecutions.checkNameMatchers(summaryNameChecks) - - // Disabled due to strange fails in tests for primitive streams -// valueExecutions.checkNamesForBasicErrors() - } - if (testDisplayName) { - valueExecutions.checkDisplayNameMatchers(summaryDisplayNameChecks) - } - - valueExecutions.checkMatchers(matchers, arguments) - assertTrue(coverageMatcher(coverage)) { - "Coverage matcher '$coverageMatcher' fails for $coverage (at least: ${coverage.toAtLeast()})" - } - - processTestCase(testSet) - } - } - - fun List>.checkTypes( - resultType: KClass<*>, - argumentTypes: List> - ) { - for (execution in this) { - val typesWithArgs = argumentTypes.zip(execution.stateBefore.params.map { it.type }) - - assertTrue(typesWithArgs.all { it.second::class.isInstance(it.first::class) }) { "Param types do not match" } - - execution.returnValue.getOrNull()?.let { returnValue -> - assertTrue(resultType::class.isInstance(returnValue::class)) { "Return type does not match" } - } - } - } - - fun List>.checkMatchers( - matchers: Array>, - arguments: (UtValueExecution<*>) -> List - ) { - val notMatched = matchers.indices.filter { i -> - this.none { ex -> - runCatching { invokeMatcher(matchers[i], arguments(ex)) }.getOrDefault(false) - } - } - - val matchersNumbers = notMatched.map { it + 1 } - - assertTrue(notMatched.isEmpty()) { "Execution matchers $matchersNumbers not match to ${this.prettify()}" } - - // Uncomment if you want to check that each value matches at least one matcher. -// val notMatchedValues = this.filter { ex -> -// matchers.none { matcher -> -// runCatching { -// invokeMatcher(matcher, arguments(ex)) -// }.getOrDefault(false) -// } -// } -// assertTrue(notMatchedValues.isEmpty()) { "Values not match to matchers, ${notMatchedValues.prettify()}" } - } - - fun List>.checkSummaryMatchers(summaryTextChecks: List<(List?) -> Boolean>) { - val notMatched = summaryTextChecks.indices.filter { i -> - this.none { ex -> summaryTextChecks[i](ex.summary) } - } - assertTrue(notMatched.isEmpty()) { - "Summary matchers ${notMatched.map { it + 1 }} not match for \n${ - this.joinToString(separator = "\n\n") { - "Next summary start ".padEnd(50, '-') + "\n" + - prettifyDocStatementList(it.summary ?: emptyList()) - } - }" - } - } - - private fun prettifyDocStatementList(docStmts: List): String { - val flattenStmts = flattenDocStatements(docStmts) - return flattenStmts.joinToString(separator = "\n") { "${it.javaClass.simpleName.padEnd(20)} content:$it;" } - } - - fun List>.checkNameMatchers(nameTextChecks: List<(String?) -> Boolean>) { - val notMatched = nameTextChecks.indices.filter { i -> - this.none { execution -> nameTextChecks[i](execution.testMethodName) } - } - assertTrue(notMatched.isEmpty()) { - "Test method name matchers ${notMatched.map { it + 1 }} not match for ${map { it.testMethodName }.prettify()}" - } - } - - fun List>.checkDisplayNameMatchers(displayNameTextChecks: List<(String?) -> Boolean>) { - val notMatched = displayNameTextChecks.indices.filter { i -> - this.none { ex -> displayNameTextChecks[i](ex.displayName) } - } - assertTrue(notMatched.isEmpty()) { - "Test display name matchers ${notMatched.map { it + 1 }} not match for ${map { it.displayName }.prettify()}" - } - } - -// fun List>.checkCommentsForBasicErrors() { -// val emptyLines = this.filter { -// it.summary?.contains("\n\n") ?: false -// } -// assertTrue(emptyLines.isEmpty()) { "Empty lines in the comments: ${emptyLines.map { it.summary }.prettify()}" } -// } - -// fun List>.checkNamesForBasicErrors() { -// val wrongASTNodeConversion = this.filter { -// it.testMethodName?.contains("null") ?: false -// } -// assertTrue(wrongASTNodeConversion.isEmpty()) { -// "Null in AST node conversion in the names: ${wrongASTNodeConversion.map { it.testMethodName }.prettify()}" -// } -// } - - fun walk( - method: ExecutableId, - mockStrategy: MockStrategyApi, - additionalDependenciesClassPath: String = "" - ): MethodResult { - val testSet = executions(method, mockStrategy, additionalDependenciesClassPath) - val methodCoverage = methodCoverage( - method, - testSet.toValueTestCase().executions, - buildDir.toString() + File.pathSeparator + additionalDependenciesClassPath - ) - return MethodResult(testSet, methodCoverage) - } - - fun executions( - method: ExecutableId, - mockStrategy: MockStrategyApi, - additionalDependenciesClassPath: String - ): UtMethodTestSet { - val buildInfo = CodeGenerationIntegrationTest.Companion.BuildInfo(buildDir, additionalDependenciesClassPath) - - val testCaseGenerator = testCaseGeneratorCache - .getOrPut(buildInfo) { - TestSpecificTestCaseGenerator( - buildDir, - additionalDependenciesClassPath, - System.getProperty("java.class.path") - ) - } - return testCaseGenerator.generate(method, mockStrategy) - } - - fun executionsModel( - method: ExecutableId, - mockStrategy: MockStrategyApi, - additionalDependencies: Array> = emptyArray() - ): UtMethodTestSet { - val additionalDependenciesClassPath = - computeAdditionalDependenciesClasspathAndBuildDir(method.classId.jClass, additionalDependencies) - withUtContext(UtContext(method.classId.jClass.classLoader)) { - val buildInfo = CodeGenerationIntegrationTest.Companion.BuildInfo(buildDir, additionalDependenciesClassPath) - val testCaseGenerator = testCaseGeneratorCache - .getOrPut(buildInfo) { - TestSpecificTestCaseGenerator( - buildDir, - additionalDependenciesClassPath, - System.getProperty("java.class.path") - ) - } - return testCaseGenerator.generate(method, mockStrategy) - } - } - - companion object { - private var previousClassLocation: ClassLocation? = null - private lateinit var buildDir: Path - - fun computeAdditionalDependenciesClasspathAndBuildDir( - clazz: Class<*>, - additionalDependencies: Array> - ): String { - val additionalDependenciesClassPath = additionalDependencies - .map { locateClass(it) } - .joinToString(File.pathSeparator) { findPathToClassFiles(it).toAbsolutePath().toString() } - val classLocation = locateClass(clazz) - if (classLocation != previousClassLocation) { - buildDir = findPathToClassFiles(classLocation) - previousClassLocation = classLocation - } - return additionalDependenciesClassPath - } - } - - data class MethodWithMockStrategy( - val method: ExecutableId, - val mockStrategy: MockStrategyApi, - val substituteStatics: Boolean - ) - - data class MethodResult(val testSet: UtMethodTestSet, val coverage: Coverage) -} - -@Suppress("UNCHECKED_CAST") -// TODO please use matcher.reflect().call(...) when it will be ready, currently call isn't supported in kotlin reflect -public fun invokeMatcher(matcher: Function, params: List) = when (matcher) { - is Function1<*, *> -> (matcher as Function1).invoke(params[0]) - is Function2<*, *, *> -> (matcher as Function2).invoke(params[0], params[1]) - is Function3<*, *, *, *> -> (matcher as Function3).invoke( - params[0], params[1], params[2] - ) - is Function4<*, *, *, *, *> -> (matcher as Function4).invoke( - params[0], params[1], params[2], params[3] - ) - is Function5<*, *, *, *, *, *> -> (matcher as Function5).invoke( - params[0], params[1], params[2], params[3], params[4], - ) - is Function6<*, *, *, *, *, *, *> -> (matcher as Function6).invoke( - params[0], params[1], params[2], params[3], params[4], params[5], - ) - is Function7<*, *, *, *, *, *, *, *> -> (matcher as Function7).invoke( - params[0], params[1], params[2], params[3], params[4], params[5], params[6], - ) - else -> error("Function with arity > 7 not supported") -} - -fun between(bounds: IntRange) = ExecutionsNumberMatcher("$bounds") { it in bounds } -val ignoreExecutionsNumber = ExecutionsNumberMatcher("Do not calculate") { it > 0 } - -fun atLeast(percents: Int) = AtLeast(percents) - -fun signatureOf(function: Function<*>): String { - function as KFunction - return function.executableId.signature -} - -fun MockInfo.mocksMethod(method: Function<*>) = signatureOf(method) == this.method.signature - -fun List.singleMockOrNull(field: String, method: Function<*>): MockInfo? = - singleOrNull { (it.mock as? FieldMockTarget)?.field == field && it.mocksMethod(method) } - -fun List.singleMock(field: String, method: Function<*>): MockInfo = - single { (it.mock as? FieldMockTarget)?.field == field && it.mocksMethod(method) } - -fun List.singleMock(id: MockId, method: Function<*>): MockInfo = - single { (it.mock as? ObjectMockTarget)?.id == id && it.mocksMethod(method) } - -inline fun MockInfo.value(index: Int = 0): T = - when (val value = (values[index] as? UtConcreteValue<*>)?.value) { - is T -> value - else -> error("Unsupported type: ${values[index]}") - } - -fun MockInfo.mockValue(index: Int = 0): UtMockValue = values[index] as UtMockValue - -fun MockInfo.isParameter(num: Int): Boolean = (mock as? ParameterMockTarget)?.index == num - -inline fun Result<*>.isException(): Boolean = exceptionOrNull() is T - -inline fun UtCompositeModel.mockValues(methodName: String): List = - mocks.filterKeys { it.name == methodName }.values.flatten().map { it.primitiveValue() } - -inline fun UtModel.primitiveValue(): T = - (this as? UtPrimitiveModel)?.value as? T ?: error("Can't transform $this to ${T::class}") - -sealed class CoverageMatcher(private val description: String, private val cmp: (Coverage) -> Boolean) { - operator fun invoke(c: Coverage) = cmp(c) - override fun toString() = description -} - -object Full : CoverageMatcher("full coverage", { it.counters.all { it.total == it.covered } }) - -class AtLeast(percents: Int) : CoverageMatcher("at least $percents% coverage", - { it.counters.all { 100 * it.covered >= percents * it.total } }) - -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 withStaticsBeforeAndExceptions(ex: UtValueExecution<*>) = ex.paramsBefore + ex.staticsBefore + ex.returnValue -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 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<*>) = - ex.paramsBefore + listOf(ex.mocks) + listOf(ex.instrumentation) + ex.evaluatedResult - -fun withMocksInstrumentationAndThis(ex: UtValueExecution<*>) = - listOf(ex.callerBefore) + ex.paramsBefore + listOf(ex.mocks) + listOf(ex.instrumentation) + ex.evaluatedResult - -// mutations -fun withParamsMutations(ex: UtValueExecution<*>) = ex.paramsBefore + ex.paramsAfter -fun withMutations(ex: UtValueExecution<*>) = ex.paramsBefore + ex.staticsBefore + ex.paramsAfter + ex.staticsAfter -fun withParamsMutationsAndResult(ex: UtValueExecution<*>) = ex.paramsBefore + ex.paramsAfter + ex.evaluatedResult -fun withMutationsAndResult(ex: UtValueExecution<*>) = - ex.paramsBefore + ex.staticsBefore + ex.paramsAfter + ex.staticsAfter + ex.evaluatedResult - -fun withMutationsAndThis(ex: UtValueExecution<*>) = - mutableListOf().apply { - add(ex.callerBefore) - addAll(ex.paramsBefore) - add(ex.staticsBefore) - - add(ex.callerAfter) - addAll(ex.paramsAfter) - add(ex.staticsAfter) - - add(ex.returnValue) - } - -private val UtValueExecution<*>.callerBefore get() = stateBefore.caller!!.value -private val UtValueExecution<*>.paramsBefore get() = stateBefore.params.map { it.value } -private val UtValueExecution<*>.staticsBefore get() = stateBefore.statics - -private val UtValueExecution<*>.callerAfter get() = stateAfter.caller!!.value -private val UtValueExecution<*>.paramsAfter get() = stateAfter.params.map { it.value } -private val UtValueExecution<*>.staticsAfter get() = stateAfter.statics - -private val UtValueExecution<*>.evaluatedResult get() = returnValue.getOrNull() - -fun keyContain(vararg keys: String) = { summary: String? -> - if (summary != null) { - keys.all { it in summary } - } else false -} - -fun keyMatch(keyText: String) = { summary: String? -> - keyText == summary -} - - -private fun flattenDocStatements(summary: List): List { - val flatten = mutableListOf() - for (s in summary) { - when (s) { - is DocPreTagStatement -> flatten.addAll(flattenDocStatements(s.content)) - is DocClassLinkStmt -> flatten.add(s) - is DocMethodLinkStmt -> flatten.add(s) - is DocCodeStmt -> flatten.add(s) - is DocRegularStmt -> flatten.add(s) - is DocCustomTagStatement -> flatten.add(s) - } - } - return flatten -} - -fun keyContain(vararg keys: DocStatement) = { summary: List? -> - summary?.let { keys.all { key -> key in flattenDocStatements(it) } } ?: false -} - -fun keyMatch(keyStmt: List) = { summary: List? -> - summary?.let { keyStmt == summary } ?: false -} - - -fun Map>.findByName(name: String) = entries.single { name == it.key.name }.value.value -fun Map>.singleValue() = values.single().value - -typealias StaticsType = Map> -private typealias Mocks = List -private typealias Instrumentation = List - - -inline fun withSettingsFromTestFrameworkConfiguration( - config: TestFrameworkConfiguration, - block: () -> T -): T { - val substituteStaticsWithSymbolicVariable = UtSettings.substituteStaticsWithSymbolicVariable - UtSettings.substituteStaticsWithSymbolicVariable = config.resetNonFinalFieldsAfterClinit - - val previousConfig = TestCodeGeneratorPipeline.currentTestFrameworkConfiguration - TestCodeGeneratorPipeline.currentTestFrameworkConfiguration = config - try { - return block() - } finally { - UtSettings.substituteStaticsWithSymbolicVariable = substituteStaticsWithSymbolicVariable - TestCodeGeneratorPipeline.currentTestFrameworkConfiguration = previousConfig - } -} diff --git a/utbot-framework/src/main/kotlin/org/z3/Z3Extension.kt b/utbot-framework/src/main/kotlin/org/z3/Z3Extension.kt index 39fb296e85..ad63ebc5d5 100644 --- a/utbot-framework/src/main/kotlin/org/z3/Z3Extension.kt +++ b/utbot-framework/src/main/kotlin/org/z3/Z3Extension.kt @@ -5,7 +5,7 @@ package com.microsoft.z3 import java.lang.Integer.parseUnsignedInt import java.lang.Long.parseUnsignedLong -fun Model.eval(expr: Expr): Expr = this.eval(expr, true) +fun Model.eval(expr: Expr<*>): Expr<*> = this.eval(expr, true) fun BitVecNum.toIntNum(unsigned: Boolean = false): Any = when (sortSize) { Byte.SIZE_BITS -> parseUnsignedLong(this.toBinaryString(), 2).toByte() @@ -44,7 +44,7 @@ fun FPNum.toFloatingPointNum(): Number = when (sort) { else -> error("Unknown type: $sort") } -fun Context.mkSeqNth(s: SeqExpr, index: Expr): Expr { +fun Context.mkSeqNth(s: SeqExpr<*>, index: Expr<*>): Expr<*> { this.checkContextMatch(s, index) return Expr.create(this, Native.mkSeqNth(nCtx(), s.nativeObject, index.nativeObject)) } \ No newline at end of file diff --git a/utbot-framework/src/main/resources/taint/config.yaml b/utbot-framework/src/main/resources/taint/config.yaml new file mode 100644 index 0000000000..bc708767de --- /dev/null +++ b/utbot-framework/src/main/resources/taint/config.yaml @@ -0,0 +1,124 @@ +sources: + - java.util.Scanner.next: + add-to: return + marks: user-input + - java.io.BufferedReader.readLine: + add-to: return + marks: user-input + - javax.servlet.http.HttpServletRequest.getParameter: + add-to: return + marks: user-input + - java.util.Properties.getProperty: + add-to: return + marks: user-input + - java.sql.ResultSet.getString: + add-to: return + marks: user-input + - javax.servlet.http.HttpServletRequest.getQueryString: + add-to: return + marks: user-input + +cleaners: + - java.lang.String.isEmpty: + remove-from: this + marks: [ ] + conditions: + return: true + +passes: + - java.lang.String.getBytes: + get-from: this + add-to: return + marks: [ ] + conditions: + this: { not: "" } + - java.lang.String.split: + get-from: this + add-to: return + marks: [ ] + conditions: + this: { not: "" } + - java.lang.String.concat: + get-from: this + add-to: return + marks: [ ] + conditions: + this: { not: "" } + - java.lang.String.concat: + get-from: arg1 + add-to: return + marks: [ ] + conditions: + arg1: { not: "" } + - java.lang.StringBuilder.append: + get-from: arg1 + add-to: this + marks: [ ] + conditions: + arg1: { not: "" } + - java.lang.StringBuilder.toString: + get-from: this + add-to: return + marks: [ ] + + - java.sql.Connection.prepareStatement: + get-from: arg1 + add-to: [ this, return ] + marks: [ ] + - java.sql.PreparedStatement.setString: + get-from: arg2 + add-to: this + marks: [ ] + + - java.sql.Statement.addBatch: + get-from: arg1 + add-to: this + marks: [ ] + + - java.io.ByteArrayOutputStream.writeData: + get-from: arg1 + add-to: this + marks: [ ] + - java.io.ByteArrayOutputStream.toByteArray: + get-from: this + add-to: return + marks: [ ] + - java.io.ByteArrayInputStream.: + get-from: arg1 + add-to: [ this, return ] + marks: [ ] + - java.io.ObjectInputStream.: + get-from: arg1 + add-to: [ this, return ] + marks: [ ] + - java.io.ObjectInputStream.readObject: + get-from: this + add-to: return + marks: [ ] + +sinks: + - java.sql.Statement.execute: + check: arg1 + marks: user-input + - java.sql.Statement.executeUpdate: + check: arg1 + marks: user-input + - java.sql.Statement.executeBatch: + check: this + marks: user-input + - java.sql.Statement.executeQuery: + check: arg1 + marks: user-input + + - java.sql.PreparedStatement.execute: + check: this + marks: user-input + - java.sql.PreparedStatement.executeUpdate: + check: this + marks: user-input + - java.sql.PreparedStatement.executeBatch: + check: this + marks: user-input + - java.sql.PreparedStatement.executeQuery: + check: this + marks: user-input diff --git a/utbot-fuzzers/build.gradle.kts b/utbot-fuzzers/build.gradle.kts deleted file mode 100644 index 07015c9dbe..0000000000 --- a/utbot-fuzzers/build.gradle.kts +++ /dev/null @@ -1,30 +0,0 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - -plugins { - id("com.github.johnrengelman.shadow") version "7.1.2" -} - -tasks { - withType { - archiveClassifier.set(" ") - minimize() - } -} - -val sootCommitHash: String by rootProject -val kotlinLoggingVersion: String by rootProject -val rgxgenVersion: String by rootProject - -dependencies { - implementation(project(":utbot-framework-api")) - - implementation("com.github.UnitTestBot:soot:${sootCommitHash}") - implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) - implementation(group = "com.github.curious-odd-man", name = "rgxgen", version = rgxgenVersion) -} - -tasks { - compileJava { - options.compilerArgs = emptyList() - } -} \ No newline at end of file diff --git a/utbot-fuzzers/readme.md b/utbot-fuzzers/readme.md deleted file mode 100644 index f15cb0b06f..0000000000 --- a/utbot-fuzzers/readme.md +++ /dev/null @@ -1,206 +0,0 @@ -# UTBot Fuzzer - -Fuzzer generates method input values to improve method coverage or find unexpected errors. In UTBot next strategies can be used to find values like: - -* **Default values** for objects, e.g. 0, 0.0, empty string or null values. -* **Corner case values** of primitives types, e.g. Integer.MAX_VALUE, Double.POSITIVE_INFINITY, etc. -* **Method constants and their simple mutations** of primitive types. -* **Objects** created via its constructors or field mutators. - -After values are found fuzzer creates all its possible combinations and runs methods with these combinations. - -For example, if a method has two parameters of types `boolean` and `int` the follow values can be found: -``` -boolean = [false, true] -int = [0, MAX_VALUE, MIN_VALUE] -``` - -Now, fuzzer creates `2 * 3 = 6` combinations of them: - -``` -[false, 0], [false, MAX_VALUE], [false, MIN_VALUE], [true, 0], [true, MAX_VALUE], [true, MIN_VALUE] -``` - -To find more branches of execution as fast as possible fuzzer also shuffles -combinations and supplies them for the running. - -## Design - -Fuzzer requires model providers that create a set of `UtModel` for a given `ClassId`. -Fuzzer iterates through these providers and creates models, which are used for generating combinations later. -Each combination contains concrete values that can be accepted by the method. -For example, if a method has signature with `String, double, int` as parameters then fuzzer can create combination `"sometext", Double.POSITIVE_INFINITY, 0`. - -Fuzzer's entry point is: -```kotlin -// org.utbot.fuzzer.FuzzerKt -fun fuzz(method: FuzzedMethodDescription, vararg models: ModelProvider): Sequence> -``` - -`FuzzedMethodDescription` stores comprehensive information about a method: -* signature (parameters and return types) -* name/package/class (optional) -* constants found in the method body (should be replaced with CGF when possible) - -`ModelProvider` provides models for a give parameters set as described below. - -Fuzz method returns a sequence of acceptable values for the method in random order. The sequence is lazy. - -Model provider should implement - -```kotlin -fun generate(description: FuzzedMethodDescription): Sequence -``` -For every parameter should exist at least one `UtModel`. `ModelProvider.withFallback` can be used to process those classes which cannot be processed by provider. -Several providers can be combined into one by using `ModelProvider.with(anotherModel: ModelProvider)`. - -Common way to generate all combinations is: - -```kotlin -ObjectModelProvider() - .with(PrimitiveModelProvider) - // ... - .with(ObjectModelProvider) - .withFallback { classId -> - createDefaultModelByClass(classID) - } -``` -or - -```kotlin -// org.utbot.fuzzer.FuzzerKt -fun defaultModelProviders(idGenerator: IntSupplier) -``` - -## List of builtin providers - -### PrimitiveDefaultsModelProvider - -Creates default values for every primitive types: - -``` -boolean: false -byte: 0 -short: 0 -int: 0 -long: 0 -float: 0.0 -double: 0.0 -char: \u0000 -string: "" -``` - -### PrimitiveModelProvider - -Creates default values and some corner case values such as Integer.MAX_VALUE, 0.0, Double.NaN, empty string, etc. - -``` -boolean: false, true -byte: 0, 1, -1, Byte.MIN_VALUE, Byte.MAX_VALUE -short: 0, 1, -1, Short.MIN_VALUE, Short.MAX_VALUE -int: 0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE -long: 0, 1, -1, Long.MIN_VALUE, Long.MAX_VALUE -float: 0.0, 1.1, -1.1, Float.MIN_VALUE, Float.MAX_VALUE, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NaN -double: 0.0, 1.1, -1.1, Double.MIN_VALUE, Double.MAX_VALUE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NaN -char: Char.MIN_VALUE, Char.MAX_VALUE -string: "", " ", "string", "\n\t\r" -``` - -### PrimitiveWrapperModelProvider - -Creates primitive models for boxed types: `Boolean`, `Byte`, `Short`, `Integer`, `Long`, `Double`, `Float`, `Character`, `String` - -### ConstantsModelProvider - -Uses information about concrete values from `FuzzedMethodDescription#concreteValues` to generate simple values. -Only primitive values are supported. - -### NullModelProvider - -Creates `UtNullModel` for every reference class. - -### EnumModelProvider - -Creates models for any enum type. - -### CollectionModelProvider - -Creates concrete collections for collection interfaces: `List`, `Set`, `Map`, `Collection`, `Iterable`, `Iterator` - -### ArrayModelProvider - -Creates an empty and non-empty for any type. - -### ObjectModelProvider - -ObjectModelProvider is the most sophisticated provider. It creates model of class that has public constructors -and public mutators (fields or setters/getters). If class has constructor that accepts another object within an argument that value -is created recursively. Depth of recursion is limited to 1. Thus, for inner object fuzzing doesn't try to use every -constructor but find the one with the least number of parameters and, if it is possible, only -constructor with primitives values. If there is available only constructor with another object as a parameter then -`null` is passed to it. - -Let's look at this example: - -```java -class A { - private int a; - private Object object; - - public A(int a, A o) { - this.a = a; - this.o = o; - } -} -``` - -For it fuzzing create these models: -``` -new Object(0, new A(0, null)); -new Object(Integer.MIN_VALUE, new A(0, null)); -new Object(Integer.MAX_VALUE, new A(0, null)); -new Object(0, new A(Integer.MIN_VALUE, null)); -new Object(Integer.MIN_VALUE, new A(Integer.MIN_VALUE, null)); -new Object(Integer.MAX_VALUE, new A(Integer.MIN_VALUE, null)); -new Object(0, new A(Integer.MAX_VALUE, null)); -new Object(Integer.MIN_VALUE, new A(Integer.MAX_VALUE, null)); -new Object(Integer.MAX_VALUE, new A(Integer.MAX_VALUE, null)); -``` - -For classes that have empty public constructor and field mutators all those mutators will be fuzzed as well. -Field mutators are listed below: -* public or package-private (and accessible) non-final non-static fields -* pairs of setter/getter that satisfy the common agreement: - * setter/getter is public or package-private (and accessible) - * have field name as a postfix, e.g.: `int myField -> * setMyField(int v)/int getMyField()`, where * means any returned type - -For example, fields _a_, _b_ and _d_ will be fuzzed, but _c_ and _e_ will not: - -```java -class A { - int a; - public char b; - public final int c = 0; - private String d; - private boolean e; - - public A setD(String s) { - this.d = s; - return this; - } - - public String getD() { - return d; - } - - public boolean getE() { - return e; - } -} -``` - -### Other providers - -There are several other providers that can find some values, using addition information, -like `CharToStringModelProvider` that takes all chars found in `charAt(i) == c` statement -and merge them into several strings. \ 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 deleted file mode 100644 index 0dd190f1d1..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/CartesianProduct.kt +++ /dev/null @@ -1,69 +0,0 @@ -package org.utbot.fuzzer - -import kotlin.jvm.Throws -import kotlin.random.Random - -/** - * Creates iterable for all values of cartesian product of `lists`. - */ -class CartesianProduct( - private val lists: List>, - private val random: Random? = null -): Iterable> { - - /** - * Estimated number of all combinations. - */ - val estimatedSize: Long - get() = Combinations(*lists.map { it.size }.toIntArray()).size - - @Throws(TooManyCombinationsException::class) - fun asSequence(): Sequence> { - val combinations = Combinations(*lists.map { it.size }.toIntArray()) - val sequence = if (random != null) { - sequence { - forEachChunk(Int.MAX_VALUE, combinations.size) { startIndex, combinationSize, _ -> - val permutation = PseudoShuffledIntProgression(combinationSize, random) - val temp = IntArray(size = lists.size) - for (it in 0 until combinationSize) { - yield(combinations[permutation[it] + startIndex, temp]) - } - } - } - } else { - combinations.asSequence() - } - return sequence.map { combination -> - combination.mapIndexedTo(ArrayList(combination.size)) { index, value -> lists[index][value] } - } - } - - override fun iterator(): Iterator> = asSequence().iterator() - - companion object { - /** - * Consumer for processing blocks of input larger block. - * - * If source example is sized to 12 and every block is sized to 5 then consumer should be called 3 times with these values: - * - * 1. start = 0, size = 5, remain = 7 - * 2. start = 5, size = 5, remain = 2 - * 3. start = 10, size = 2, remain = 0 - * - * The sum of start, size and remain should be equal to source block size. - */ - internal inline fun forEachChunk( - chunkSize: Int, - totalSize: Long, - block: (start: Long, size: Int, remain: Long) -> Unit - ) { - val iterationsCount = totalSize / chunkSize + if (totalSize % chunkSize == 0L) 0 else 1 - (0L until iterationsCount).forEach { iteration -> - val start = iteration * chunkSize - val size = minOf(chunkSize.toLong(), totalSize - start).toInt() - val remain = totalSize - size - start - block(start, size, remain) - } - } - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedParameter.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedParameter.kt deleted file mode 100644 index 2009a5b9e2..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedParameter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.utbot.fuzzer - -/** - * Fuzzed parameter of a method. - * - * @param index of the parameter in method signature - * @param value fuzzed values - */ -class FuzzedParameter( - val index: Int, - val value: FuzzedValue -) { - operator fun component1() = index - operator fun component2() = value -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt deleted file mode 100644 index a46080cba2..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.framework.plugin.api.ClassId - -/** - * Contains information about classId and generic types, that should be applied. - * - * Currently, there's some limitation for generics that are supported: - * 1. Only concrete types and collections are supported - * 2. No relative types like: `Map` - * - * Note, that this class can be replaced by API mechanism for collecting parametrized types, - * but at the moment it doesn't fully support all necessary operations. - * - * @see ClassId.typeParameters - */ -class FuzzedType( - val classId: ClassId, - val generics: List = emptyList() -) \ 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 deleted file mode 100644 index 55e78704a9..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt +++ /dev/null @@ -1,26 +0,0 @@ -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. - */ -open 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 deleted file mode 100644 index 7f0f699511..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt +++ /dev/null @@ -1,266 +0,0 @@ -package org.utbot.fuzzer - -import mu.KotlinLogging -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.util.intClassId -import org.utbot.framework.plugin.api.util.voidClassId -import org.utbot.fuzzer.mutators.NumberRandomMutator -import org.utbot.fuzzer.mutators.RegexStringModelMutator -import org.utbot.fuzzer.mutators.StringRandomMutator -import org.utbot.fuzzer.providers.ArrayModelProvider -import org.utbot.fuzzer.providers.CharToStringModelProvider -import org.utbot.fuzzer.providers.CollectionWithEmptyStatesModelProvider -import org.utbot.fuzzer.providers.ConstantsModelProvider -import org.utbot.fuzzer.providers.EnumModelProvider -import org.utbot.fuzzer.providers.CollectionWithModificationModelProvider -import org.utbot.fuzzer.providers.NumberClassModelProvider -import org.utbot.fuzzer.providers.ObjectModelProvider -import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider -import org.utbot.fuzzer.providers.PrimitiveWrapperModelProvider -import org.utbot.fuzzer.providers.PrimitivesModelProvider -import org.utbot.fuzzer.providers.RegexModelProvider -import org.utbot.fuzzer.providers.StringConstantModelProvider -import java.util.* -import java.util.concurrent.atomic.AtomicInteger -import kotlin.random.Random -import org.utbot.fuzzer.providers.DateConstantModelProvider -import org.utbot.fuzzer.providers.PrimitiveRandomModelProvider -import org.utbot.fuzzer.providers.RecursiveModelProvider - -private val logger by lazy { KotlinLogging.logger {} } - -/** - * Identifier generator interface for fuzzer model providers. - * - * Provides fresh identifiers for generated models. - * - * Warning: specific generators are not guaranteed to be thread-safe. - * - * @param Id the identifier type (e.g., [Int] for [UtReferenceModel] providers) - */ -interface IdGenerator { - /** - * Create a fresh identifier. Each subsequent call should return a different value. - * - * The method is not guaranteed to be thread-safe, unless an implementation makes such a guarantee. - */ - fun createId(): Id -} - -/** - * Identity preserving identifier generator interface. - * - * It allows to optionally save identifiers assigned to specific objects and later get the same identifiers - * for these objects instead of fresh identifiers. This feature is necessary, for example, to implement reference - * equality for enum models. - * - * Warning: specific generators are not guaranteed to be thread-safe. - * - * @param Id the identifier type (e.g., [Int] for [UtReferenceModel] providers) - */ -interface IdentityPreservingIdGenerator : IdGenerator { - /** - * Return an identifier for a specified non-null object. If an identifier has already been assigned - * to an object, subsequent calls should return the same identifier for this object. - * - * Note: the interface does not specify whether reference equality or regular `equals`/`compareTo` equality - * will be used to compare objects. Each implementation may provide these guarantees by itself. - * - * The method is not guaranteed to be thread-safe, unless an implementation makes such a guarantee. - */ - fun getOrCreateIdForValue(value: Any): Id -} - -/** - * An identity preserving id generator for fuzzer value providers that returns identifiers of type [Int]. - * - * When identity-preserving identifier is requested, objects are compared by reference. - * The generator is not thread-safe. - * - * @param lowerBound an integer value so that any generated identifier is strictly greater than it. - * - * Warning: when generating [UtReferenceModel] identifiers, no identifier should be equal to zero, - * as this value is reserved for [UtNullModel]. To guarantee it, [lowerBound] should never be negative. - * Avoid using custom lower bounds (maybe except fuzzer unit tests), use the predefined default value instead. - */ -class ReferencePreservingIntIdGenerator(lowerBound: Int = DEFAULT_LOWER_BOUND) : IdentityPreservingIdGenerator { - private val lastId: AtomicInteger = AtomicInteger(lowerBound) - private val cache: IdentityHashMap = IdentityHashMap() - - override fun getOrCreateIdForValue(value: Any): Int { - return cache.getOrPut(value) { createId() } - } - - override fun createId(): Int { - return lastId.incrementAndGet() - } - - companion object { - /** - * The default lower bound (all generated integer identifiers will be greater than it). - * - * It is defined as a large value because all synthetic [UtModel] instances - * must have greater identifiers than the real models. - */ - const val DEFAULT_LOWER_BOUND: Int = 1500_000_000 - } -} - -/** - * Generated by fuzzer sequence of values which can be passed into the method. - */ -fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvider): Sequence> { - if (modelProviders.isEmpty()) { - throw IllegalArgumentException("At least one model provider is required") - } - - val values = List>(description.parameters.size) { mutableListOf() } - modelProviders.forEach { fuzzingProvider -> - fuzzingProvider.generate(description).forEach { (index, model) -> - values[index].add(model) - } - } - description.parameters.forEachIndexed { index, classId -> - val models = values[index] - if (models.isEmpty()) { - logger.warn { "There's no models provided classId=$classId. No suitable values are generated for ${description.name}" } - return emptySequence() - } - } - return CartesianProduct(values, Random(0L)).asSequence() -} - -/** - * Wraps sequence of values, iterates through them and mutates. - * - * Mutation when possible is generated after every value of source sequence and then supplies values until it needed. - * [statistics] should not be updated by this method, but can be changed by caller. - */ -fun Sequence>.withMutations(statistics: FuzzerStatistics, description: FuzzedMethodDescription, vararg mutators: ModelMutator) = sequence { - val fvi = iterator() - val mutatorList = mutators.toList() - val random = Random(0L) - while (fvi.hasNext()) { - // Takes a value that was generated by model providers and submits it - yield(fvi.next()) - // Fuzzing can generate values that don't recover new paths. - // So, fuzzing tries to mutate values on each loop - // if there are too many attempts to find new paths without mutations. - yieldMutated(statistics, description, mutatorList, random) - } - // try mutations if fuzzer tried all combinations if any seeds are available - @Suppress("ControlFlowWithEmptyBody") - while (yieldMutated(statistics, description, mutatorList, random)) {} -} - -/** - * Create a sequence that contains all [defaultValues] plus any value which is found as fuzzing with concrete values. - * - * Method is useful for generating some bound values, - * for example, when for code `array.length > 4` there are 2 values in concrete value: 4 and 5. - * - * All values after filtering are cast to [Int]. - */ -fun fuzzNumbers(concreteValues: Collection, vararg defaultValues: Int, filter: (Number) -> Boolean = { true }): Sequence { - val description = FuzzedMethodDescription("helper: number fuzzing", voidClassId, listOf(intClassId), concreteValues) - val fuzzedValues = fuzz(description, ConstantsModelProvider) - .mapNotNull { ((it.single().model as? UtPrimitiveModel)?.value as? Number) } - .filter(filter) - .map { it.toInt() } - return (defaultValues.asSequence() + fuzzedValues).distinct() -} - -/** - * Creates a model provider from a list of default providers. - */ -fun defaultModelProviders(idGenerator: IdentityPreservingIdGenerator): ModelProvider { - return modelProviderForRecursiveCalls(idGenerator, recursionDepth = -1) - .with(ObjectModelProvider(idGenerator)) - .with(ArrayModelProvider(idGenerator)) - .with(CollectionWithModificationModelProvider(idGenerator)) - .exceptIsInstance() - .except(PrimitiveDefaultsModelProvider) - .with(PrimitivesModelProvider) -} - -/** - * Creates a model provider from a list of providers that we want to use by default in [RecursiveModelProvider] - */ -internal fun modelProviderForRecursiveCalls(idGenerator: IdentityPreservingIdGenerator, recursionDepth: Int): ModelProvider { - val nonRecursiveProviders = ModelProvider.of( - CollectionWithEmptyStatesModelProvider(idGenerator), - EnumModelProvider(idGenerator), - DateConstantModelProvider(idGenerator), - RegexModelProvider, - StringConstantModelProvider, - CharToStringModelProvider, - ConstantsModelProvider, - PrimitiveRandomModelProvider(Random(0)), - PrimitiveDefaultsModelProvider, - PrimitiveWrapperModelProvider, - NumberClassModelProvider(idGenerator, Random(0)), - ) - - return if (recursionDepth >= 0) { - nonRecursiveProviders - .with(ObjectModelProvider(idGenerator, recursionDepth)) - .with(ArrayModelProvider(idGenerator, recursionDepth)) - .with(CollectionWithModificationModelProvider(idGenerator, recursionDepth)) - } else { - nonRecursiveProviders - } -} - - -fun defaultModelMutators(): List = listOf( - StringRandomMutator, - RegexStringModelMutator, - NumberRandomMutator, -) - -/** - * Tries to mutate a random value from the seed. - * - * Returns `null` if didn't try to do any mutation. - */ -fun mutateRandomValueOrNull( - statistics: FuzzerStatistics, - description: FuzzedMethodDescription, - mutators: List = defaultModelMutators(), - random: Random = Random, -): List? { - if (mutators.isEmpty()) return null - val values = statistics.takeIf { it.isNotEmpty() }?.randomValues(random) ?: return null - var newValues :MutableList? = null - mutators.asSequence() - .forEach { mut -> - mut.mutate(description, values, random).forEach { (index, value) -> - newValues = (newValues ?: values.toMutableList()) - newValues?.set(index, value) - } - } - return newValues -} - -/** - * Run mutations and yields values into the sequence. - * - * Mutations are supplied infinitely until [repeat] returns true. [repeat] is run before mutation. - * - * @param statistics coverage-based seed - * @param description method description - * @param mutators mutators which are applied to the random value - * @param random instance that is used to choose random index from the [statistics] - */ -suspend fun SequenceScope>.yieldMutated( - statistics: FuzzerStatistics, - description: FuzzedMethodDescription, - mutators: List, - random: Random -) : Boolean { - mutateRandomValueOrNull(statistics, description, mutators, random)?.let { - yield(it) - return true - } - return false -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt deleted file mode 100644 index 521fa7457d..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzerStatistics.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.utbot.fuzzer - -import kotlin.math.pow -import kotlin.random.Random - -/** - * Stores information that can be useful for fuzzing such as coverage, run count, etc. - */ -interface FuzzerStatistics { - - val seeds: Collection - - /** - * Returns a random seed to process. - */ - fun randomSeed(random: Random): K? - - fun randomValues(random: Random): List? - - fun executions(seed: K): Int - - operator fun get(seed: K): List? - - fun isEmpty(): Boolean - - fun isNotEmpty(): Boolean { - return !isEmpty() - } -} - -class TrieBasedFuzzerStatistics( - private val values: LinkedHashMap, List> = linkedMapOf() -) : FuzzerStatistics> { - - override val seeds: Collection> - get() = values.keys - - override fun randomSeed(random: Random): Trie.Node? { - return values.keys.elementAtOrNull(randomIndex(random)) - } - - override fun isEmpty(): Boolean { - return values.isEmpty() - } - - override fun isNotEmpty(): Boolean { - return values.isNotEmpty() - } - - override fun randomValues(random: Random): List? { - return values.values.elementAtOrNull(randomIndex(random)) - } - - private fun randomIndex(random: Random): Int { - val frequencies = DoubleArray(values.size).also { f -> - values.keys.forEachIndexed { index, key -> - f[index] = 1 / key.count.toDouble().pow(2) - } - } - return random.chooseOne(frequencies) - } - - override fun get(seed: Trie.Node): List? { - return values[seed] - } - - override fun executions(seed: Trie.Node): Int { - return seed.count - } - -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt deleted file mode 100644 index 486a583ad9..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelMutator.kt +++ /dev/null @@ -1,44 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.framework.plugin.api.UtModel -import kotlin.random.Random - -/** - * Mutates values and returns it. - */ -interface ModelMutator { - - /** - * Mutates values set. - * - * Default implementation iterates through values and delegates to `mutate(FuzzedMethodDescription, Int, Random)`. - */ - fun mutate( - description: FuzzedMethodDescription, - parameters: List, - random: Random, - ) : List { - return parameters - .asSequence() - .mapIndexedNotNull { index, fuzzedValue -> - mutate(description, index, fuzzedValue, random)?.let { mutated -> - FuzzedParameter(index, mutated) - } - } - .toList() - } - - /** - * Mutate a single value if it is possible. - */ - fun mutate( - description: FuzzedMethodDescription, - index: Int, - value: FuzzedValue, - random: Random - ) : FuzzedValue? - - fun UtModel.mutatedFrom(template: FuzzedValue, block: FuzzedValue.() -> Unit = {}): FuzzedValue { - return FuzzedValue(this, template.createdBy).apply(block) - } -} \ 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 deleted file mode 100644 index 4b28dc0dcf..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt +++ /dev/null @@ -1,164 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.ClassId - -fun interface ModelProvider { - - /** - * Generates values for the method. - * - * @param description a fuzzed method description - * @return sequence that produces [FuzzedParameter]. - */ - fun generate(description: FuzzedMethodDescription): Sequence - - /** - * Combines this model provider with `anotherModelProvider` into one instance. - * - * This model provider is called before `anotherModelProvider`. - */ - 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)) - } - } - - /** - * Applies [transform] for current provider - */ - fun map(transform: (ModelProvider) -> ModelProvider): ModelProvider { - return if (this is Combined) { - Combined(providers.map(transform)) - } else { - transform(this) - } - } - - /** - * Creates [ModelProvider] that passes unprocessed classes to `modelProvider`. - * - * 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 modelProvider is called and every value of [ClassId] is collected which wasn't created by this model provider. - */ - fun withFallback(modelProvider: ModelProvider) : ModelProvider { - val thisModelProvider = this - return ModelProvider { description -> - sequence { - val providedByDelegateMethodParameters = mutableSetOf() - thisModelProvider.generate(description).forEach { (index, model) -> - providedByDelegateMethodParameters += index - yieldValue(index, model) - } - val missingParameters = - (0 until description.parameters.size).filter { !providedByDelegateMethodParameters.contains(it) } - if (missingParameters.isNotEmpty()) { - val values = mutableMapOf>() - modelProvider.generate(description).forEach { (i, m) -> values.computeIfAbsent(i) { mutableListOf() }.add(m) } - missingParameters.forEach { index -> - values[index]?.let { models -> - models.forEach { model -> - yieldValue(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( ModelProvider { description -> - sequence { - description.parametersMap.forEach { (classId, indices) -> - fallbackModelSupplier(classId)?.let { model -> - indices.forEach { index -> - yieldValue(index, model.fuzzed()) - } - } - } - } - }) - } - - companion object { - @JvmStatic - fun of(vararg providers: ModelProvider): ModelProvider { - return Combined(providers.toList()) - } - - suspend fun SequenceScope.yieldValue(index: Int, value: FuzzedValue) { - yield(FuzzedParameter(index, value)) - } - - suspend fun SequenceScope.yieldAllValues(indices: List, models: Sequence) { - indices.forEach { index -> - models.forEach { model -> - yieldValue(index, model) - } - } - } - - suspend fun SequenceScope.yieldAllValues(indices: List, models: List) { - yieldAllValues(indices, models.asSequence()) - } - } - - /** - * Wrapper class that delegates implementation to the [providers]. - */ - private class Combined(providers: List): ModelProvider { - val providers: List - - init { - // Flattening to avoid Combined inside Combined (for correct work of except, map, etc.) - this.providers = providers.flatMap { - if (it is Combined) - it.providers - else - listOf(it) - } - } - - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - providers.forEach { provider -> - provider.generate(description).forEach { - yieldValue(it.index, it.value) - } - } - } - } - - 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/UtFuzzedExecution.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/UtFuzzedExecution.kt deleted file mode 100644 index 211ec0fcfb..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/UtFuzzedExecution.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.framework.plugin.api.Coverage -import org.utbot.framework.plugin.api.DocStatement -import org.utbot.framework.plugin.api.EnvironmentModels -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.UtExecution -import org.utbot.framework.plugin.api.UtExecutionResult - -/** - * Fuzzed execution. - * - * Contains: - * - execution parameters, including thisInstance; - * - result; - * - static fields changed during execution; - * - coverage information (instructions) if this execution was obtained from the concrete execution. - * - comments, method names and display names created by utbot-summary module. - */ -class UtFuzzedExecution( - stateBefore: EnvironmentModels, - stateAfter: EnvironmentModels, - result: UtExecutionResult, - coverage: Coverage? = null, - summary: List? = null, - testMethodName: String? = null, - displayName: String? = null, - val fuzzingValues: List? = null, - val fuzzedMethodDescription: FuzzedMethodDescription? = null -) : UtExecution(stateBefore, stateAfter, result, coverage, summary, testMethodName, displayName) { - /** - * By design the 'before' and 'after' states contain info about the same fields. - * It means that it is not possible for a field to be present at 'before' and to be absent at 'after'. - * The reverse is also impossible. - */ - val staticFields: Set - get() = stateBefore.statics.keys // TODO: should we keep it for the Fuzzed Execution? - - override fun toString(): String = buildString { - append("UtFuzzedExecution(") - appendLine() - - append(":") - appendLine() - append(stateBefore) - appendLine() - - append(":") - appendLine() - append(stateAfter) - appendLine() - - append(":") - appendLine() - append(result) - append(")") - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt deleted file mode 100644 index 9634d46112..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/NumberRandomMutator.kt +++ /dev/null @@ -1,60 +0,0 @@ -package org.utbot.fuzzer.mutators - -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelMutator -import org.utbot.fuzzer.invertBit -import kotlin.random.Random - -/** - * Mutates any [Number] changing random bit. - */ -object NumberRandomMutator : ModelMutator { - - override fun mutate( - description: FuzzedMethodDescription, - index: Int, - value: FuzzedValue, - random: Random - ): FuzzedValue? { - val model = value.model - return if (model is UtPrimitiveModel && model.value is Number) { - val newValue = changeRandomBit(random, model.value as Number) - UtPrimitiveModel(newValue).mutatedFrom(value) { - summary = "%var% = $newValue (mutated from ${model.value})" - } - } else null - } - - private fun changeRandomBit(random: Random, number: Number): Number { - val size = when (number) { - is Byte -> Byte.SIZE_BITS - is Short -> Short.SIZE_BITS - is Int -> Int.SIZE_BITS - is Float -> Float.SIZE_BITS - is Long -> Long.SIZE_BITS - is Double -> Double.SIZE_BITS - else -> error("Unknown type: ${number.javaClass}") - } - val asLong = when (number) { - is Byte, is Short, is Int -> number.toLong() - is Long -> number - is Float -> number.toRawBits().toLong() - is Double -> number.toRawBits() - else -> error("Unknown type: ${number.javaClass}") - } - val bitIndex = random.nextInt(size) - val mutated = asLong.invertBit(bitIndex) - return when (number) { - is Byte -> mutated.toByte() - is Short -> mutated.toShort() - is Int -> mutated.toInt() - is Float -> Float.fromBits(mutated.toInt()) - is Long -> mutated - is Double -> Double.fromBits(mutated) - else -> error("Unknown type: ${number.javaClass}") - } - } -} - diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/RegexStringModelMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/RegexStringModelMutator.kt deleted file mode 100644 index c9285eb7b8..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/RegexStringModelMutator.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.utbot.fuzzer.mutators - -import com.github.curiousoddman.rgxgen.RgxGen -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelMutator -import org.utbot.fuzzer.providers.RegexFuzzedValue -import org.utbot.fuzzer.providers.RegexModelProvider -import kotlin.random.Random -import kotlin.random.asJavaRandom - -/** - * Provides different regex value for a concrete regex pattern - */ -object RegexStringModelMutator : ModelMutator { - - override fun mutate( - description: FuzzedMethodDescription, - index: Int, - value: FuzzedValue, - random: Random - ): FuzzedValue? { - if (value is RegexFuzzedValue) { - val string = RgxGen(value.regex).apply { - setProperties(RegexModelProvider.rgxGenProperties) - }.generate(random.asJavaRandom()) - return RegexFuzzedValue(UtPrimitiveModel(string).mutatedFrom(value) { - summary = "%var% = mutated regex ${value.regex}" - }, value.regex) - } - return null - } - -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt deleted file mode 100644 index 3274fb0254..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/mutators/StringRandomMutator.kt +++ /dev/null @@ -1,74 +0,0 @@ -package org.utbot.fuzzer.mutators - -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.util.stringClassId -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelMutator -import org.utbot.fuzzer.flipCoin -import kotlin.random.Random - -/** - * Mutates string by adding and/or removal symbol at random position. - */ -object StringRandomMutator : ModelMutator { - - override fun mutate( - description: FuzzedMethodDescription, - index: Int, - value: FuzzedValue, - random: Random - ): FuzzedValue? { - return (value.model as? UtPrimitiveModel) - ?.takeIf { it.classId == stringClassId } - ?.let { model -> - mutate(random, model.value as String).let { - UtPrimitiveModel(it).mutatedFrom(value) { - summary = "%var% = mutated string" - } - } - } - } - - private fun mutate(random: Random, string: String): String { - // we can miss some mutation for a purpose - val position = random.nextInt(string.length + 1) - var result: String = string - if (random.flipCoin(probability = 50)) { - result = tryRemoveChar(random, result, position) ?: string - } - if (random.flipCoin(probability = 50) && result.length < 1000) { - result = tryAddChar(random, result, position) - } - return result - } - - private fun tryAddChar(random: Random, value: String, position: Int): String { - val charToMutate = if (value.isNotEmpty()) { - value.random(random) - } else { - // use any meaningful character from the ascii table - random.nextInt(33, 127).toChar() - } - return buildString { - append(value.substring(0, position)) - // try to change char to some that is close enough to origin char - val charTableSpread = 64 - if (random.nextBoolean()) { - append(charToMutate - random.nextInt(1, charTableSpread)) - } else { - append(charToMutate + random.nextInt(1, charTableSpread)) - } - append(value.substring(position, value.length)) - } - } - - private fun tryRemoveChar(random: Random, value: String, position: Int): String? { - if (position >= value.length) return null - val toRemove = random.nextInt(value.length) - return buildString { - append(value.substring(0, toRemove)) - append(value.substring(toRemove + 1, value.length)) - } - } -} \ 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 deleted file mode 100644 index 1475850b38..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/AbstractModelProvider.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.utbot.fuzzer.providers - -import org.utbot.framework.plugin.api.* -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue - -/** - * Simple model implementation. - */ -@Suppress("unused") -abstract class AbstractModelProvider: ModelProvider { - override fun generate(description: FuzzedMethodDescription): Sequence = sequence{ - description.parametersMap.forEach { (classId, indices) -> - toModel(classId)?.let { defaultModel -> - indices.forEach { index -> - yieldValue(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 deleted file mode 100644 index 5dbf6f33c0..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ArrayModelProvider.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.utbot.fuzzer.providers - -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.UtArrayModel -import org.utbot.framework.plugin.api.UtModel -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.FuzzedType -import org.utbot.fuzzer.IdentityPreservingIdGenerator -import org.utbot.fuzzer.fuzzNumbers - -class ArrayModelProvider( - idGenerator: IdentityPreservingIdGenerator, - recursionDepthLeft: Int = 2 -) : RecursiveModelProvider(idGenerator, recursionDepthLeft) { - - override fun newInstance(parentProvider: RecursiveModelProvider, constructor: ModelConstructor): RecursiveModelProvider { - val provider = ArrayModelProvider(parentProvider.idGenerator, parentProvider.recursionDepthLeft - 1) - provider.copySettings(parentProvider) - provider.totalLimit = minOf(parentProvider.totalLimit, constructor.limit) - return provider - } - - override fun generateModelConstructors( - description: FuzzedMethodDescription, - parameterIndex: Int, - classId: ClassId, - ): Sequence = sequence { - if (!classId.isArray) return@sequence - val lengths = fuzzNumbers(description.concreteValues, 0, 3) { it in 1..10 }.toList() - lengths.forEach { length -> - yield(ModelConstructor(listOf(FuzzedType(classId.elementClassId!!)), repeat = length) { values -> - createFuzzedArrayModel(classId, length, values.map { it.model } ) - }.apply { - limit = (totalLimit / lengths.size).coerceAtLeast(1) - }) - } - } - - private fun createFuzzedArrayModel(arrayClassId: ClassId, length: Int, values: List?) = - UtArrayModel( - idGenerator.createId(), - arrayClassId, - length, - arrayClassId.elementClassId!!.defaultValueModel(), - values?.withIndex()?.associate { it.index to it.value }?.toMutableMap() ?: mutableMapOf() - ).fuzzed { - this.summary = "%var% = ${arrayClassId.elementClassId!!.simpleName}[$length]" - } -} \ 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 deleted file mode 100644 index 450cea7115..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CharToStringModelProvider.kt +++ /dev/null @@ -1,33 +0,0 @@ -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.FuzzedParameter -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue - -/** - * Collects all char constants and creates string with them. - */ -object CharToStringModelProvider : ModelProvider { - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - val indices = description.parametersMap[stringClassId] ?: return@sequence - if (indices.isNotEmpty()) { - val string = description.concreteValues.asSequence() - .filter { it.classId == charClassId } - .map { it.value } - .filterIsInstance() - .joinToString(separator = "") - if (string.isNotEmpty()) { - sequenceOf(string.reversed(), string).forEach { str -> - val model = UtPrimitiveModel(str).fuzzed() - indices.forEach { - yieldValue(it, model) - } - } - } - } - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionWithEmptyStatesModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionWithEmptyStatesModelProvider.kt deleted file mode 100644 index c3f18831f7..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionWithEmptyStatesModelProvider.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.utbot.fuzzer.providers - -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.isSubtypeOf -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter -import org.utbot.fuzzer.IdGenerator -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues -import org.utbot.fuzzer.objects.create - -/** - * 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 CollectionWithEmptyStatesModelProvider( - private val idGenerator: IdGenerator -) : ModelProvider { - - private val generators = listOf( - Info(List::class.id, "emptyList"), - Info(Set::class.id, "emptySet"), - Info(Map::class.id, "emptyMap"), - Info(Collection::class.id, "emptyList", returnType = List::class.id), - Info(Iterable::class.id, "emptyList", returnType = List::class.id), - Info(Iterator::class.id, "emptyIterator"), - ) - - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - description.parametersMap - .asSequence() - .forEach { (classId, indices) -> - generators.find { classId == it.classId }?.let { generator -> - yieldAllValues(indices, listOf(generator.classId.create { - id = { idGenerator.createId() } - using static method(java.util.Collections::class.id, generator.methodName, returns = generator.returnType) with values() - }.fuzzed { summary = "%var% = empty collection" })) - } - } - } - - private class Info(val classId: ClassId, val methodName: String, val returnType: ClassId = classId) -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionWithModificationModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionWithModificationModelProvider.kt deleted file mode 100644 index 434c3056dd..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionWithModificationModelProvider.kt +++ /dev/null @@ -1,141 +0,0 @@ -package org.utbot.fuzzer.providers - -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.util.booleanClassId -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.objectClassId -import org.utbot.framework.plugin.api.util.voidClassId -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedType -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.IdentityPreservingIdGenerator -import org.utbot.fuzzer.fuzzNumbers -import org.utbot.fuzzer.objects.create - -class CollectionWithModificationModelProvider( - idGenerator: IdentityPreservingIdGenerator, - recursionDepthLeft: Int = 2, - private var defaultModificationCount: IntArray = intArrayOf(0, 1, 3) -) : RecursiveModelProvider(idGenerator, recursionDepthLeft) { - - init { - totalLimit = 100_000 - } - - // List of available implementations with modification method to insert values - // Should be listed from more specific interface to more general, - // because suitable info is searched by the list. - private val modifications = listOf( - // SETS - Info(java.util.NavigableSet::class.id, java.util.TreeSet::class.id, "add", listOf(objectClassId), booleanClassId) { - it.size == 1 && it[0].classId.isSubtypeOfWithReflection(java.lang.Comparable::class.id) - }, - Info(java.util.SortedSet::class.id, java.util.TreeSet::class.id, "add", listOf(objectClassId), booleanClassId) { - it.size == 1 && it[0].classId.isSubtypeOfWithReflection(java.lang.Comparable::class.id) - }, - Info(java.util.Set::class.id, java.util.HashSet::class.id, "add", listOf(objectClassId), booleanClassId), - // QUEUES - Info(java.util.Queue::class.id, java.util.ArrayDeque::class.id, "add", listOf(objectClassId), booleanClassId), - Info(java.util.Deque::class.id, java.util.ArrayDeque::class.id, "add", listOf(objectClassId), booleanClassId), - Info(java.util.Stack::class.id, java.util.Stack::class.id, "push", listOf(objectClassId), booleanClassId), - // LISTS - Info(java.util.List::class.id, java.util.ArrayList::class.id, "add", listOf(objectClassId), booleanClassId), - // MAPS - Info(java.util.NavigableMap::class.id, java.util.TreeMap::class.id, "put", listOf(objectClassId, objectClassId), objectClassId) { - it.size == 2 && it[0].classId.isSubtypeOfWithReflection(java.lang.Comparable::class.id) - }, - Info(java.util.SortedMap::class.id, java.util.TreeMap::class.id, "put", listOf(objectClassId, objectClassId), objectClassId) { - it.size == 2 && it[0].classId.isSubtypeOfWithReflection(java.lang.Comparable::class.id) - }, - Info(java.util.Map::class.id, java.util.HashMap::class.id, "put", listOf(objectClassId, objectClassId), objectClassId), - // ITERABLE - Info(java.util.Collection::class.id, java.util.ArrayList::class.id, "add", listOf(objectClassId), booleanClassId), - Info(java.lang.Iterable::class.id, java.util.ArrayList::class.id, "add", listOf(objectClassId), booleanClassId), - ) - private var modificationCount = 7 - - override fun newInstance(parentProvider: RecursiveModelProvider, constructor: ModelConstructor): RecursiveModelProvider { - val newInstance = CollectionWithModificationModelProvider( - parentProvider.idGenerator, parentProvider.recursionDepthLeft - 1 - ) - newInstance.copySettings(parentProvider) - if (parentProvider is CollectionWithModificationModelProvider) { - newInstance.defaultModificationCount = parentProvider.defaultModificationCount - } - return newInstance - } - - override fun generateModelConstructors( - description: FuzzedMethodDescription, - parameterIndex: Int, - classId: ClassId, - ): Sequence { - - val info: Info? = if (!classId.isAbstract) { - when { - classId.isSubtypeOfWithReflection(Collection::class.id) -> Info(classId, classId, "add", listOf(objectClassId), booleanClassId) - classId.isSubtypeOfWithReflection(Map::class.id) -> Info(classId, classId, "put", listOf(objectClassId, objectClassId), objectClassId) - else -> null - } - } else { - modifications.find { - classId == it.superClass - } - } - - val sequence = info?.let { - val genericTypes = description.fuzzerType(parameterIndex)?.generics ?: emptyList() - if (genericTypes.isNotEmpty()) { - // this check removes cases when TreeSet or TreeMap is created without comparable key - val lengths = if (info.canModify(genericTypes)) { - fuzzNumbers(description.concreteValues, *defaultModificationCount) { it in 1..modificationCount } - } else { - sequenceOf(0) - } - lengths.map { length -> - ModelConstructor(genericTypes, repeat = length) { values -> - info.assembleModel(info.concreteClass, values) - } - } - } else { - emptySequence() - } - } - return sequence ?: emptySequence() - } - - private fun Info.assembleModel(concreteClassId: ClassId, values: List): FuzzedValue { - return concreteClassId.create { - id = { idGenerator.createId() } - using empty constructor - val paramCount = params.size - values.asSequence() - .windowed(paramCount, paramCount) - .forEach { each -> - call instance method( - methodName, - params, - returnType - ) with values(*Array(paramCount) { each[it].model }) - } - }.fuzzed { - summary = "%var% = test collection" - } - } - - private class Info( - val superClass: ClassId, - val concreteClass: ClassId, - val methodName: String, - val params: List, - val returnType: ClassId = voidClassId, - val canModify: (List) -> Boolean = { true } - ) - - private fun ClassId.isSubtypeOfWithReflection(another: ClassId): Boolean { - // commented code above doesn't work this case: SomeList extends LinkedList {} and Collection -// return isSubtypeOf(another) - return another.jClass.isAssignableFrom(this.jClass) - } -} \ 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 deleted file mode 100644 index 95ee2af8d0..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ConstantsModelProvider.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.utbot.fuzzer.providers - -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.util.isPrimitive -import org.utbot.fuzzer.FuzzedContext -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue - -/** - * Traverses through method constants and creates appropriate models for them. - */ -object ConstantsModelProvider : ModelProvider { - - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - 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 -> - yieldValue(index, m) - } - } - } - } - - fun modifyValue(value: Any, op: FuzzedContext): FuzzedValue? { - if (op !is FuzzedContext.Comparison) return null - val multiplier = if (op == FuzzedContext.Comparison.LT || op == FuzzedContext.Comparison.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 == FuzzedContext.Comparison.EQ || op == FuzzedContext.Comparison.LE || op == FuzzedContext.Comparison.GE) { - op.reverse() - } else op).sign - } $value" } } - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/DateConstantModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/DateConstantModelProvider.kt deleted file mode 100644 index f5f403b610..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/DateConstantModelProvider.kt +++ /dev/null @@ -1,171 +0,0 @@ -package org.utbot.fuzzer.providers - -import java.text.SimpleDateFormat -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtExecutableCallModel -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.UtStatementModel -import org.utbot.framework.plugin.api.util.dateClassId -import org.utbot.framework.plugin.api.util.executableId -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.intClassId -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.longClassId -import org.utbot.framework.plugin.api.util.stringClassId -import org.utbot.framework.plugin.api.util.voidClassId -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter -import org.utbot.fuzzer.FuzzedType -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.IdentityPreservingIdGenerator -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues -import org.utbot.fuzzer.defaultModelProviders -import org.utbot.fuzzer.fuzz -import org.utbot.fuzzer.hex -import org.utbot.fuzzer.objects.assembleModel - -class DateConstantModelProvider( - private val idGenerator: IdentityPreservingIdGenerator, -) : ModelProvider { - - var totalLimit: Int = 20 - - companion object { - private const val defaultDateFormat = "dd-MM-yyyy HH:mm:ss:ms" - } - - private fun String.isDate(format: String): Boolean { - val formatter = SimpleDateFormat(format).apply { - isLenient = false - } - return runCatching { formatter.parse(trim()) }.isSuccess - } - - private fun String.isDateFormat(): Boolean { - return none { it.isDigit() } && // fixes concrete date values - runCatching { SimpleDateFormat(this) }.isSuccess - } - - private fun generateFromNumbers( - baseMethodDescription: FuzzedMethodDescription, - ): Sequence { - val constructorsFromNumbers = dateClassId.allConstructors - .filter { constructor -> - constructor.parameters.isNotEmpty() && - constructor.parameters.all { it == intClassId || it == longClassId } - }.map { constructorId -> - with(constructorId) { - ModelConstructor(parameters.map(::FuzzedType)) { assembleModel(idGenerator.createId(), constructorId, it) } - } - }.sortedBy { it.neededTypes.size } - - return sequence { - constructorsFromNumbers.forEach { constructor -> - yieldAll( - fuzzValues( - constructor.neededTypes.map(FuzzedType::classId), - baseMethodDescription, - defaultModelProviders(idGenerator) - ).map(constructor.createModel) - ) - } - } - } - - private fun generateFromDates( - baseMethodDescription: FuzzedMethodDescription, - ): Sequence { - val strings = baseMethodDescription.concreteValues - .asSequence() - .filter { it.classId == stringClassId } - .map { it.value as String } - .distinct() - val formats = strings.filter { it.isDateFormat() } + defaultDateFormat - val formatToDates = formats.associateWith { format -> strings.filter { it.isDate(format) } } - - return sequence { - formatToDates.forEach { (format, dates) -> - dates.forEach { date -> - yield(assembleDateFromString(idGenerator.createId(), format, date)) - } - } - } - } - - private fun generateNowDate(): Sequence { - val constructor = dateClassId.allConstructors.first { it.parameters.isEmpty() } - return sequenceOf(assembleModel(idGenerator.createId(), constructor, emptyList())) - } - - override fun generate(description: FuzzedMethodDescription): Sequence { - val parameters = description.parametersMap[dateClassId] - if (parameters.isNullOrEmpty()) { - return emptySequence() - } - - return sequence { - yieldAllValues( - parameters, - generateNowDate() + generateFromDates(description) + - generateFromNumbers(description).take(totalLimit) - ) - }.take(totalLimit) - } - - private fun fuzzValues( - types: List, - baseMethodDescription: FuzzedMethodDescription, - modelProvider: ModelProvider, - ): Sequence> { - if (types.isEmpty()) - return sequenceOf(listOf()) - val syntheticMethodDescription = FuzzedMethodDescription( - "", // TODO: maybe add more info here - voidClassId, - types, - baseMethodDescription.concreteValues - ).apply { - packageName = baseMethodDescription.packageName - } - return fuzz(syntheticMethodDescription, modelProvider) - } - - private fun assembleDateFromString(id: Int, formatString: String, dateString: String): FuzzedValue { - val simpleDateFormatModel = assembleSimpleDateFormat(idGenerator.createId(), formatString) - val dateFormatParse = simpleDateFormatModel.classId.jClass - .getMethod("parse", String::class.java).executableId - val instantiationCall = UtExecutableCallModel( - simpleDateFormatModel, dateFormatParse, listOf(UtPrimitiveModel(dateString)) - ) - return UtAssembleModel( - id, - dateClassId, - "$dateFormatParse#" + id.hex(), - instantiationCall - ).fuzzed { - summary = "%var% = $dateFormatParse($stringClassId)" - } - } - - private fun assembleSimpleDateFormat(id: Int, formatString: String): UtAssembleModel { - val simpleDateFormatId = SimpleDateFormat::class.java.id - val formatStringConstructor = simpleDateFormatId.allConstructors.first { - it.parameters.singleOrNull() == stringClassId - } - val formatSetLenient = SimpleDateFormat::setLenient.executableId - val formatModel = UtPrimitiveModel(formatString) - - val instantiationCall = UtExecutableCallModel(instance = null, formatStringConstructor, listOf(formatModel)) - return UtAssembleModel( - id, - simpleDateFormatId, - "$simpleDateFormatId[$stringClassId]#" + id.hex(), - instantiationCall - ) { - listOf(UtExecutableCallModel(instance = this, formatSetLenient, listOf(UtPrimitiveModel(false)))) - } - } - -} 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 deleted file mode 100644 index 57aa0dcf13..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/EnumModelProvider.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.utbot.fuzzer.providers - -import org.utbot.framework.plugin.api.UtEnumConstantModel -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.fuzzer.IdentityPreservingIdGenerator -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues - -class EnumModelProvider(private val idGenerator: IdentityPreservingIdGenerator) : ModelProvider { - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - description.parametersMap - .asSequence() - .filter { (classId, _) -> classId.jClass.isEnum } - .forEach { (classId, indices) -> - yieldAllValues(indices, classId.jClass.enumConstants.filterIsInstance>().map { - val id = idGenerator.getOrCreateIdForValue(it) - UtEnumConstantModel(id, classId, it).fuzzed { summary = "%var% = ${it.name}" } - }) - } - } -} 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 deleted file mode 100644 index 5e23a0f2c8..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/NullModelProvider.kt +++ /dev/null @@ -1,25 +0,0 @@ -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.FuzzedParameter -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue - -/** - * 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): Sequence = sequence { - description.parametersMap - .asSequence() - .filter { (classId, _) -> classId.isRefType } - .forEach { (classId, indices) -> - val model = UtNullModel(classId) - indices.forEach { - yieldValue(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/NumberClassModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/NumberClassModelProvider.kt deleted file mode 100644 index d0a9938abb..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/NumberClassModelProvider.kt +++ /dev/null @@ -1,74 +0,0 @@ -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.byteClassId -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.isPrimitive -import org.utbot.framework.plugin.api.util.longClassId -import org.utbot.framework.plugin.api.util.primitiveByWrapper -import org.utbot.framework.plugin.api.util.shortClassId -import org.utbot.framework.plugin.api.util.wrapperByPrimitive -import org.utbot.fuzzer.FuzzedConcreteValue -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter -import org.utbot.fuzzer.IdentityPreservingIdGenerator -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue -import org.utbot.fuzzer.objects.create -import kotlin.random.Random - -/** - * Provides random implementation if current requested type is [Number]. - */ -class NumberClassModelProvider( - val idGenerator: IdentityPreservingIdGenerator, - val random: Random, -) : ModelProvider { - // byteClassId generates bad code because of type cast on method Byte.valueOf - private val types = setOf(/*byteClassId,*/ shortClassId, intClassId, longClassId, floatClassId, doubleClassId) - - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - description.parametersMap[Number::class.id]?.forEach { index -> - val fuzzedValues = description.concreteValues.filter { types.contains(it.classId) } + - (-5 until 5).map { FuzzedConcreteValue(types.random(random), it) } - fuzzedValues.forEach { fuzzedValue -> - val targetType = fuzzedValue.classId - check(targetType.isPrimitive) { "$targetType is not primitive value" } - val castedValue = castNumberIfPossible(fuzzedValue.value as Number, targetType) - val targetValues = listOfNotNull( - castedValue.let(::UtPrimitiveModel), - ConstantsModelProvider.modifyValue(castedValue, fuzzedValue.fuzzedContext)?.model - ) - // we use wrapper type to generate simple values, - // because at the moment code generator uses reflection - // if primitive types are provided - val wrapperType = wrapperByPrimitive[targetType] ?: return@sequence - targetValues.forEach { targetValue -> - yieldValue(index, wrapperType.create { - id = { idGenerator.createId() } - using static method( - classId = wrapperType, - name = "valueOf", - params = listOf(primitiveByWrapper[wrapperType]!!), - returns = wrapperType - ) with values(targetValue) - }.fuzzed { summary = "%var% = ${Number::class.simpleName}(${targetValue})" }) - } - } - } - } - - private fun castNumberIfPossible(number: Number, classId: ClassId): Number = when (classId) { - byteClassId -> number.toInt().toByte() - shortClassId -> number.toInt().toShort() - intClassId -> number.toInt() - longClassId -> number.toLong() - floatClassId -> number.toFloat() - doubleClassId -> number.toDouble() - else -> number - } -} \ 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 deleted file mode 100644 index 8c612e3866..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt +++ /dev/null @@ -1,189 +0,0 @@ -package org.utbot.fuzzer.providers - -import java.lang.reflect.Constructor -import java.lang.reflect.Field -import java.lang.reflect.Member -import java.lang.reflect.Method -import java.lang.reflect.Modifier.isFinal -import java.lang.reflect.Modifier.isPrivate -import java.lang.reflect.Modifier.isProtected -import java.lang.reflect.Modifier.isPublic -import java.lang.reflect.Modifier.isStatic -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.ConstructorId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.MethodId -import org.utbot.framework.plugin.api.UtAssembleModel -import org.utbot.framework.plugin.api.UtDirectSetFieldModel -import org.utbot.framework.plugin.api.UtExecutableCallModel -import org.utbot.framework.plugin.api.util.dateClassId -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.isEnum -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.FuzzedType -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.IdentityPreservingIdGenerator -import org.utbot.fuzzer.objects.assembleModel - -/** - * Creates [UtAssembleModel] for objects which have public constructors - */ -class ObjectModelProvider( - idGenerator: IdentityPreservingIdGenerator, - recursionDepthLeft: Int = 2, -) : RecursiveModelProvider(idGenerator, recursionDepthLeft) { - override fun newInstance(parentProvider: RecursiveModelProvider, constructor: ModelConstructor): RecursiveModelProvider { - val newInstance = ObjectModelProvider(parentProvider.idGenerator, parentProvider.recursionDepthLeft - 1) - newInstance.copySettings(parentProvider) - newInstance.branchingLimit = 1 - return newInstance - } - - override fun generateModelConstructors( - description: FuzzedMethodDescription, - parameterIndex: Int, - classId: ClassId, - ): Sequence = sequence { - if (unwantedConstructorsClasses.contains(classId) - || classId.isPrimitiveWrapper - || classId.isEnum - || classId.isAbstract - || (classId.isInner && !classId.isStatic) - ) return@sequence - - val constructors = collectConstructors(classId) { javaConstructor -> - isAccessible(javaConstructor, description.packageName) - }.sortedWith( - primitiveParameterizedConstructorsFirstAndThenByParameterCount - ) - - constructors.forEach { constructorId -> - // When branching limit = 1 this block tries to create new values - // and mutate some fields. Only if there's no option next block - // with empty constructor should be used. - if (constructorId.parameters.isEmpty()) { - val fields = findSuitableFields(constructorId.classId, description) - if (fields.isNotEmpty()) { - yield( - ModelConstructor(fields.map { FuzzedType(it.classId) }) { - generateModelsWithFieldsInitialization(constructorId, fields, it) - } - ) - } - } - yield(ModelConstructor(constructorId.parameters.map { classId -> FuzzedType(classId) }) { - assembleModel(idGenerator.createId(), constructorId, it) - }) - } - } - - private fun generateModelsWithFieldsInitialization( - constructorId: ConstructorId, - fields: List, - fieldValues: List - ): FuzzedValue { - val fuzzedModel = assembleModel(idGenerator.createId(), constructorId, emptyList()) - val assembleModel = fuzzedModel.model as? UtAssembleModel - ?: error("Expected UtAssembleModel but ${fuzzedModel.model::class.java} found") - val modificationChain = - assembleModel.modificationsChain as? MutableList ?: error("Modification chain must be mutable") - fieldValues.asSequence().mapIndexedNotNull { index, value -> - val field = fields[index] - when { - field.canBeSetDirectly -> UtDirectSetFieldModel( - fuzzedModel.model, - FieldId(constructorId.classId, field.name), - value.model - ) - field.setter != null -> UtExecutableCallModel( - fuzzedModel.model, - MethodId( - constructorId.classId, - field.setter.name, - field.setter.returnType.id, - listOf(field.classId) - ), - listOf(value.model) - ) - else -> null - } - }.forEach(modificationChain::add) - return fuzzedModel - } - - companion object { - - private val unwantedConstructorsClasses = listOf( - stringClassId, dateClassId - ) - - 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 isAccessible(member: Member, packageName: String?): Boolean { - return isPublic(member.modifiers) || - (packageName != null && isPackagePrivate(member.modifiers) && member.declaringClass.`package`?.name == packageName) - } - - private fun isPackagePrivate(modifiers: Int): Boolean { - val hasAnyAccessModifier = isPrivate(modifiers) - || isProtected(modifiers) - || isProtected(modifiers) - return !hasAnyAccessModifier - } - - private fun findSuitableFields(classId: ClassId, description: FuzzedMethodDescription): List { - val jClass = classId.jClass - return jClass.declaredFields.map { field -> - FieldDescription( - field.name, - field.type.id, - isAccessible(field, description.packageName) && !isFinal(field.modifiers) && !isStatic(field.modifiers), - jClass.findPublicSetterIfHasPublicGetter(field, description) - ) - } - } - - private fun Class<*>.findPublicSetterIfHasPublicGetter(field: Field, description: FuzzedMethodDescription): Method? { - val postfixName = field.name.capitalize() - val setterName = "set$postfixName" - val getterName = "get$postfixName" - val getter = try { getDeclaredMethod(getterName) } catch (_: NoSuchMethodException) { return null } - return if (isAccessible(getter, description.packageName) && getter.returnType == field.type) { - declaredMethods.find { - isAccessible(it, description.packageName) && - it.name == setterName && - it.parameterCount == 1 && - it.parameterTypes[0] == field.type - } - } else { - null - } - } - - private val primitiveParameterizedConstructorsFirstAndThenByParameterCount = - compareByDescending { constructorId -> - constructorId.parameters.all { classId -> - classId.isPrimitive || classId == stringClassId - } - }.thenComparingInt { constructorId -> - constructorId.parameters.size - } - - private class FieldDescription( - val name: String, - val classId: ClassId, - val canBeSetDirectly: Boolean, - val setter: Method?, - ) - } -} \ 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 deleted file mode 100644 index 621e3795c6..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveDefaultsModelProvider.kt +++ /dev/null @@ -1,46 +0,0 @@ -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.FuzzedParameter -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue - -/** - * Provides default values for primitive types. - */ -object PrimitiveDefaultsModelProvider : ModelProvider { - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - description.parametersMap.forEach { (classId, parameterIndices) -> - valueOf(classId)?.let { model -> - parameterIndices.forEach { index -> - yieldValue(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/PrimitiveRandomModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveRandomModelProvider.kt deleted file mode 100644 index f866742cab..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveRandomModelProvider.kt +++ /dev/null @@ -1,66 +0,0 @@ -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.primitiveByWrapper -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.FuzzedParameter -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue -import kotlin.random.Random - -/** - * Provides default values for primitive types. - */ -class PrimitiveRandomModelProvider(val random: Random, val size: Int = 5) : ModelProvider { - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - description.parametersMap.forEach { (classId, parameterIndices) -> - for (i in 1..size) { - valueOf(primitiveByWrapper[classId] ?: classId)?.let { model -> - parameterIndices.forEach { index -> - yieldValue(index, model) - } - } - } - } - } - - fun valueOf(classId: ClassId): FuzzedValue? = when (classId) { - booleanClassId -> random.nextBoolean().let { v -> UtPrimitiveModel(v).fuzzed { summary = "%var% = $v" } } - byteClassId -> random.nextInt(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).let { v -> - UtPrimitiveModel(v.toByte()).fuzzed { summary = "%var% = random byte" } - } - charClassId -> random.nextInt(1, 256).let { v -> - UtPrimitiveModel(v.toChar()).fuzzed { summary = "%var% = random char" } - } - shortClassId -> random.nextInt(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).let { v -> - UtPrimitiveModel(v.toShort()).fuzzed { summary = "%var% = random short" } - } - intClassId -> random.nextInt().let { v -> - UtPrimitiveModel(v).fuzzed { summary = "%var% = random integer" } - } - longClassId -> random.nextLong().let { v -> - UtPrimitiveModel(v).fuzzed { summary = "%var% = random long" } - } - floatClassId -> random.nextFloat().let { v -> - UtPrimitiveModel(v).fuzzed { summary = "%var% = random float" } - } - doubleClassId -> random.nextDouble().let { v -> - UtPrimitiveModel(0.0).fuzzed { summary = "%var% = random double" } - } - stringClassId -> (1..5).map { random.nextInt('a'.code, 'z'.code).toChar() }.joinToString("").let { s -> - UtPrimitiveModel(s).fuzzed { summary = "%var% = random string" } - } - 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 deleted file mode 100644 index a032ad609f..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveWrapperModelProvider.kt +++ /dev/null @@ -1,64 +0,0 @@ -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.FuzzedParameter -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues - -object PrimitiveWrapperModelProvider: ModelProvider { - - private val constantModels = ModelProvider.of( - PrimitiveDefaultsModelProvider, - ConstantsModelProvider, - StringConstantModelProvider - ) - - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - 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@sequence - } - - val constants = mutableMapOf>() - constantModels.generate(FuzzedMethodDescription( - name = "Primitive wrapper constant generation ", - returnType = voidClassId, - parameters = primitiveWrapperTypesAsPrimitiveTypes, - concreteValues = description.concreteValues - )).forEach { (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 -> - yieldAllValues(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 deleted file mode 100644 index 5c9ffe4090..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitivesModelProvider.kt +++ /dev/null @@ -1,90 +0,0 @@ -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.FuzzedParameter -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue - -/** - * Produces bound values for primitive types. - */ -object PrimitivesModelProvider : ModelProvider { - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - 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 -> - yieldValue(index, model) - } - } - } - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RecursiveModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RecursiveModelProvider.kt deleted file mode 100644 index 855886fadb..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RecursiveModelProvider.kt +++ /dev/null @@ -1,121 +0,0 @@ -package org.utbot.fuzzer.providers - -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.util.voidClassId -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter -import org.utbot.fuzzer.FuzzedType -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.IdentityPreservingIdGenerator -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues -import org.utbot.fuzzer.exceptIsInstance -import org.utbot.fuzzer.fuzz -import org.utbot.fuzzer.modelProviderForRecursiveCalls - -/** - * Auxiliary data class that stores information describing how to construct model from parts (submodels) - * - * @param neededTypes list containing type ([ClassId]) of each submodel - * @param repeat if value greater than 1, [neededTypes] is duplicated, therefore [createModel] should accept `neededTypes.size * repeat` values - * @param createModel lambda that takes subModels (they should have types listed in [neededTypes]) and generates a model using them - */ -data class ModelConstructor( - val neededTypes: List, - val repeat: Int = 1, - val createModel: (subModels: List) -> FuzzedValue, -) { - var limit: Int = Int.MAX_VALUE -} - -/** - * Abstraction for providers that may call other providers recursively inside them. [generate] will firstly get possible - * model constructors (provided by [generateModelConstructors]) and then fuzz parameters for each of them using synthetic method - * - * @param recursionDepthLeft maximum recursion level, i.e. maximum number of nested calls produced by this provider - * - * @property modelProviderForRecursiveCalls providers that can be called by this provider. - * Note that if [modelProviderForRecursiveCalls] has instances of [RecursiveModelProvider] then this provider will use - * their copies created by [newInstance] rather than themselves (this is important because we have to change some - * properties like [recursionDepthLeft], [totalLimit], etc.) - * @property fallbackProvider provider that will be used instead [modelProviderForRecursiveCalls] after reaching maximum recursion level - * @property totalLimit maximum number of values produced by this provider - * @property branchingLimit maximum number of [ModelConstructor]s used by [generate] (see [generateModelConstructors]) - */ -abstract class RecursiveModelProvider( - val idGenerator: IdentityPreservingIdGenerator, - val recursionDepthLeft: Int -) : ModelProvider { - var modelProviderForRecursiveCalls: ModelProvider = modelProviderForRecursiveCalls(idGenerator, recursionDepthLeft - 1) - var fallbackProvider: ModelProvider = NullModelProvider - var totalLimit: Int = 1000 - var branchingLimit: Int = Int.MAX_VALUE - - /** - * Creates instance of the class on which it is called, assuming that it will be called recursively from [parentProvider] - */ - protected abstract fun newInstance(parentProvider: RecursiveModelProvider, constructor: ModelConstructor): RecursiveModelProvider - - /** - * Creates [ModelProvider]s that will be used to generate values recursively. The order of elements in returned list is important: - * only first [branchingLimit] constructors will be used, so you should place most effective providers first - */ - protected abstract fun generateModelConstructors( - description: FuzzedMethodDescription, - parameterIndex: Int, - classId: ClassId, - ): Sequence - - protected open fun copySettings(other: RecursiveModelProvider): RecursiveModelProvider { - modelProviderForRecursiveCalls = other.modelProviderForRecursiveCalls - fallbackProvider = other.fallbackProvider - totalLimit = other.totalLimit - branchingLimit = other.branchingLimit - return this - } - - final override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - description.parameters.forEachIndexed { index, classId -> - generateModelConstructors(description, index, classId) - .take(branchingLimit) - .forEach { creator -> - yieldAllValues(listOf(index), creator.recursiveCall(description)) - } - } - }.take(totalLimit) - - private fun ModelConstructor.recursiveCall(baseMethodDescription: FuzzedMethodDescription): Sequence { - // when no parameters are needed just call model creator once, - // for example, if collection is empty or object has empty constructor - if (neededTypes.isEmpty() || repeat == 0) { - return sequenceOf(createModel(listOf())) - } - val syntheticMethodDescription = FuzzedMethodDescription( - "", - voidClassId, - (1..repeat).flatMap { neededTypes.map { it.classId } }, - baseMethodDescription.concreteValues - ).apply { - packageName = baseMethodDescription.packageName - fuzzerType = { index -> - neededTypes[index % neededTypes.size] // because we can repeat neededTypes several times - } - } - return fuzz(syntheticMethodDescription, nextModelProvider(this)) - .map { createModel(it) } - .take(limit) - } - - private fun nextModelProvider(constructor: ModelConstructor): ModelProvider = - if (recursionDepthLeft > 0) { - modelProviderForRecursiveCalls.map { - if (it is RecursiveModelProvider) { - it.newInstance(this, constructor) - } else { it } - } - } else { - modelProviderForRecursiveCalls - .exceptIsInstance() - .withFallback(fallbackProvider) - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RegexModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RegexModelProvider.kt deleted file mode 100644 index b2b56440b4..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/RegexModelProvider.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.utbot.fuzzer.providers - -import com.github.curiousoddman.rgxgen.RgxGen -import com.github.curiousoddman.rgxgen.config.RgxGenOption -import com.github.curiousoddman.rgxgen.config.RgxGenProperties -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.stringClassId -import org.utbot.fuzzer.FuzzedContext -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues -import java.util.regex.Pattern -import java.util.regex.PatternSyntaxException -import kotlin.random.Random -import kotlin.random.asJavaRandom - -object RegexModelProvider : ModelProvider { - - val rgxGenProperties = RgxGenProperties().apply { - setProperty(RgxGenOption.INFINITE_PATTERN_REPETITION.key, "5") - } - - override fun generate(description: FuzzedMethodDescription): Sequence { - val parameters = description.parametersMap[stringClassId] - if (parameters.isNullOrEmpty()) { - return emptySequence() - } - val regexes = description.concreteValues - .asSequence() - .filter { it.classId == stringClassId } - .filter { it.fuzzedContext.isPatterMatchingContext() } - .map { it.value as String } - .distinct() - .filter { it.isNotBlank() } - .filter { - try { - Pattern.compile(it); true - } catch (_: PatternSyntaxException) { - false - } - }.map { - it to RgxGen(it).apply { - setProperties(rgxGenProperties) - }.generate(Random(0).asJavaRandom()) - } - - return sequence { - yieldAllValues(parameters, regexes.map { - RegexFuzzedValue(UtPrimitiveModel(it.second).fuzzed { summary = "%var% = regex ${it.first}" }, it.first) - }) - } - } - - private fun FuzzedContext.isPatterMatchingContext(): Boolean { - if (this !is FuzzedContext.Call) return false - val stringMethodWithRegexArguments = setOf("matches", "split") - return when { - method.classId == Pattern::class.java.id -> true - method.classId == String::class.java.id && stringMethodWithRegexArguments.contains(method.name) -> true - else -> false - } - } -} - -class RegexFuzzedValue(value: FuzzedValue, val regex: String) : FuzzedValue(value.model, value.createdBy) \ 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 deleted file mode 100644 index 1f9a47238d..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/StringConstantModelProvider.kt +++ /dev/null @@ -1,33 +0,0 @@ -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.FuzzedParameter -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.ModelProvider.Companion.yieldAllValues -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue - -object StringConstantModelProvider : ModelProvider { - - override fun generate(description: FuzzedMethodDescription): Sequence = sequence { - description.concreteValues - .asSequence() - .filter { (classId, _) -> classId == stringClassId } - .forEach { (_, value, _) -> - description.parametersMap.getOrElse(stringClassId) { emptyList() }.forEach { index -> - yieldValue(index, UtPrimitiveModel(value).fuzzed { summary = "%var% = string" }) - } - } - val charsAsStrings = description.concreteValues - .asSequence() - .filter { (classId, _) -> classId == charClassId } - .map { (_, value, _) -> - UtPrimitiveModel((value as Char).toString()).fuzzed { - summary = "%var% = $value" - } - } - yieldAllValues(description.parametersMap.getOrElse(stringClassId) { emptyList() }, charsAsStrings) - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/SampleEnum.java b/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/SampleEnum.java deleted file mode 100644 index 1571254a70..0000000000 --- a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/SampleEnum.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.utbot.framework.plugin.api.samples; - -public enum SampleEnum { - LEFT, - RIGHT -} diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CollectionModelProviderTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CollectionModelProviderTest.kt deleted file mode 100644 index 9f29d50ef0..0000000000 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CollectionModelProviderTest.kt +++ /dev/null @@ -1,271 +0,0 @@ -package org.utbot.framework.plugin.api - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.framework.plugin.api.util.booleanClassId -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.intClassId -import org.utbot.framework.plugin.api.util.intWrapperClassId -import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.objectClassId -import org.utbot.framework.plugin.api.util.primitiveByWrapper -import org.utbot.framework.plugin.api.util.primitiveWrappers -import org.utbot.framework.plugin.api.util.stringClassId -import org.utbot.framework.plugin.api.util.withUtContext -import org.utbot.fuzzer.FuzzedType -import org.utbot.fuzzer.exceptIsInstance -import org.utbot.fuzzer.providers.CollectionWithEmptyStatesModelProvider -import org.utbot.fuzzer.providers.CollectionWithModificationModelProvider -import java.util.* - -class CollectionModelProviderTest { - - interface MyInterface - - @Test - fun `empty collection is created for unknown interface without modifications`() { - withUtContext(UtContext(this::class.java.classLoader)) { - val result = collect( - CollectionWithModificationModelProvider(TestIdentityPreservingIdGenerator), - parameters = listOf(Collection::class.id), - ) { - fuzzerType = { FuzzedType(Collection::class.id, listOf(FuzzedType(MyInterface::class.id))) } - } - assertEquals(1, result.size) - val models = result[0]!! - assertEquals(1, models.size) { "test should generate only 1 empty model" } - val model = models[0] - assertTrue(model is UtAssembleModel) { "unexpected model type" } - assertEquals(0, (model as UtAssembleModel).modificationsChain.size) { "Model should not have any modifications" } - } - } - - @Test - fun `collection is created with modification of concrete class`() { - val modifications = intArrayOf(0, 1, 3, 5) - withUtContext(UtContext(this::class.java.classLoader)) { - val result = collect( - CollectionWithModificationModelProvider( - TestIdentityPreservingIdGenerator, - defaultModificationCount = modifications - ), - parameters = listOf(Collection::class.id), - ) { - fuzzerType = { FuzzedType(Collection::class.id, listOf(FuzzedType(objectClassId))) } - } - assertEquals(1, result.size) - val models = result[0]!! - assertEquals(modifications.size, models.size) { "test should generate only 3 model: empty, with 1 modification and 3 modification" } - modifications.forEachIndexed { index, expectedModifications -> - val model = models[index] - assertTrue(model is UtAssembleModel) { "unexpected model type" } - assertEquals(expectedModifications, (model as UtAssembleModel).modificationsChain.size) { "Model has unexpected number of modifications" } - } - } - } - - @Test - fun `collection can create simple values with concrete type`() { - val modifications = intArrayOf(1) - withUtContext(UtContext(this::class.java.classLoader)) { - val result = collect( - CollectionWithModificationModelProvider( - TestIdentityPreservingIdGenerator, - defaultModificationCount = modifications - ).apply { - totalLimit = 1 - }, - parameters = listOf(Collection::class.id), - ) { - fuzzerType = { FuzzedType(Collection::class.id, listOf(FuzzedType(intWrapperClassId))) } - } - assertEquals(1, result.size) - val models = result[0]!! - assertEquals(modifications.size, models.size) - modifications.forEachIndexed { index, expectedModifications -> - val model = models[index] - assertTrue(model is UtAssembleModel) - val modificationsChain = (model as UtAssembleModel).modificationsChain - assertEquals(expectedModifications, modificationsChain.size) - val statementModel = modificationsChain[0] - testStatementIsAsSimpleAddIntToCollection(ArrayList::class.id, statementModel) - } - } - } - - @Test - fun `collection can create recursively values with concrete type`() { - val modifications = intArrayOf(1) - withUtContext(UtContext(this::class.java.classLoader)) { - val result = collect( - CollectionWithModificationModelProvider( - TestIdentityPreservingIdGenerator, - defaultModificationCount = modifications - ).apply { - // removes empty collections from the result - modelProviderForRecursiveCalls = modelProviderForRecursiveCalls - .exceptIsInstance() - totalLimit = 1 - }, - parameters = listOf(Collection::class.id), - ) { - fuzzerType = { - FuzzedType(Collection::class.id, listOf( - FuzzedType(Set::class.id, listOf( - FuzzedType(intWrapperClassId) - )) - )) - } - } - assertEquals(1, result.size) - val models = result[0]!! - assertEquals(modifications.size, models.size) - modifications.forEachIndexed { index, expectedModifications -> - val model = models[index] - assertTrue(model is UtAssembleModel) - val modificationsChain = (model as UtAssembleModel).modificationsChain - assertEquals(expectedModifications, modificationsChain.size) - var statementModel = modificationsChain[0] - assertTrue(statementModel is UtExecutableCallModel) - statementModel as UtExecutableCallModel - assertEquals( - MethodId(ArrayList::class.id, "add", booleanClassId, listOf(objectClassId)), - statementModel.executable - ) - assertEquals(1, statementModel.params.size) - val innerType = statementModel.params[0] - assertTrue(innerType is UtAssembleModel) - innerType as UtAssembleModel - assertEquals(HashSet::class.id, innerType.classId) - assertEquals(1, innerType.modificationsChain.size) - statementModel = innerType.modificationsChain[0] - testStatementIsAsSimpleAddIntToCollection(HashSet::class.id, statementModel) - } - } - } - - private fun testStatementIsAsSimpleAddIntToCollection(collectionId: ClassId, statementModel: UtStatementModel) { - testStatementIsAsSimpleAddGenericSimpleTypeToCollection(collectionId, intWrapperClassId, statementModel) - } - - private fun testStatementIsAsSimpleAddGenericSimpleTypeToCollection(collectionId: ClassId, genericId: ClassId, statementModel: UtStatementModel) { - assertTrue(primitiveWrappers.contains(genericId)) { "This test works only with primitive wrapper types" } - assertTrue(statementModel is UtExecutableCallModel) - statementModel as UtExecutableCallModel - assertEquals( - MethodId(collectionId, "add", booleanClassId, listOf(objectClassId)), - statementModel.executable - ) - assertEquals(1, statementModel.params.size) - val classModel = statementModel.params[0] - assertTrue(classModel is UtPrimitiveModel) - classModel as UtPrimitiveModel - assertEquals(primitiveByWrapper[genericId], classModel.classId) - assertTrue(genericId.jClass.isAssignableFrom(classModel.value::class.java)) - } - - @Test - fun `map can create simple values with concrete type`() { - val modifications = intArrayOf(1) - withUtContext(UtContext(this::class.java.classLoader)) { - val result = collect( - CollectionWithModificationModelProvider( - TestIdentityPreservingIdGenerator, - defaultModificationCount = modifications - ).apply { - totalLimit = 1 - }, - parameters = listOf(Map::class.id), - ) { - fuzzerType = { FuzzedType(Map::class.id, listOf(FuzzedType(intWrapperClassId), FuzzedType(stringClassId))) } - } - assertEquals(1, result.size) - val models = result[0]!! - assertEquals(modifications.size, models.size) - modifications.forEachIndexed { index, expectedModifications -> - val model = models[index] - assertTrue(model is UtAssembleModel) - val modificationsChain = (model as UtAssembleModel).modificationsChain - assertEquals(expectedModifications, modificationsChain.size) - val statementModel = modificationsChain[0] - testStatementIsAsSimpleAddIntStringToMap(HashMap::class.id, statementModel) - } - } - } - - private fun testStatementIsAsSimpleAddIntStringToMap(collectionId: ClassId, statementModel: UtStatementModel) { - assertTrue(statementModel is UtExecutableCallModel) - statementModel as UtExecutableCallModel - assertEquals( - MethodId(collectionId, "put", objectClassId, listOf(objectClassId, objectClassId)), - statementModel.executable - ) - assertEquals(2, statementModel.params.size) - val intClassModel = statementModel.params[0] - assertTrue(intClassModel is UtPrimitiveModel) - intClassModel as UtPrimitiveModel - assertEquals(intClassId, intClassModel.classId) - assertTrue(intClassModel.value is Int) - val stringClassModel = statementModel.params[1] - assertTrue(stringClassModel is UtPrimitiveModel) - stringClassModel as UtPrimitiveModel - assertEquals(stringClassId, stringClassModel.classId) - assertTrue(stringClassModel.value is String) - } - - ///region REGRESSION TESTS - @Test - fun lists() { - testExpectedCollectionIsCreatedWithCorrectGenericType(Collection::class.id, ArrayList::class.id, intWrapperClassId) - testExpectedCollectionIsCreatedWithCorrectGenericType(List::class.id, ArrayList::class.id, intWrapperClassId) - testExpectedCollectionIsCreatedWithCorrectGenericType(Stack::class.id, Stack::class.id, intWrapperClassId) - testExpectedCollectionIsCreatedWithCorrectGenericType(java.util.Deque::class.id, java.util.ArrayDeque::class.id, intWrapperClassId) - testExpectedCollectionIsCreatedWithCorrectGenericType(Queue::class.id, java.util.ArrayDeque::class.id, intWrapperClassId) - } - - @Test - fun sets() { - testExpectedCollectionIsCreatedWithCorrectGenericType(Set::class.id, HashSet::class.id, intWrapperClassId) - testExpectedCollectionIsCreatedWithCorrectGenericType(SortedSet::class.id, TreeSet::class.id, intWrapperClassId) - testExpectedCollectionIsCreatedWithCorrectGenericType(NavigableSet::class.id, TreeSet::class.id, intWrapperClassId) - } - - class ConcreteClass : LinkedList() - - @Test - fun `concrete class is created`() { - testExpectedCollectionIsCreatedWithCorrectGenericType(ConcreteClass::class.id, ConcreteClass::class.id, intWrapperClassId) - } - - private fun testExpectedCollectionIsCreatedWithCorrectGenericType(collectionId: ClassId, expectedId: ClassId, genericId: ClassId) { - withUtContext(UtContext(this::class.java.classLoader)) { - val modifications = intArrayOf(1) - val result = collect( - CollectionWithModificationModelProvider( - TestIdentityPreservingIdGenerator, - defaultModificationCount = modifications - ).apply { - totalLimit = 1 - }, - parameters = listOf(collectionId), - ) { - fuzzerType = { FuzzedType(collectionId, listOf(FuzzedType(genericId))) } - } - assertEquals(1, result.size) - val models = result[0]!! - assertEquals(modifications.size, models.size) - modifications.forEachIndexed { index, expectedModifications -> - val model = models[index] - assertTrue(model is UtAssembleModel) - val modificationsChain = (model as UtAssembleModel).modificationsChain - assertEquals(expectedModifications, modificationsChain.size) - val statementModel = modificationsChain[0] - testStatementIsAsSimpleAddGenericSimpleTypeToCollection(expectedId, genericId, statementModel) - } - } - } - - ///end region -} \ 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 deleted file mode 100644 index f357b2260c..0000000000 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FuzzedValueDescriptionTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -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.FuzzedContext -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, FuzzedContext.Comparison.EQ), - FuzzedConcreteValue(intClassId, 20, FuzzedContext.Comparison.NE), - FuzzedConcreteValue(intClassId, 30, FuzzedContext.Comparison.LT), - FuzzedConcreteValue(intClassId, 40, FuzzedContext.Comparison.LE), - FuzzedConcreteValue(intClassId, 50, FuzzedContext.Comparison.GT), - FuzzedConcreteValue(intClassId, 60, FuzzedContext.Comparison.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 - ) - ).forEach { (_, 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/FuzzerTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FuzzerTest.kt deleted file mode 100644 index da4e5135c0..0000000000 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FuzzerTest.kt +++ /dev/null @@ -1,160 +0,0 @@ -package org.utbot.framework.plugin.api - -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.assertThrows -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.fuzzer.FuzzedConcreteValue -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.FuzzedParameter -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.fuzz -import org.utbot.fuzzer.providers.ConstantsModelProvider -import org.utbot.fuzzer.providers.NullModelProvider -import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider -import org.utbot.fuzzer.providers.PrimitiveWrapperModelProvider.fuzzed -import java.lang.IllegalArgumentException -import java.util.concurrent.TimeUnit - -class FuzzerTest { - - private val testProvider = ModelProvider.of(PrimitiveDefaultsModelProvider, NullModelProvider) - - @Test - fun `error when no provider is passed`() { - assertThrows { - fuzz(newDescription(emptyList())) - } - } - - @Test - fun `zero values for empty input`() { - val fuzz = fuzz(newDescription(emptyList()), testProvider) - assertNull(fuzz.firstOrNull()) - } - - @Test - fun `single value for every type`() { - val fuzz = fuzz( - newDescription( - defaultTypes() - ), - testProvider - ) - assertEquals(1, fuzz.count()) { "Default provider should create 1 value for every type, but have ${fuzz.count()}" } - assertEquals(listOf( - UtPrimitiveModel(false), - UtPrimitiveModel(0.toByte()), - UtPrimitiveModel('\u0000'), - UtPrimitiveModel(0.toShort()), - UtPrimitiveModel(0), - UtPrimitiveModel(0L), - UtPrimitiveModel(0.0f), - UtPrimitiveModel(0.0), - UtNullModel(Any::class.java.id) - ), fuzz.first().map { it.model }) - } - - @Test - fun `concrete values are created`() { - val concreteValues = listOf( - FuzzedConcreteValue(intClassId, 1), - FuzzedConcreteValue(intClassId, 2), - FuzzedConcreteValue(intClassId, 3), - ) - val fuzz = fuzz(newDescription(listOf(intClassId), concreteValues), ConstantsModelProvider) - assertEquals(concreteValues.size, fuzz.count()) - assertEquals(setOf( - UtPrimitiveModel(1), - UtPrimitiveModel(2), - UtPrimitiveModel(3), - ), fuzz.map { it.first().model }.toSet()) - } - - @Test - fun `concrete values are created but filtered`() { - val concreteValues = listOf( - FuzzedConcreteValue(intClassId, 1), - FuzzedConcreteValue(intClassId, 2), - FuzzedConcreteValue(intClassId, 3), - ) - val fuzz = fuzz(newDescription(listOf(charClassId), concreteValues), ConstantsModelProvider) - assertEquals(0, fuzz.count()) - } - - @Test - fun `all combinations is found`() { - val fuzz = fuzz(newDescription(listOf(booleanClassId, intClassId)), ModelProvider { - sequenceOf( - FuzzedParameter(0, UtPrimitiveModel(true).fuzzed()), - FuzzedParameter(0, UtPrimitiveModel(false).fuzzed()), - FuzzedParameter(1, UtPrimitiveModel(-1).fuzzed()), - FuzzedParameter(1, UtPrimitiveModel(0).fuzzed()), - FuzzedParameter(1, UtPrimitiveModel(1).fuzzed()), - ) - }) - assertEquals(6, fuzz.count()) - assertEquals(setOf( - listOf(UtPrimitiveModel(true), UtPrimitiveModel(-1)), - listOf(UtPrimitiveModel(false), UtPrimitiveModel(-1)), - listOf(UtPrimitiveModel(true), UtPrimitiveModel(0)), - listOf(UtPrimitiveModel(false), UtPrimitiveModel(0)), - listOf(UtPrimitiveModel(true), UtPrimitiveModel(1)), - listOf(UtPrimitiveModel(false), UtPrimitiveModel(1)), - ), fuzz.map { arguments -> arguments.map { fuzzedValue -> fuzzedValue.model } }.toSet()) - } - - // Because of Long limitation fuzzer can process no more than 511 values for method with 7 parameters - @Test - @Timeout(1, unit = TimeUnit.SECONDS) - fun `the worst case works well`() { - assertDoesNotThrow { - val values = (0 until 511).map { UtPrimitiveModel(it).fuzzed() }.asSequence() - val provider = ModelProvider { descr -> - (0 until descr.parameters.size).asSequence() - .flatMap { index -> values.map { FuzzedParameter(index, it) } } - } - val parameters = (0 until 7).mapTo(mutableListOf()) { intClassId } - val fuzz = fuzz(newDescription(parameters), provider) - val first10 = fuzz.take(10).toList() - assertEquals(10, first10.size) - } - } - - private fun defaultTypes(includeStringId: Boolean = false): List { - val result = mutableListOf( - booleanClassId, - byteClassId, - charClassId, - shortClassId, - intClassId, - longClassId, - floatClassId, - doubleClassId, - ) - if (includeStringId) { - result += stringClassId - } - result += Any::class.java.id - return result - } - - private fun newDescription( - parameters: List, - concreteValues: Collection = emptyList() - ): FuzzedMethodDescription { - return FuzzedMethodDescription("testMethod", voidClassId, parameters, concreteValues) - } - -} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt deleted file mode 100644 index 35bc58d284..0000000000 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelMutatorTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.utbot.framework.plugin.api - -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.utbot.fuzzer.invertBit -import kotlin.random.Random - -class ModelMutatorTest { - - @Test - fun `invert bit works for long`() { - var attempts = 100_000 - val random = Random(2210) - sequence { - while (true) { - yield(random.nextLong()) - } - }.forEach { value -> - if (attempts-- <= 0) { return } - for (bit in 0 until Long.SIZE_BITS) { - val newValue = value.invertBit(bit) - val oldBinary = value.toBinaryString() - val newBinary = newValue.toBinaryString() - assertEquals(oldBinary.length, newBinary.length) - for (test in Long.SIZE_BITS - 1 downTo 0) { - if (test != Long.SIZE_BITS - 1 - bit) { - assertEquals(oldBinary[test], newBinary[test]) { "$oldBinary : $newBinary for value $value" } - } else { - assertNotEquals(oldBinary[test], newBinary[test]) { "$oldBinary : $newBinary for value $value" } - } - } - } - } - } - - private fun Long.toBinaryString() = java.lang.Long.toBinaryString(this).padStart(Long.SIZE_BITS, '0') -} \ 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 deleted file mode 100644 index 041ea33339..0000000000 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelProviderTest.kt +++ /dev/null @@ -1,613 +0,0 @@ -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.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.samples.FieldSetterClass -import org.utbot.framework.plugin.api.samples.OuterClassWithEnums -import org.utbot.framework.plugin.api.samples.PackagePrivateFieldAndClass -import org.utbot.framework.plugin.api.samples.SampleEnum -import org.utbot.framework.plugin.api.samples.WithInnerClass -import org.utbot.framework.plugin.api.util.executableId -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.FuzzedContext -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.IdentityPreservingIdGenerator -import org.utbot.fuzzer.ReferencePreservingIntIdGenerator -import org.utbot.fuzzer.ModelProvider.Companion.yieldValue -import org.utbot.fuzzer.defaultModelProviders -import org.utbot.fuzzer.mutators.StringRandomMutator -import org.utbot.fuzzer.providers.CharToStringModelProvider.fuzzed -import org.utbot.fuzzer.providers.EnumModelProvider -import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider -import java.util.Date -import java.util.concurrent.atomic.AtomicInteger -import kotlin.random.Random - -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, FuzzedContext.Comparison.EQ), - FuzzedConcreteValue(intClassId, 20, FuzzedContext.Comparison.NE), - FuzzedConcreteValue(intClassId, 30, FuzzedContext.Comparison.LT), - FuzzedConcreteValue(intClassId, 40, FuzzedContext.Comparison.LE), - FuzzedConcreteValue(intClassId, 50, FuzzedContext.Comparison.GT), - FuzzedConcreteValue(intClassId, 60, FuzzedContext.Comparison.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, ""), - ) - ) - - 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"), - ) - ) - - 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 method = String::class.java.getDeclaredMethod("subSequence", Int::class.java, Int::class.java) - - val description = FuzzedMethodDescription( - "method", - voidClassId, - listOf(stringClassId), - listOf( - FuzzedConcreteValue(stringClassId, "nonemptystring", FuzzedContext.Call(method.executableId)) - ) - ) - - val models = mutableMapOf>().apply { - StringConstantModelProvider.generate(description).forEach { (i, m) -> - computeIfAbsent(i) { mutableListOf() }.add(m) - } - } - - assertEquals(1, models.size) - assertEquals(1, models[0]!!.size) - - val mutate = StringRandomMutator.mutate(description, listOf(models[0]!![0]), Random(0)) - - assertEquals(1, mutate.size) - assertEquals(UtPrimitiveModel("nonemptystring"), models[0]!![0].model) - assertEquals(UtPrimitiveModel("nonemptstring"), mutate[0].value.model) - } - - @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(ReferencePreservingIntIdGenerator(0)).apply { - modelProviderForRecursiveCalls = 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 -> - val stm = model.instantiationCall - 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(ReferencePreservingIntIdGenerator(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(ReferencePreservingIntIdGenerator(0)), - parameters = listOf(classId) - ) - - assertEquals(1, models.size) - assertTrue(models[0]!!.isNotEmpty()) - val chain = (models[0]!![0] as UtAssembleModel).instantiationCall - chain.params.forEach { - assertEquals(intClassId, it.classId) - } - } - } - - @Test - fun `test fallback model can create custom values for any parameter`() { - val firstParameterIsUserGenerated = ModelProvider { - sequence { - yieldValue(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(ReferencePreservingIntIdGenerator(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(ReferencePreservingIntIdGenerator(0)), parameters = listOf(OneTwoThree::class.java.id)) - assertEquals(1, result.size) - assertEquals(3, result[0]!!.size) - OneTwoThree.values().forEachIndexed { index: Int, value -> - assertEquals(UtEnumConstantModel(index + 1, 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(ReferencePreservingIntIdGenerator(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(ReferencePreservingIntIdGenerator(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(ReferencePreservingIntIdGenerator(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(ReferencePreservingIntIdGenerator(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).instantiationCall.let { - assertEquals(1, it.params.size) - val objectParamInConstructor = it.params[0] - assertInstanceOf(UtAssembleModel::class.java, objectParamInConstructor) - val innerAssembledModel = objectParamInConstructor as UtAssembleModel - assertEquals(Any::class.java.id, innerAssembledModel.classId) - val objectCreation = innerAssembledModel.instantiationCall - 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(ReferencePreservingIntIdGenerator(0), recursionDepthLeft = 1), - parameters = listOf(MyA::class.java.id) - ) - assertEquals(1, result.size) - assertEquals(1, result[0]!!.size) - val outerModel = result[0]!![0] as UtAssembleModel - outerModel.instantiationCall.let { - val constructorParameters = it.params - assertEquals(1, constructorParameters.size) - val innerModel = (constructorParameters[0] as UtAssembleModel) - assertEquals(MyA::class.java.id, innerModel.classId) - val innerConstructorParameters = innerModel.instantiationCall - 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(ReferencePreservingIntIdGenerator(0)), - parameters = listOf(Outer::class.java.id) - ) - assertEquals(1, result.size) - var callCount = 0 - result[0]!!.filterIsInstance().forEach { outerModel -> - callCount++ - outerModel.instantiationCall.let { - val constructorParameters = it.params - assertEquals(1, constructorParameters.size) - val innerModel = (constructorParameters[0] as UtAssembleModel) - assertEquals(Inner::class.java.id, innerModel.classId) - val innerConstructorParameters = innerModel.instantiationCall - 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) - } - } - assertEquals(callCount, result[0]!!.size) - } - } - - @Test - fun `test complex object is created with setters`() { - val j = FieldSetterClass::class.java - assertEquals(6, j.declaredFields.size) - assertTrue( - setOf( - "pubStaticField", - "pubFinalField", - "pubField", - "pubFieldWithSetter", - "prvField", - "prvFieldWithSetter", - ).containsAll(j.declaredFields.map { it.name }) - ) - assertEquals(4, j.declaredMethods.size) - assertTrue( - setOf( - "getPubFieldWithSetter", - "setPubFieldWithSetter", - "getPrvFieldWithSetter", - "setPrvFieldWithSetter", - ).containsAll(j.declaredMethods.map { it.name }) - ) - - withUtContext(UtContext(this::class.java.classLoader)) { - val result = collect(ObjectModelProvider(ReferencePreservingIntIdGenerator(0), recursionDepthLeft = 1).apply { - modelProviderForRecursiveCalls = PrimitiveDefaultsModelProvider - }, parameters = listOf(FieldSetterClass::class.java.id)) - assertEquals(1, result.size) - assertEquals(2, result[0]!!.size) - assertEquals(0, (result[0]!![1] as UtAssembleModel).modificationsChain.size) { "One of models must be without any modifications" } - val expectedModificationSize = 3 - val modificationsChain = (result[0]!![0] as UtAssembleModel).modificationsChain - val actualModificationSize = modificationsChain.size - assertEquals(expectedModificationSize, actualModificationSize) { "In target class there's only $expectedModificationSize fields that can be changed, but generated $actualModificationSize modifications" } - - assertEquals("pubField", (modificationsChain[0] as UtDirectSetFieldModel).fieldId.name) - assertEquals("pubFieldWithSetter", (modificationsChain[1] as UtDirectSetFieldModel).fieldId.name) - assertEquals("setPrvFieldWithSetter", (modificationsChain[2] as UtExecutableCallModel).executable.name) - } - } - - @Test - fun `test complex object is created with setters and package private field and constructor`() { - val j = PackagePrivateFieldAndClass::class.java - assertEquals(1, j.declaredFields.size) - assertTrue( - setOf( - "pkgField", - ).containsAll(j.declaredFields.map { it.name }) - ) - - withUtContext(UtContext(this::class.java.classLoader)) { - val result = collect(ObjectModelProvider(ReferencePreservingIntIdGenerator(0)).apply { - modelProviderForRecursiveCalls = PrimitiveDefaultsModelProvider - }, parameters = listOf(PackagePrivateFieldAndClass::class.java.id)) { - packageName = PackagePrivateFieldAndClass::class.java.`package`.name - } - assertEquals(1, result.size) - assertEquals(3, result[0]!!.size) - assertEquals(0, (result[0]!![2] as UtAssembleModel).modificationsChain.size) { "One of models must be without any modifications" } - assertEquals(0, (result[0]!![1] as UtAssembleModel).modificationsChain.size) { "Modification by constructor doesn't change fields" } - val expectedModificationSize = 1 - val modificationsChain = (result[0]!![0] as UtAssembleModel).modificationsChain - val actualModificationSize = modificationsChain.size - assertEquals(expectedModificationSize, actualModificationSize) { "In target class there's only $expectedModificationSize fields that can be changed, but generated $actualModificationSize modifications" } - - assertEquals("pkgField", (modificationsChain[0] as UtDirectSetFieldModel).fieldId.name) - } - } - - @Test - fun `test that enum models in a recursive object model are consistent`() { - withUtContext(UtContext(this::class.java.classLoader)) { - val idGenerator = ReferencePreservingIntIdGenerator() - val expectedIds = SampleEnum.values().associateWith { idGenerator.getOrCreateIdForValue(it) } - - val result = collect( - ObjectModelProvider(idGenerator), - parameters = listOf(OuterClassWithEnums::class.java.id) - ) - - assertEquals(1, result.size) - val models = result[0] ?: fail("Inconsistent result") - - for (model in models) { - val outerModel = (model as? UtAssembleModel) - ?.instantiationCall - ?: fail("No final instantiation model found for the outer class") - for (param in outerModel.params) { - when (param) { - is UtEnumConstantModel -> { - assertEquals(expectedIds[param.value], param.id) - } - is UtAssembleModel -> { - for (enumParam in param.instantiationCall.params) { - enumParam as UtEnumConstantModel - assertEquals(expectedIds[enumParam.value], enumParam.id) - } - } - else -> { - fail("Unexpected parameter model: $param") - } - } - } - } - } - } - - @Test - fun `no models are created for inner non-static class`() { - withUtContext(UtContext(this::class.java.classLoader)) { - val result = collect( - ObjectModelProvider(TestIdentityPreservingIdGenerator), - parameters = listOf(WithInnerClass.NonStatic::class.id) - ) - assertEquals(0, result.size) - } - } - - @Test - fun `some models are created for inner static class`() { - withUtContext(UtContext(this::class.java.classLoader)) { - val result = collect( - ObjectModelProvider(TestIdentityPreservingIdGenerator), - parameters = listOf(WithInnerClass.Static::class.id) - ) - assertEquals(1, result.size) - assertTrue(result[0]!!.isNotEmpty()) - } - } - - private enum class OneTwoThree { - ONE, TWO, THREE - } -} - -internal fun collect( - modelProvider: ModelProvider, - name: String = "testMethod", - returnType: ClassId = voidClassId, - parameters: List, - constants: List = emptyList(), - block: FuzzedMethodDescription.() -> Unit = {} -): Map> { - return mutableMapOf>().apply { - modelProvider.generate(FuzzedMethodDescription(name, returnType, parameters, constants).apply(block)).forEach { (i, m) -> - computeIfAbsent(i) { mutableListOf() }.add(m.model) - } - } -} - -internal object TestIdentityPreservingIdGenerator : IdentityPreservingIdGenerator { - private val cache = mutableMapOf() - private val gen = AtomicInteger() - override fun getOrCreateIdForValue(value: Any): Int = cache.computeIfAbsent(value) { createId() } - override fun createId(): Int = gen.incrementAndGet() -} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/RandomExtensionsTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/RandomExtensionsTest.kt deleted file mode 100644 index 764d13219d..0000000000 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/RandomExtensionsTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -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.chooseOne -import org.utbot.fuzzer.flipCoin -import kotlin.math.abs -import kotlin.random.Random - -class RandomExtensionsTest { - - @Test - fun `default implementation always returns 0`() { - val frequencies = doubleArrayOf(1.0) - (0 until 1000).forEach { _ -> - assertEquals(0, Random.chooseOne(frequencies)) - } - } - - @ParameterizedTest(name = "seed{arguments}") - @ValueSource(ints = [0, 100, -123, 99999, 84]) - fun `with default forEach function frequencies is equal`(seed: Int) { - val random = Random(seed) - val frequencies = doubleArrayOf(10.0, 20.0, 30.0, 40.0) - val result = IntArray(frequencies.size) - assertEquals(100.0, frequencies.sum()) { "In this test every frequency value represents a percent. The sum must be equal to 100" } - val tries = 100_000 - val errors = tries / 100 // 1% - (0 until tries).forEach { _ -> - result[random.chooseOne(frequencies)]++ - } - val expected = frequencies.map { tries * it / 100} - result.forEachIndexed { index, value -> - assertTrue(abs(expected[index] - value) < errors) { - "The error should not extent $errors for $tries cases, but ${expected[index]} and $value too far" - } - } - } - - @Test - fun `inverting probabilities from the documentation`() { - val frequencies = doubleArrayOf(20.0, 80.0) - val random = Random(0) - val result = IntArray(frequencies.size) - val tries = 10_000 - val errors = tries / 100 // 1% - (0 until tries).forEach { _ -> - result[random.chooseOne(DoubleArray(frequencies.size) { 100.0 - frequencies[it] })]++ - } - result.forEachIndexed { index, value -> - val expected = frequencies[frequencies.size - 1 - index] * errors - assertTrue(abs(value - expected) < errors) { - "The error should not extent 100 for 10 000 cases, but $expected and $value too far" - } - } - } - - @Test - fun `flip the coin is fair enough`() { - val random = Random(0) - var result = 0 - val probability = 20 - val experiments = 1_000_000 - for (i in 0 until experiments) { - if (random.flipCoin(probability)) { - result++ - } - } - val error = experiments / 1000 // 0.1 % - assertTrue(abs(result - experiments * probability / 100) < error) - } -} \ No newline at end of file diff --git a/utbot-fuzzing/build.gradle.kts b/utbot-fuzzing/build.gradle.kts new file mode 100644 index 0000000000..a5ab65bf30 --- /dev/null +++ b/utbot-fuzzing/build.gradle.kts @@ -0,0 +1,7 @@ +val kotlinLoggingVersion: String by rootProject +val rgxgenVersion: String by rootProject + +dependencies { + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation(group = "org.cornutum.regexp", name = "regexp-gen", version = "2.0.1") +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/java/org/utbot/fuzzing/demo/A.java b/utbot-fuzzing/src/main/java/org/utbot/fuzzing/demo/A.java new file mode 100644 index 0000000000..d073a0f0a6 --- /dev/null +++ b/utbot-fuzzing/src/main/java/org/utbot/fuzzing/demo/A.java @@ -0,0 +1,33 @@ +package org.utbot.fuzzing.demo; + +/** + * Example class that is used in {@link JavaFuzzingKt} + */ +@SuppressWarnings("unused") +final class A { + + public String name; + public int age; + public A copy; + + public A() { + } + + public A(String name) { + this.name = name; + } + + public A(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public String toString() { + return "A{" + + "name='" + name + '\'' + + ", age=" + age + + ", copy=" + copy + + '}'; + } +} diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt new file mode 100644 index 0000000000..a33cba2ea0 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Api.kt @@ -0,0 +1,743 @@ +@file:JvmName("FuzzingApi") +package org.utbot.fuzzing + +import kotlinx.coroutines.* +import mu.KotlinLogging +import org.utbot.fuzzing.seeds.KnownValue +import org.utbot.fuzzing.utils.MissedSeed +import org.utbot.fuzzing.utils.chooseOne +import org.utbot.fuzzing.utils.flipCoin +import org.utbot.fuzzing.utils.transformIfNotEmpty +import kotlin.random.Random + +private val logger by lazy { KotlinLogging.logger {} } + +/** + * Describes some data to start fuzzing: initial seeds and how to run target program using generated values. + * + * @see [org.utbot.fuzzing.demo.AbcFuzzingKt] + * @see [org.utbot.fuzzing.demo.JavaFuzzing] + * @see [org.utbot.fuzzing.demo.JsonFuzzingKt] + */ +interface Fuzzing, FEEDBACK : Feedback> { + + /** + * Before producing seeds, this method is called to recognize, + * whether seeds should be generated especially. + * + * [Description.clone] method must be overridden, or it throws an exception if the scope is changed. + */ + fun enrich(description: DESCRIPTION, type: TYPE, scope: Scope) {} + + /** + * Generates seeds for a concrete type. + * + * If any information except type is required, like parameter index or another, + * [description] parameter can be used. + * + * NB: Fuzzing implementation caches seeds for concrete types to improve performance because + * usually all seeds are statically defined. In case some dynamic behavior is required use + * [Feedback.control] to reset caches. + */ + fun generate(description: DESCRIPTION, type: TYPE): Sequence> + + /** + * This method is called on every value list generated by fuzzer. + * + * Fuzzing combines, randomize and mutates values using the seeds. + * Then it generates values and runs them with this method. This method should provide some feedback, + * which is the most important part for a good fuzzing result. [emptyFeedback] can be provided only for test + * or infinite loops. Consider implementing own implementation of [Feedback] to provide more correct data or + * use [BaseFeedback] to generate key based feedback. In this case, the key is used to analyze what value should be next. + * + * @param description contains user-defined information about the current run. Can be used as a state of the run. + * @param values current values to process. + */ + suspend fun handle(description: DESCRIPTION, values: List): FEEDBACK + + /** + * Starts fuzzing with new description but with copy of [Statistic]. + */ + suspend fun fork(description: DESCRIPTION, statistics: Statistic) { + fuzz(description, StatisticImpl(statistics)) + } + + /** + * Checks whether the fuzzer should stop. + */ + suspend fun isCancelled(description: DESCRIPTION, stats: Statistic): Boolean { + return false + } + + suspend fun beforeIteration(description: DESCRIPTION, statistics: Statistic) { } + suspend fun afterIteration(description: DESCRIPTION, statistics: Statistic) { } +} + +/** + * Some description of the current fuzzing run. Usually, it contains the name of the target method and its parameter list. + */ +open class Description( + parameters: List +) { + val parameters: List = parameters.toList() + + open fun clone(scope: Scope): Description { + error("Scope was changed for $this, but method clone is not specified") + } +} + +class Scope( + val parameterIndex: Int, + val recursionDepth: Int, + private val properties: MutableMap, Any?> = hashMapOf(), +) { + fun putProperty(param: ScopeProperty, value: T) { + properties[param] = value + } + + fun getProperty(param: ScopeProperty): T? { + @Suppress("UNCHECKED_CAST") + return properties[param] as? T + } + + fun isNotEmpty(): Boolean = properties.isNotEmpty() +} + +class ScopeProperty( + val description: String +) { + fun getValue(scope: Scope): T? { + return scope.getProperty(this) + } +} + +/** + * Input value that fuzzing knows how to build and use them. + */ +sealed interface Seed { + /** + * Simple value is just a concrete value that should be used as is. + * + * Any mutation can be provided if it is applicable to this value. + */ + class Simple(val value: RESULT, val mutation: (RESULT, random: Random) -> RESULT = { f, _ -> f }): Seed + + /** + * Known value is a typical value that can be manipulated by fuzzing without knowledge about object structure + * in concrete language. For example, integer can be represented as a bit vector of n-bits. + * + * The list of the known to fuzzing values are: + * + * 1. BitVectorValue represents a vector of bits. + * 2. ... + */ + class Known>(val value: V, val build: (V) -> RESULT): Seed + + /** + * Recursive value defines an object with typically has a constructor and list of modifications. + * + * This task creates a tree of object values. + */ + class Recursive( + val construct: Routine.Create, + val modify: Sequence> = emptySequence(), + val empty: Routine.Empty + ) : Seed + + /** + * Collection is a task, that has 2 main options: + * + * 1. Construction the collection + * 2. Modification of the collections that depends on some number of iterations. + */ + class Collection( + val construct: Routine.Collection, + val modify: Routine.ForEach + ) : Seed +} + +/** + * Routine is a task that is used to build a value. + * + * There are several types of a routine, which all are generally only functions. + * These functions accept some data and generate target value. + */ +sealed class Routine(val types: List) : Iterable by types { + + /** + * Creates an empty recursive object. + */ + class Create( + types: List, + val builder: (arguments: List) -> R, + ) : Routine(types) { + operator fun invoke(arguments: List): R = builder(arguments) + } + + /** + * Calls routine for a given object. + */ + class Call( + types: List, + val callable: (instance: R, arguments: List) -> Unit + ) : Routine(types) { + operator fun invoke(instance: R, arguments: List) { + callable(instance, arguments) + } + } + + /** + * Creates a collection of concrete sizes. + */ + class Collection( + val builder: (size: Int) -> R, + ) : Routine(emptyList()) { + operator fun invoke(size: Int): R = builder(size) + } + + /** + * Is called for a collection with index of iterations. + */ + class ForEach( + types: List, + val callable: (instance: R, index: Int, arguments: List) -> Unit + ) : Routine(types) { + operator fun invoke(instance: R, index: Int, arguments: List) = callable(instance, index, arguments) + } + + /** + * Empty routine that generates a concrete value. + */ + class Empty( + val builder: () -> R, + ) : Routine(emptyList()) { + operator fun invoke(): R = builder() + } +} + +/** + * Interface to force [Any.hashCode] and [Any.equals] implementation for [Feedback], + * because it is used in the map. + */ +interface AsKey { + override fun equals(other: Any?): Boolean + override fun hashCode(): Int +} + +/** + * Language feedback from a concrete execution of the target code. + */ +interface Feedback : AsKey { + /** + * Controls what fuzzing should do. + * + * @see [Control] + */ + val control: Control +} + +/** + * Base implementation of [Feedback]. + * + * NB! [VALUE] type must implement [equals] and [hashCode] due to the fact it uses as a key in map. + * If it doesn't implement those methods, [OutOfMemoryError] is possible. + */ +data class BaseFeedback( + val result: VALUE, + override val control: Control, +) : Feedback + +/** + * Controls fuzzing execution. + */ +enum class Control { + /** + * Analyze feedback and continue. + */ + CONTINUE, + + /** + * Do not process this feedback and just start the next value generation. + */ + PASS, + + /** + * Stop fuzzing. + */ + STOP, +} + +/** + * Returns empty feedback which is equals to any another empty feedback. + */ +@Suppress("UNCHECKED_CAST") +fun emptyFeedback(): Feedback = (EmptyFeedback as Feedback) + +private object EmptyFeedback : Feedback { + override val control: Control + get() = Control.CONTINUE + + override fun equals(other: Any?): Boolean { + return true + } + + override fun hashCode(): Int { + return 0 + } +} + +class NoSeedValueException internal constructor( + // this type cannot be generalized because Java forbids types for [Throwable]. + val type: Any? +) : Exception() { + override fun fillInStackTrace(): Throwable { + return this + } + + override val message: String + get() = "No seed candidates generated for type: $type" +} + +suspend fun , F : Feedback> Fuzzing.fuzz( + description: D, + random: Random = Random(0), + configuration: Configuration = Configuration() +) { + fuzz(description, StatisticImpl(random = random, configuration = configuration)) +} + +/** + * Starts fuzzing for this [Fuzzing] object. + * + * This is an entry point for every fuzzing. + */ +private suspend fun , F : Feedback> Fuzzing.fuzz( + description: D, + statistic: StatisticImpl, +) { + val random = statistic.random + val configuration = statistic.configuration + val fuzzing = this + val typeCache = hashMapOf>>() + val mutationFactory = MutationFactory() + fun fuzzOne(parameters: List): Node = fuzz( + parameters = parameters, + fuzzing = fuzzing, + description = description, + random = random, + configuration = configuration, + builder = PassRoutine("Main Routine"), + state = State(typeCache, statistic.missedTypes), + ) + + while (!fuzzing.isCancelled(description, statistic)) { + beforeIteration(description, statistic) + val values = if (statistic.isNotEmpty() && random.flipCoin(configuration.probSeedRetrievingInsteadGenerating)) { + statistic.getRandomSeed(random, configuration).let { + mutationFactory.mutate(it, random, configuration) + } + } else { + val actualParameters = description.parameters + // fuzz one value, seems to be bad, when have only a few and simple values + fuzzOne(actualParameters).let { + if (random.flipCoin(configuration.probMutationRate)) { + mutationFactory.mutate(it, random, configuration) + } else { + it + } + } + } + afterIteration(description, statistic) + + yield() + statistic.apply { + totalRuns++ + } + check(values.parameters.size == values.result.size) { "Cannot create value for ${values.parameters}" } + val valuesCache = mutableMapOf, R>() + val result = values.result.map { valuesCache.computeIfAbsent(it) { r -> create(r) } } + val feedback = fuzzing.handle(description, result) + when (feedback.control) { + Control.CONTINUE -> { + statistic.put(random, configuration, feedback, values) + } + Control.STOP -> { + break + } + Control.PASS -> {} + } + } +} + + +///region Implementation of the fuzzing and non-public functions. + +private fun , FEEDBACK : Feedback> fuzz( + parameters: List, + fuzzing: Fuzzing, + description: DESCRIPTION, + random: Random, + configuration: Configuration, + builder: Routine, + state: State, +): Node { + val typeCache = mutableMapOf>>() + val result = parameters.mapIndexed { index, type -> + val results = typeCache.computeIfAbsent(type) { mutableListOf() } + if (results.isNotEmpty() && random.flipCoin(configuration.probReuseGeneratedValueForSameType)) { + // we need to check cases when one value is passed for different arguments + results.random(random) + } else { + produce(type, fuzzing, description, random, configuration, state.copy { + parameterIndex = index + }).also { + results += it + } + } + } + // is not inlined to debug values generated for a concrete type + return Node(result, parameters, builder) +} + +private fun , FEEDBACK : Feedback> produce( + type: TYPE, + fuzzing: Fuzzing, + description: DESCRIPTION, + random: Random, + configuration: Configuration, + state: State, +): Result { + val scope = Scope(state.parameterIndex, state.recursionTreeDepth).apply { + fuzzing.enrich(description, type, this) + } + @Suppress("UNCHECKED_CAST") + val seeds = when { + scope.isNotEmpty() -> { + fuzzing.generate(description.clone(scope) as DESCRIPTION, type).toList() + } + else -> state.cache.computeIfAbsent(type) { + fuzzing.generate(description, it).toList() + } + } + if (seeds.isEmpty()) { + throw NoSeedValueException(type) + } + return seeds.random(random).let { + when (it) { + is Seed.Simple -> Result.Simple(it.value, it.mutation) + is Seed.Known -> it.asResult() + is Seed.Recursive -> reduce(it, fuzzing, description, random, configuration, state) + is Seed.Collection -> reduce(it, fuzzing, description, random, configuration, state) + } + } +} + +/** + * reduces [Seed.Collection] type. When `configuration.recursionTreeDepth` limit is reached it creates + * an empty collection and doesn't do any modification to it. + */ +private fun , FEEDBACK : Feedback> reduce( + task: Seed.Collection, + fuzzing: Fuzzing, + description: DESCRIPTION, + random: Random, + configuration: Configuration, + state: State, +): Result { + return if (state.recursionTreeDepth > configuration.recursionTreeDepth) { + Result.Empty { task.construct.builder(0) } + } else try { + val iterations = when { + state.iterations >= 0 && random.flipCoin(configuration.probCreateRectangleCollectionInsteadSawLike) -> state.iterations + random.flipCoin(configuration.probEmptyCollectionCreation) -> 0 + else -> random.nextInt(1, configuration.collectionIterations + 1) + } + Result.Collection( + construct = fuzz( + task.construct.types, + fuzzing, + description, + random, + configuration, + task.construct, + state.copy { + recursionTreeDepth++ + } + ), + modify = if (random.flipCoin(configuration.probCollectionDuplicationInsteadCreateNew)) { + val result = fuzz(task.modify.types, fuzzing, description, random, configuration, task.modify, state.copy { + recursionTreeDepth++ + this.iterations = iterations + parameterIndex = -1 + }) + List(iterations) { result } + } else { + (0 until iterations).map { + fuzz(task.modify.types, fuzzing, description, random, configuration, task.modify, state.copy { + recursionTreeDepth++ + this.iterations = iterations + }) + } + }, + iterations = iterations + ) + } catch (nsv: NoSeedValueException) { + @Suppress("UNCHECKED_CAST") + state.missedTypes[nsv.type as TYPE] = task + if (configuration.generateEmptyCollectionsForMissedTypes) { + Result.Empty { task.construct.builder(0) } + } else { + throw nsv + } + } +} + +/** + * reduces [Seed.Recursive] type. When `configuration.recursionTreeDepth` limit is reached it calls + * `Seed.Recursive#empty` routine to create an empty object. + */ +private fun , FEEDBACK : Feedback> reduce( + task: Seed.Recursive, + fuzzing: Fuzzing, + description: DESCRIPTION, + random: Random, + configuration: Configuration, + state: State, +): Result { + return if (state.recursionTreeDepth > configuration.recursionTreeDepth) { + Result.Empty { task.empty.builder() } + } else try { + Result.Recursive( + construct = fuzz( + task.construct.types, + fuzzing, + description, + random, + configuration, + task.construct, + state.copy { + recursionTreeDepth++ + iterations = -1 + parameterIndex = -1 + } + ), + modify = task.modify + .toMutableList() + .transformIfNotEmpty { + shuffle(random) + take(configuration.maxNumberOfRecursiveSeedModifications) + } + .mapTo(arrayListOf()) { routine -> + fuzz( + routine.types, + fuzzing, + description, + random, + configuration, + routine, + state.copy { + recursionTreeDepth++ + iterations = -1 + parameterIndex = -1 + } + ) + } + ) + } catch (nsv: NoSeedValueException) { + @Suppress("UNCHECKED_CAST") + state.missedTypes[nsv.type as TYPE] = task + if (configuration.generateEmptyRecursiveForMissedTypes) { + Result.Empty { task.empty.builder() } + } else { + throw nsv + } + } +} + + +/** + * Creates a real result. + * + * Fuzzing doesn't use real object because it mutates values by itself. + */ +@Suppress("UNCHECKED_CAST") +private fun create(result: Result): R = when(result) { + is Result.Simple -> result.result + is Result.Known -> (result.build as KnownValue<*>.() -> R)(result.value) + is Result.Recursive -> with(result) { + val obj: R = when (val c = construct.builder) { + is Routine.Create -> c(construct.result.map { create(it) }) + is Routine.Empty -> c() + else -> error("Undefined create method") + } + modify.forEach { func -> + when (val builder = func.builder) { + is Routine.Call -> builder(obj, func.result.map { create(it) }) + is PassRoutine -> logger.warn { "Routine pass: ${builder.description}" } + else -> error("Undefined object call method ${func.builder}") + } + } + obj + } + is Result.Collection -> with(result) { + val collection: R = when (val c = construct.builder) { + is Routine.Create -> c(construct.result.map { create(it) }) + is Routine.Empty -> c() + is Routine.Collection -> c(modify.size) + else -> error("Undefined create method") + } + modify.forEachIndexed { index, func -> + when (val builder = func.builder) { + is Routine.ForEach -> builder(collection, index, func.result.map { create(it) }) + is PassRoutine -> logger.warn { "Routine pass: ${builder.description}" } + else -> error("Undefined collection call method ${func.builder}") + } + } + collection + } + is Result.Empty -> result.build() +} + +/** + * Empty routine to start a recursion within [fuzz]. + */ +private data class PassRoutine(val description: String) : Routine(emptyList()) + +/** + * Internal state for one fuzzing run. + */ +private class State( + val cache: MutableMap>>, + val missedTypes: MissedSeed, + val recursionTreeDepth: Int = 1, + val iterations: Int = -1, + val parameterIndex: Int = -1, +) { + + fun copy(block: Builder.() -> Unit): State { + return Builder(this).apply(block).build() + } + + class Builder( + state: State + ) { + var recursionTreeDepth: Int = state.recursionTreeDepth + var cache: MutableMap>> = state.cache + var missedTypes: MissedSeed = state.missedTypes + var iterations: Int = state.iterations + var parameterIndex: Int = state.parameterIndex + + fun build(): State { + return State( + cache, + missedTypes, + recursionTreeDepth, + iterations, + parameterIndex, + ) + } + } +} + +/** + * The result of producing real values for the language. + */ +sealed interface Result { + + /** + * Simple result as is. + */ + class Simple(val result: RESULT, val mutation: (RESULT, random: Random) -> RESULT = emptyMutation()) : Result + + /** + * Known value. + */ + class Known>(val value: V, val build: (V) -> RESULT) : Result + /** + * A tree of object that has constructor and some modifications. + */ + class Recursive( + val construct: Node, + val modify: List>, + ) : Result + + /** + * A tree of collection-like structures and their modification. + */ + class Collection( + val construct: Node, + val modify: List>, + val iterations: Int, + ) : Result + + /** + * Empty result which just returns a value. + */ + class Empty( + val build: () -> RESULT + ) : Result +} + +/** + * Temporary object to storage information about partly calculated values tree. + */ +class Node( + val result: List>, + val parameters: List, + val builder: Routine, +) + +private class StatisticImpl>( + override var totalRuns: Long = 0, + override val startTime: Long = System.nanoTime(), + override var missedTypes: MissedSeed = MissedSeed(), + override val random: Random, + override val configuration: Configuration, +) : Statistic { + + constructor(source: Statistic) : this( + totalRuns = source.totalRuns, + startTime = source.startTime, + missedTypes = source.missedTypes, + random = source.random, + configuration = source.configuration.copy(), + ) + + override val elapsedTime: Long + get() = System.nanoTime() - startTime + private val seeds = linkedMapOf>() + private val count = linkedMapOf() + + fun put(random: Random, configuration: Configuration, feedback: FEEDBACK, seed: Node) { + if (random.flipCoin(configuration.probUpdateSeedInsteadOfKeepOld)) { + seeds[feedback] = seed + } else { + seeds.putIfAbsent(feedback, seed) + } + count[feedback] = count.getOrDefault(feedback, 0L) + 1L + } + + fun getRandomSeed(random: Random, configuration: Configuration): Node { + if (seeds.isEmpty()) error("Call `isNotEmpty` before getting the seed") + val entries = seeds.entries.toList() + val frequencies = DoubleArray(seeds.size).also { f -> + entries.forEachIndexed { index, (key, _) -> + f[index] = configuration.energyFunction(count.getOrDefault(key, 0L)) + } + } + val index = random.chooseOne(frequencies) + return entries[index].value + } + + fun isNotEmpty() = seeds.isNotEmpty() +} +///endregion + + +///region Utilities +@Suppress("UNCHECKED_CAST") +private fun > Seed.Known.asResult(): Result.Known { + val value: T = value as T + return Result.Known(value, build as KnownValue.() -> RESULT) +} +///endregion diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt new file mode 100644 index 0000000000..a6c19926c9 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Configuration.kt @@ -0,0 +1,98 @@ +package org.utbot.fuzzing + +import kotlin.math.pow + +/** + * Configures fuzzing behaviour. Usually, it is not required to tune anything. + */ +data class Configuration( + + /** + * Choose between already generated values and new generation of values. + */ + var probSeedRetrievingInsteadGenerating: Int = 70, + + /** + * Choose between generation and mutation. + */ + var probMutationRate: Int = 99, + + /** + * Fuzzer creates a tree of object for generating values. At some point this recursion should be stopped. + * + * To stop recursion [Seed.Recursive.empty] is called to create new values. + */ + var recursionTreeDepth: Int = 4, + + /** + * The limit of collection size to create. + */ + var collectionIterations: Int = 5, + + /** + * Energy function that is used to choose seeded value. + */ + var energyFunction: (x: Long) -> Double = { x -> 1 / x.coerceAtLeast(1L).toDouble().pow(2) }, + + /** + * Probability to prefer shuffling collection instead of mutation one value from modification + */ + var probCollectionShuffleInsteadResultMutation: Int = 75, + + /** + * Probability of creating shifted array values instead of generating new values for modification. + */ + var probCollectionDuplicationInsteadCreateNew: Int = 10, + + /** + * Probability of creating empty collections + */ + var probEmptyCollectionCreation: Int = 1, + + /** + * Probability to prefer change constructor instead of modification. + */ + var probConstructorMutationInsteadModificationMutation: Int = 30, + + /** + * Probability to a shuffle modification list of the recursive object + */ + var probShuffleAndCutRecursiveObjectModificationMutation: Int = 30, + + /** + * Probability to prefer create rectangle collections instead of creating saw-like one. + */ + var probCreateRectangleCollectionInsteadSawLike: Int = 80, + + /** + * Probability of updating old seed instead of leaving to the new one when [Feedback] has same key. + */ + var probUpdateSeedInsteadOfKeepOld: Int = 70, + + /** + * When mutating StringValue a new string will not exceed this value. + */ + var maxStringLengthWhenMutated: Int = 128, + + /** + * Probability of reusing same generated value when 2 or more parameters have the same type. + */ + var probReuseGeneratedValueForSameType: Int = 1, + + /** + * When true any [Seed.Collection] will not try + * to generate modification if a current type is already known to fail to generate values. + */ + var generateEmptyCollectionsForMissedTypes: Boolean = true, + + /** + * When true any [Seed.Recursive] will not try + * to generate a recursive object, but will use [Seed.Recursive.empty] instead. + */ + var generateEmptyRecursiveForMissedTypes: Boolean = true, + + /** + * Limits maximum number of recursive seed modifications + */ + var maxNumberOfRecursiveSeedModifications: Int = 10, +) \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt new file mode 100644 index 0000000000..9868be39b8 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Mutations.kt @@ -0,0 +1,334 @@ +@file:Suppress("ReplaceRangeStartEndInclusiveWithFirstLast") + +package org.utbot.fuzzing + +import org.utbot.fuzzing.seeds.* +import org.utbot.fuzzing.utils.chooseOne +import org.utbot.fuzzing.utils.flipCoin +import kotlin.random.Random + +class MutationFactory { + + fun mutate(node: Node, random: Random, configuration: Configuration): Node { + if (node.result.isEmpty()) return node + val indexOfMutatedResult = random.chooseOne(node.result.map(::rate).toDoubleArray()) + val recursive: NodeMutation = NodeMutation { n, r, c -> + mutate(n, r, c) + } + val mutated = when (val resultToMutate = node.result[indexOfMutatedResult]) { + is Result.Simple -> Result.Simple(resultToMutate.mutation(resultToMutate.result, random), resultToMutate.mutation) + is Result.Known -> { + val mutations = resultToMutate.value.mutations() + if (mutations.isNotEmpty()) { + resultToMutate.mutate(mutations.random(random), random, configuration) + } else { + resultToMutate + } + } + is Result.Recursive -> { + when { + resultToMutate.modify.isEmpty() || random.flipCoin(configuration.probConstructorMutationInsteadModificationMutation) -> + RecursiveMutations.Constructor() + random.flipCoin(configuration.probShuffleAndCutRecursiveObjectModificationMutation) -> + RecursiveMutations.ShuffleAndCutModifications() + else -> + RecursiveMutations.Mutate() + }.mutate(resultToMutate, recursive, random, configuration) + } + is Result.Collection -> if (resultToMutate.modify.isNotEmpty()) { + when { + random.flipCoin(100 - configuration.probCollectionShuffleInsteadResultMutation) -> + CollectionMutations.Mutate() + else -> + CollectionMutations.Shuffle() + }.mutate(resultToMutate, recursive, random, configuration) + } else { + resultToMutate + } + is Result.Empty -> resultToMutate + } + return Node(node.result.toMutableList().apply { + set(indexOfMutatedResult, mutated) + }, node.parameters, node.builder) + } + + /** + * Rates somehow the result. + * + * For example, fuzzing should not try to mutate some empty structures, like empty collections or objects. + */ + private fun rate(result: Result): Double { + if (!canMutate(result)) { + return ALMOST_ZERO + } + return when (result) { + is Result.Recursive -> if (result.construct.parameters.isEmpty() and result.modify.isEmpty()) ALMOST_ZERO else 0.5 + is Result.Collection -> if (result.iterations == 0) return ALMOST_ZERO else 0.7 + is StringValue -> 2.0 + is Result.Known -> 1.2 + is Result.Simple -> 2.0 + is Result.Empty -> ALMOST_ZERO + } + } + + private fun canMutate(node: Result): Boolean { + return when (node) { + is Result.Simple -> node.mutation === emptyMutation() + is Result.Known -> node.value.mutations().isNotEmpty() + is Result.Recursive -> node.modify.isNotEmpty() + is Result.Collection -> node.modify.isNotEmpty() && node.iterations > 0 + is Result.Empty -> false + } + } + + @Suppress("UNCHECKED_CAST") + private fun > Result.Known.mutate(mutation: Mutation, random: Random, configuration: Configuration): Result.Known { + val source: T = value as T + val mutate = mutation.mutate(source, random, configuration) + return Result.Known( + mutate, + build as (T) -> RESULT + ) + } +} + +private const val ALMOST_ZERO = 1E-7 +private val IDENTITY_MUTATION: (Any, random: Random) -> Any = { f, _ -> f } + +fun emptyMutation(): (RESULT, random: Random) -> RESULT { + @Suppress("UNCHECKED_CAST") + return IDENTITY_MUTATION as (RESULT, random: Random) -> RESULT +} + +/** + * Mutations is an object which applies some changes to the source object + * and then returns a new object (or old one without changes). + */ +fun interface Mutation { + fun mutate(source: T, random: Random, configuration: Configuration): T +} + +sealed class BitVectorMutations : Mutation { + + abstract fun rangeOfMutation(source: BitVectorValue): IntRange + + override fun mutate(source: BitVectorValue, random: Random, configuration: Configuration): BitVectorValue { + with (rangeOfMutation(source)) { + val firstBits = random.nextInt(start, endInclusive.coerceAtLeast(1)) + return BitVectorValue(source, this@BitVectorMutations).apply { this[firstBits] = !this[firstBits] } + } + } + + object SlightDifferent : BitVectorMutations() { + override fun rangeOfMutation(source: BitVectorValue) = 0 .. source.size / 4 + } + + object DifferentWithSameSign : BitVectorMutations() { + override fun rangeOfMutation(source: BitVectorValue) = source.size / 4 .. source.size + } + + object ChangeSign : BitVectorMutations() { + override fun rangeOfMutation(source: BitVectorValue) = source.size - 1 .. source.size + } +} + +sealed interface IEEE754Mutations : Mutation { + + object ChangeSign : IEEE754Mutations { + override fun mutate(source: IEEE754Value, random: Random, configuration: Configuration): IEEE754Value { + return IEEE754Value(source, this).apply { + setRaw(0, !getRaw(0)) + } + } + } + + object Mantissa : IEEE754Mutations { + override fun mutate(source: IEEE754Value, random: Random, configuration: Configuration): IEEE754Value { + val i = random.nextInt(0, source.mantissaSize) + return IEEE754Value(source, this).apply { + setRaw(1 + exponentSize + i, !getRaw(1 + exponentSize + i)) + } + } + } + + object Exponent : IEEE754Mutations { + override fun mutate(source: IEEE754Value, random: Random, configuration: Configuration): IEEE754Value { + val i = random.nextInt(0, source.exponentSize) + return IEEE754Value(source, this).apply { + setRaw(1 + i, !getRaw(1 + i)) + } + } + } +} + +sealed interface StringMutations : Mutation { + + object AddCharacter : StringMutations { + override fun mutate(source: StringValue, random: Random, configuration: Configuration): StringValue { + val value = source.value + if (value.length >= configuration.maxStringLengthWhenMutated) { + return source + } + val position = random.nextInt(value.length + 1) + val charToMutate = if (value.isNotEmpty()) { + value.random(random) + } else { + // use any meaningful character from the ascii table + random.nextInt(33, 127).toChar() + } + val newString = buildString { + append(value.substring(0, position)) + // try to change char to some that is close enough to origin char + val charTableSpread = 64 + if (random.nextBoolean()) { + append(charToMutate - random.nextInt(1, charTableSpread)) + } else { + append(charToMutate + random.nextInt(1, charTableSpread)) + } + append(value.substring(position, value.length)) + } + return StringValue(newString, lastMutation = this, mutatedFrom = source) + } + } + + object RemoveCharacter : StringMutations { + override fun mutate(source: StringValue, random: Random, configuration: Configuration): StringValue { + val value = source.value + val position = random.nextInt(value.length + 1) + if (position >= value.length) return source + val toRemove = random.nextInt(value.length) + val newString = buildString { + append(value.substring(0, toRemove)) + append(value.substring(toRemove + 1, value.length)) + } + return StringValue(newString, this) + } + } + + object ShuffleCharacters : StringMutations { + override fun mutate(source: StringValue, random: Random, configuration: Configuration): StringValue { + return StringValue( + value = String(source.value.toCharArray().apply { shuffle(random) }), + lastMutation = this + ) + } + } +} + +fun interface NodeMutation : Mutation> + +sealed interface CollectionMutations : Mutation, NodeMutation>> { + + override fun mutate( + source: Pair, NodeMutation>, + random: Random, + configuration: Configuration + ): Pair, NodeMutation> { + return mutate(source.first, source.second, random, configuration) to source.second + } + + fun mutate( + source: Result.Collection, + recursive: NodeMutation, + random: Random, + configuration: Configuration + ) : Result.Collection + + class Shuffle : CollectionMutations { + override fun mutate( + source: Result.Collection, + recursive: NodeMutation, + random: Random, + configuration: Configuration + ): Result.Collection { + return Result.Collection( + construct = source.construct, + modify = source.modify.toMutableList().shuffled(random), + iterations = source.iterations + ) + } + } + + class Mutate : CollectionMutations { + override fun mutate( + source: Result.Collection, + recursive: NodeMutation, + random: Random, + configuration: Configuration + ): Result.Collection { + return Result.Collection( + construct = source.construct, + modify = source.modify.toMutableList().apply { + val i = random.nextInt(0, source.modify.size) + set(i, recursive.mutate(source.modify[i], random, configuration)) + }, + iterations = source.iterations + ) + } + } +} + +sealed interface RecursiveMutations : Mutation, NodeMutation>> { + + override fun mutate( + source: Pair, NodeMutation>, + random: Random, + configuration: Configuration + ): Pair, NodeMutation> { + return mutate(source.first, source.second, random, configuration) to source.second + } + + fun mutate( + source: Result.Recursive, + recursive: NodeMutation, + random: Random, + configuration: Configuration + ) : Result.Recursive + + + class Constructor : RecursiveMutations { + override fun mutate( + source: Result.Recursive, + recursive: NodeMutation, + random: Random, + configuration: Configuration + ): Result.Recursive { + return Result.Recursive( + construct = recursive.mutate(source.construct,random, configuration), + modify = source.modify + ) + } + } + + class ShuffleAndCutModifications : RecursiveMutations { + override fun mutate( + source: Result.Recursive, + recursive: NodeMutation, + random: Random, + configuration: Configuration + ): Result.Recursive { + return Result.Recursive( + construct = source.construct, + modify = source.modify.shuffled(random).take(random.nextInt(source.modify.size + 1)) + ) + } + } + + class Mutate : RecursiveMutations { + override fun mutate( + source: Result.Recursive, + recursive: NodeMutation, + random: Random, + configuration: Configuration + ): Result.Recursive { + return Result.Recursive( + construct = source.construct, + modify = source.modify.toMutableList().apply { + val i = random.nextInt(0, source.modify.size) + set(i, recursive.mutate(source.modify[i], random, configuration)) + } + ) + } + + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt new file mode 100644 index 0000000000..0a47c42624 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Providers.kt @@ -0,0 +1,223 @@ +package org.utbot.fuzzing + +import mu.KotlinLogging +import kotlin.random.Random + +private val logger by lazy { KotlinLogging.logger {} } + +/** + * Entry point to run fuzzing. + */ +suspend fun , FEEDBACK : Feedback> runFuzzing( + provider: ValueProvider, + description: DESCRIPTION, + random: Random = Random(0), + configuration: Configuration = Configuration(), + handle: suspend (description: DESCRIPTION, values: List) -> FEEDBACK +) { + BaseFuzzing(listOf(provider), handle).fuzz(description, random, configuration) +} + +/** + * Implements base concepts that use providers to generate values for some types. + * + * @param providers is a list of "type to values" generator + * @param exec this function is called when fuzzer generates values of type R to run it with target program. + */ +class BaseFuzzing, F : Feedback>( + val providers: List>, + val exec: suspend (description: D, values: List) -> F +) : Fuzzing { + + constructor(vararg providers: ValueProvider, exec: suspend (description: D, values: List) -> F) : this(providers.toList(), exec) + + override fun enrich(description: D, type: T, scope: Scope) { + providers.asSequence().forEach { + it.enrich(description, type, scope) + } + } + + override fun generate(description: D, type: T): Sequence> { + return providers.asSequence().flatMap { provider -> + try { + if (provider.accept(type)) { + provider.generate(description, type) + } else { + emptySequence() + } + } catch (t: Throwable) { + logger.error(t) { "Error occurs in value provider: $provider" } + emptySequence() + } + } + } + + override suspend fun handle(description: D, values: List): F { + return exec(description, values) + } +} + +/** + * Value provider generates [Seed] and has other methods to combine providers. + */ +fun interface ValueProvider> { + + fun enrich(description: D, type: T, scope: Scope) {} + + /** + * Generate a sequence of [Seed] that is merged with values generated by other provider. + */ + fun generate(description: D, type: T): Sequence> + + /** + * Validates if this provider is applicable to some type. + */ + fun accept(type: T): Boolean = true + + /** + * Combines this model provider with `anotherValueProviders` into one instance. + * + * This model provider is called before `anotherValueProviders`. + */ + infix fun with(anotherValueProvider: ValueProvider): ValueProvider { + fun toList(m: ValueProvider) = if (m is Combined) m.providers else listOf(m) + return Combined(toList(this) + toList(anotherValueProvider)) + } + + /** + * Removes `anotherValueProviders): ValueProvider { + return except { it == anotherValueProvider } + } + + /** + * Removes providers matching [filter] from the current one. + */ + fun except(filter: (ValueProvider) -> Boolean): ValueProvider = + map { if (filter(it)) Combined(emptyList()) else it } + + /** + * Applies [transform] for current provider + */ + fun map(transform: (ValueProvider) -> ValueProvider): ValueProvider = + transform(this) + + /** + * Uses fallback value provider in case when 'this' one failed to generate any value. + */ + fun withFallback(fallback: ValueProvider) : ValueProvider { + return Fallback(this, fallback) + } + + /** + * Creates a new value provider that creates default value if no values are generated by this provider. + */ + fun withFallback(fallbackSupplier: (T) -> Seed) : ValueProvider { + return withFallback { _, type -> + sequenceOf(fallbackSupplier(type)) + } + } + + fun letIf(flag: Boolean, block: (ValueProvider) -> ValueProvider) : ValueProvider { + return if (flag) block(this) else this + } + + /** + * Checks if current provider has fallback and return the initial provider. + * + * If the initial provider is also fallback, then it is unwrapped too. + * + * @return unwrapped provider or this if it is not fallback + */ + fun unwrapIfFallback(): ValueProvider { + return if (this is Fallback) { provider.unwrapIfFallback() } else { this } + } + + private class Fallback>( + val provider: ValueProvider, + val fallback: ValueProvider, + ): ValueProvider { + + override fun enrich(description: D, type: T, scope: Scope) { + provider.enrich(description, type, scope) + // Enriching scope by fallback value provider in this point is not quite right, + // but it doesn't look as a problem right now. + fallback.enrich(description, type, scope) + } + + override fun generate(description: D, type: T): Sequence> { + val default = if (provider.accept(type)) provider.generate(description, type) else emptySequence() + return if (default.iterator().hasNext()) { + default + } else if (fallback.accept(type)) { + fallback.generate(description, type) + } else { + emptySequence() + } + } + + override fun map(transform: (ValueProvider) -> ValueProvider): ValueProvider = + transform(Fallback(provider.map(transform), fallback.map(transform))) + } + + /** + * Wrapper class that delegates implementation to the [providers]. + */ + private class Combined>(providers: List>): ValueProvider { + val providers: List> + + init { + // Flattening to avoid Combined inside Combined (for correct work of except, map, etc.) + this.providers = providers.flatMap { + if (it is Combined) + it.providers + else + listOf(it) + } + } + + override fun enrich(description: D, type: T, scope: Scope) { + providers.forEach { it.enrich(description, type, scope) } + } + + override fun accept(type: T): Boolean { + return providers.any { it.accept(type) } + } + + override fun generate(description: D, type: T): Sequence> = sequence { + providers.asSequence().filter { it.accept(type) }.forEach { provider -> + provider.generate(description, type).forEach { + yield(it) + } + } + } + + override fun map(transform: (ValueProvider) -> ValueProvider): ValueProvider = + transform(Combined(providers.map { it.map(transform) })) + } + + companion object { + fun > of(valueProviders: List>): ValueProvider { + return Combined(valueProviders) + } + } +} + +/** + * Simple value provider for a concrete type. + * + * @param type that is used as a filter to call this provider + * @param generate yields values for the type + */ +class TypeProvider>( + val type: T, + val generate: suspend SequenceScope>.(description: D, type: T) -> Unit +) : ValueProvider { + override fun accept(type: T) = this.type == type + override fun generate(description: D, type: T) = sequence { + if (accept(type)) { + this.generate(description, type) + } + } +} diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt new file mode 100644 index 0000000000..877937cedb --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/Statistic.kt @@ -0,0 +1,16 @@ +package org.utbot.fuzzing + +import org.utbot.fuzzing.utils.MissedSeed +import kotlin.random.Random + +/** + * User class that holds data about current fuzzing running. + */ +interface Statistic { + val startTime: Long + val totalRuns: Long + val elapsedTime: Long + val missedTypes: MissedSeed + val random: Random + val configuration: Configuration +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt new file mode 100644 index 0000000000..4969ccf7f4 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/AbcFuzzing.kt @@ -0,0 +1,63 @@ +package org.utbot.fuzzing.demo + +import org.utbot.fuzzing.* + +/** + * This example shows the minimal required implementation to start fuzzing any function. + * + * Assume, there's a function that returns some positive integer if one string is a substring. + * The value of this integer is maximum number of same characters, therefore, bigger is better. + * + * Lets fuzzing values for some given string to find out does the fuzzing can find the whole string or not. + */ +fun String.findMaxSubstring(s: String) : Int { + if (s.isEmpty()) return -1 + for (i in s.indices) { + if (s[i] != this[i]) return i - 1 + } + return s.length +} + +// the given string +private const val searchString = + "fun String.findMaxSubstring(s: String) : Int {\n" + + " if (s.isEmpty()) return -1\n" + + " for (i in s.indices) {\n" + + " if (s[i] != this[i]) return i - 1\n" + + " }\n" + + " return s.length\n" + + "}" + +suspend fun main() { + // Define fuzzing description to start searching. + object : Fuzzing, BaseFeedback> { + /** + * Generate method returns several samples or seeds which are used as a base for fuzzing. + * + * In this particular case only 1 value is provided which is an empty string. Also, a mutation + * is defined for any string value. This mutation adds a random character from ASCII table. + */ + override fun generate(description: Description, type: Unit) = sequenceOf>( + Seed.Simple("") { s, r -> s + Char(r.nextInt(1, 256)) } + ) + + /** + * After the fuzzing generates a new value it calls this method to execute target program and waits for feedback. + * + * This implementation just calls the target function and returns a result. After it returns an empty feedback. + * If some returned value equals to the length of the source string then feedback returns 'stop' signal. + */ + override suspend fun handle(description: Description, values: List): BaseFeedback { + check(values.size == 1) { + "Only one value must be generated because of `description.parameters.size = ${description.parameters.size}`" + } + val input = values.first() + val result = searchString.findMaxSubstring(input) + println("findMaxSubstring(\"$input\") = $result") + return BaseFeedback( + result = result, + control = if (result == searchString.length) Control.STOP else Control.CONTINUE + ) + } + }.fuzz(Description(listOf(Unit))) +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt new file mode 100644 index 0000000000..99be17e156 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/ForkFuzzing.kt @@ -0,0 +1,57 @@ +package org.utbot.fuzzing.demo + +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.utbot.fuzzing.* +import java.util.concurrent.atomic.AtomicLong + +private enum class Type { + ANY, CONCRETE, MORE_CONCRETE +} + +fun main(): Unit = runBlocking { + launch { + object : Fuzzing, Feedback> { + + private val runs = mutableMapOf() + + override fun generate(description: Description, type: Type): Sequence> { + return sequenceOf(Seed.Simple(type.name)) + } + + override suspend fun handle(description: Description, values: List): Feedback { + description.parameters.forEach { + runs[it]!!.incrementAndGet() + } + println(values) + return emptyFeedback() + } + + override suspend fun afterIteration( + description: Description, + stats: Statistic, + ) { + if (stats.totalRuns % 10 == 0L && description.parameters.size == 1) { + val newTypes = when (description.parameters[0]) { + Type.ANY -> listOf(Type.CONCRETE) + Type.CONCRETE -> listOf(Type.MORE_CONCRETE) + Type.MORE_CONCRETE -> listOf() + } + if (newTypes.isNotEmpty()) { + val d = Description(newTypes) + fork(d, stats) + // Description can be used as a transfer object, + // that collects information about the current running. + println("Fork ended: ${d.parameters}") + } + } + } + + override suspend fun isCancelled(description: Description, stats: Statistic): Boolean { + println("info: ${description.parameters} runs ${stats.totalRuns}") + return description.parameters.all { runs.computeIfAbsent(it) { AtomicLong(0) }.get() >= 10 } + } + }.fuzz(Description(listOf(Type.ANY))) + } +// .cancel() +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt new file mode 100644 index 0000000000..844336aad1 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/HttpRequest.kt @@ -0,0 +1,60 @@ +package org.utbot.fuzzing.demo + +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.utbot.fuzzing.* +import java.util.concurrent.TimeUnit + +fun main() = runBlocking { + withTimeout(TimeUnit.SECONDS.toMillis(10)) { + object : Fuzzing, Feedback> { + override fun generate(description: Description, type: String) = sequence> { + when (type) { + "url" -> yield(Seed.Recursive( + construct = Routine.Create( + listOf("protocol", "host", "port", "path") + ) { + val (protocol, host, port, path) = it + "$protocol://$host${if (port.isNotBlank()) ":$port" else ""}/$path" + }, + empty = Routine.Empty { error("error") } + )) + "protocol" -> { + yield(Seed.Simple("http")) + yield(Seed.Simple("https")) + yield(Seed.Simple("ftp")) + } + "host" -> { + yield(Seed.Simple("localhost")) + yield(Seed.Simple("127.0.0.1")) + } + "port" -> { + yield(Seed.Simple("8080")) + yield(Seed.Simple("")) + } + "path" -> yield(Seed.Recursive( + construct = Routine.Create(listOf("page", "id")) { + it.joinToString("/") + }, + empty = Routine.Empty { error("error") } + )) + "page" -> { + yield(Seed.Simple("owners")) + yield(Seed.Simple("users")) + yield(Seed.Simple("admins")) + } + "id" -> (0..1000).forEach { yield(Seed.Simple(it.toString())) } + else -> error("unknown type '$type'") + } + } + + override suspend fun handle( + description: Description, + values: List + ): Feedback { + println(values[0]) + return BaseFeedback(values[0], Control.PASS) + } + }.fuzz(Description(listOf("url"))) + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt new file mode 100644 index 0000000000..0d3d490dd7 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JavaFuzzing.kt @@ -0,0 +1,94 @@ +package org.utbot.fuzzing.demo + +import org.utbot.fuzzing.* +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.Bool +import org.utbot.fuzzing.seeds.Signed +import org.utbot.fuzzing.seeds.StringValue + +/** + * This example implements some basics for Java fuzzing that supports only a few types: + * integers, strings, primitive arrays and user class [A]. + */ +object JavaFuzzing : Fuzzing, Any?, Description>, Feedback, Any?>> { + override fun generate(description: Description>, type: Class<*>) = sequence, Any?>> { + if (type == Boolean::class.javaPrimitiveType) { + yield(Seed.Known(Bool.TRUE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) + yield(Seed.Known(Bool.FALSE.invoke()) { obj: BitVectorValue -> obj.toBoolean() }) + } + if (type == String::class.java) { + yield(Seed.Known(StringValue(""), StringValue::value)) + yield(Seed.Known(StringValue("hello"), StringValue::value)) + } + for (signed in Signed.values()) { + if (type == Char::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(8)) { obj: BitVectorValue -> obj.toCharacter() }) + } + if (type == Byte::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(8)) { obj: BitVectorValue -> obj.toByte() }) + } + if (type == Short::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(16)) { obj: BitVectorValue -> obj.toShort() }) + } + if (type == Int::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(32)) { obj: BitVectorValue -> obj.toInt() }) + } + if (type == Long::class.javaPrimitiveType) { + yield(Seed.Known(signed.invoke(64)) { obj: BitVectorValue -> obj.toLong() }) + } + } + if (type == A::class.java) { + for (constructor in A::class.java.constructors) { + yield( + Seed.Recursive( + construct = Routine.Create(constructor.parameters.map { it.type }) { objects -> + constructor.newInstance(*objects.toTypedArray()) + }, + modify = type.fields.asSequence().map { field -> + Routine.Call(listOf(field.type)) { self: Any?, objects: List<*> -> + try { + field[self] = objects[0] + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } + } + }, + empty = Routine.Empty { null } + ) + ) + } + } + if (type.isArray) { + yield( + Seed.Collection( + construct = Routine.Collection { length: Int -> + java.lang.reflect.Array.newInstance(type.componentType, length) + }, + modify = Routine.ForEach(listOf(type.componentType)) { self: Any?, index: Int, objects: List<*> -> + java.lang.reflect.Array.set(self, index, objects[0]) + } + )) + } + } + + override suspend fun handle(description: Description>, values: List): Feedback, Any?> { + println(values.joinToString { + when (it) { + is BooleanArray -> it.contentToString() + is CharArray -> it.contentToString() + is ByteArray -> it.contentToString() + is ShortArray -> it.contentToString() + is IntArray -> it.contentToString() + is LongArray -> it.contentToString() + else -> it.toString() + } + }) + return emptyFeedback() + } +} + +suspend fun main() { + JavaFuzzing.fuzz( + Description(listOf(Int::class.javaPrimitiveType!!, CharArray::class.java, A::class.java)), + ) +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt new file mode 100644 index 0000000000..80863e4053 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/demo/JsonFuzzing.kt @@ -0,0 +1,95 @@ +package org.utbot.fuzzing.demo + +import org.utbot.fuzzing.* +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.Signed +import org.utbot.fuzzing.seeds.StringValue +import java.util.concurrent.atomic.AtomicLong +import kotlin.random.Random + +private enum class CustomType { + INT, STR, OBJ, LST +} + +private class JsonBuilder( + var before: String, + val children: MutableList = mutableListOf(), + var after: String = "", +) { + override fun toString(): String { + return buildString { + append(before) + append(children.joinToString(", ")) + append(after) + } + } +} + +/** + * This example shows how json based output can be built by fuzzer for some types. + * + * Also, some utility class such as [BaseFuzzing] and [TypeProvider] are used to start fuzzing without class inheritance. + */ +suspend fun main() { + var count = 0 + val set = mutableMapOf() + BaseFuzzing, Feedback>( + TypeProvider(CustomType.INT) { _, _ -> + for (b in Signed.values()) { + yield(Seed.Known(BitVectorValue(3, b)) { bvv -> + JsonBuilder((0 until bvv.size).joinToString(separator = "", prefix = "\"0b", postfix = "\"") { i -> + if (bvv[i]) "1" else "0" + }) + }) + } + }, + TypeProvider(CustomType.STR) { _, _ -> + listOf("Ted", "Mike", "John").forEach { n -> + yield(Seed.Known(StringValue(n)) { sv -> JsonBuilder("\"${sv.value}\"") }) + } + }, + TypeProvider(CustomType.OBJ) { _, _ -> + yield(Seed.Recursive( + construct = Routine.Create(listOf(CustomType.OBJ)) { + JsonBuilder(before = "{", after = "}") + }, + modify = sequence { + yield(Routine.Call(listOf(CustomType.INT)) { self, values -> + self.children += JsonBuilder("\"value\": ${values.joinToString(", ")}") + }) + yield(Routine.Call(listOf(CustomType.STR)) { self, values -> + self.children += JsonBuilder("\"name\": ${values.joinToString(", ")}") + }) + yield(Routine.Call(listOf(CustomType.OBJ)) { self, values -> + self.children += JsonBuilder("\"child\": ${values.joinToString(", ")}") + }) + }, + empty = Routine.Empty { + JsonBuilder("null") + } + )) + }, + TypeProvider(CustomType.LST) { _, _ -> + for (type in listOf(CustomType.INT, CustomType.STR)) { + yield(Seed.Collection( + construct = Routine.Collection { JsonBuilder(before = "[", after = "]") }, + modify = Routine.ForEach(listOf(type)) { self, _, values -> + self.children += JsonBuilder(values.joinToString(", ")) + } + )) + } + }, + ) { _, values -> + val result = values.toString() + println(result) + set.computeIfAbsent(result) { AtomicLong() }.incrementAndGet() + if (++count < 10000) emptyFeedback() else { + println("Unique ratio:" + set.size / count.toDouble()) + error("Forced from the example") + } + }.fuzz( + Description(listOf(CustomType.LST, CustomType.OBJ)), + Random(0), + Configuration(recursionTreeDepth = 2, collectionIterations = 2) + ) +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/BitVectorValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/BitVectorValue.kt new file mode 100644 index 0000000000..895c677a95 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/BitVectorValue.kt @@ -0,0 +1,273 @@ +package org.utbot.fuzzing.seeds + +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.Endian +import java.math.BigInteger +import java.util.BitSet + +class BitVectorValue : KnownValue { + + /** + * Vector of value bits. + * + * Here, the base is 2 and the exponent for each member is equal to index of this member. + * Therefore, the values is stored using LE system. + */ + private val vector: BitSet + val size: Int + override val lastMutation: Mutation? + override val mutatedFrom: BitVectorValue? + + constructor(bits: Int, bound: Bound) { + vector = BitSet(bits).also { + for (i in 0 until bits) { + it[i] = bound.initializer(i, bits) + } + } + size = bits + lastMutation = null + mutatedFrom = null + } + + constructor(other: BitVectorValue, mutation: Mutation? = null) { + vector = other.vector.clone() as BitSet + size = other.size + lastMutation = mutation + mutatedFrom = other + } + + private constructor(size: Int, value: BitSet) : this(size, { i, _ -> value[i] }) + + operator fun get(index: Int): Boolean = vector[index] + + operator fun set(index: Int, value: Boolean) { + vector[index] = value + } + + /** + * Increase value by 1. + * + * @return true if integer overflow is occurred in sign values + */ + fun inc(): Boolean { + var shift = 0 + var carry = true + while (carry and (shift < size)) { + if (!vector[shift]) { + carry = false + } + vector[shift] = !vector[shift] + shift++ + } + return !carry && shift == size + } + + /** + * Decrease value by 1. + * + * @return true if integer underflow is occurred + */ + fun dec(): Boolean { + var shift = 0 + var carry = true + while (carry and (shift < size)) { + if (vector[shift]) { + carry = false + } + vector[shift] = !vector[shift] + shift++ + } + return !carry && shift == size + } + + override fun mutations() = listOf>( + BitVectorMutations.SlightDifferent, + BitVectorMutations.DifferentWithSameSign, + BitVectorMutations.ChangeSign + ) + + override fun equals(other: Any?): Boolean { + if (other !is BitVectorValue) return false + if (size != other.size) return false + for (i in 0 until size) { + if (vector[i] != other.vector[i]) return false + } + return true + } + + override fun hashCode(): Int { + return vector.hashCode() + } + + @Suppress("MemberVisibilityCanBePrivate") + fun toString(radix: Int, isUnsigned: Boolean = false): String { + return toBigInteger(isUnsigned).toString(radix) + } + + override fun toString() = toString(10) + + @Suppress("unused") + internal fun toBinaryString(endian: Endian) = buildString { + for (i in endian.range(0, size - 1)) { + append(if (this@BitVectorValue[i]) '1' else '0') + } + } + + private fun toLong(bits: Int, shift: Int = 0): Long { + assert(bits <= 64) { "Cannot convert to long vector with more than 64 bits, but $bits is requested" } + var result = 0L + for (i in shift until minOf(bits + shift, size)) { + result = result or ((if (vector[i]) 1L else 0L) shl (i - shift)) + } + return result + } + + private fun toBigInteger(isUnsigned: Boolean): BigInteger { + val size = if (isUnsigned) size + 1 else size + val array = ByteArray(size / 8 + if (size % 8 != 0) 1 else 0) { index -> + toLong(bits = 8, shift = index * 8).toByte() + } + array.reverse() + return BigInteger(array) + } + + fun toBigInteger() = toBigInteger(false) + + fun toBoolean() = vector[0] + + fun toByte() = toLong(8).toByte() + + fun toUByte() = toLong(8).toUByte() + + fun toShort() = toLong(16).toShort() + + fun toUShort() = toLong(16).toUShort() + + fun toInt() = toLong(32).toInt() + + fun toUInt() = toLong(32).toUInt() + + fun toLong() = toLong(64) + + fun toULong() = toLong(64).toULong() + + fun toCharacter() = Char(toUShort()) + + companion object { + fun fromValue(value: Any): BitVectorValue { + return when (value) { + is Char -> fromChar(value) + is Boolean -> fromBoolean(value) + is Byte -> fromByte(value) + is Short -> fromShort(value) + is Int -> fromInt(value) + is Long -> fromLong(value) + is BigInteger -> fromBigInteger(value) + else -> error("unknown type of value $value (${value::class})") + } + } + + fun fromBoolean(value: Boolean): BitVectorValue { + return BitVectorValue(1, if (value) Bool.TRUE else Bool.FALSE) + } + + fun fromByte(value: Byte): BitVectorValue { + return fromLong(8, value.toLong()) + } + + fun fromShort(value: Short): BitVectorValue { + return fromLong(16, value.toLong()) + } + + fun fromChar(value: Char): BitVectorValue { + return fromLong(16, value.code.toLong()) + } + + fun fromInt(value: Int): BitVectorValue { + return fromLong(32, value.toLong()) + } + + fun fromLong(value: Long): BitVectorValue { + return fromLong(64, value) + } + + private fun fromLong(size: Int, value: Long): BitVectorValue { + val vector = BitSet(size) + for (i in 0 until size) { + vector[i] = value and (1L shl i) != 0L + } + return BitVectorValue(size, vector) + } + + fun fromBigInteger(value: BigInteger): BitVectorValue { + val size = 128 + val bits = value.bitCount() + assert(bits <= size) { "This value $value is too big. Max value is 2^$bits." } + val vector = BitSet(size) + for (i in 0 until size) { + vector[i] = value.testBit(i) + } + return BitVectorValue(size, vector) + } + } +} + +fun interface Bound { + fun initializer(index: Int, size: Int): Boolean +} + +class DefaultBound private constructor(private val value: Long) : Bound { + + override fun initializer(index: Int, size: Int): Boolean { + return value and (1L shl index) != 0L + } + + @Suppress("unused") + companion object { + fun ofByte(value: Byte) = DefaultBound(value.toLong()) + + fun ofUByte(value: UByte) = DefaultBound(value.toLong()) + + fun ofShort(value: Short) = DefaultBound(value.toLong()) + + fun ofUShort(value: UShort) = DefaultBound(value.toLong()) + + fun ofInt(value: Int) = DefaultBound(value.toLong()) + + fun ofUInt(value: UInt) = DefaultBound(value.toLong()) + + fun ofLong(value: Long) = DefaultBound(value) + + fun ofULong(value: ULong) = DefaultBound(value.toLong()) + } +} + +enum class Signed : Bound { + ZERO { override fun initializer(index: Int, size: Int) = false }, + MIN { override fun initializer(index: Int, size: Int) = index == size - 1 }, + NEGATIVE { override fun initializer(index: Int, size: Int) = true }, + POSITIVE { override fun initializer(index: Int, size: Int) = index == 0 }, + MAX { override fun initializer(index: Int, size: Int) = index < size - 1 }, + ; + + operator fun invoke(size: Int) = BitVectorValue(size, this) + + fun test(value: BitVectorValue) = (0..value.size).all { value[it] == initializer(it, value.size) } +} + +enum class Unsigned : Bound { + ZERO { override fun initializer(index: Int, size: Int) = false }, + POSITIVE { override fun initializer(index: Int, size: Int) = index == 0 }, + MAX { override fun initializer(index: Int, size: Int) = true }, + ; + + operator fun invoke(size: Int) = BitVectorValue(size, this) +} + +enum class Bool : Bound { + FALSE { override fun initializer(index: Int, size: Int) = false }, + TRUE { override fun initializer(index: Int, size: Int) = true }, + ; + + operator fun invoke() = BitVectorValue(1, this) +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/IEEE754Value.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/IEEE754Value.kt new file mode 100644 index 0000000000..0e44449925 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/IEEE754Value.kt @@ -0,0 +1,278 @@ +package org.utbot.fuzzing.seeds + +import org.utbot.fuzzing.* +import kotlin.math.pow + +//val FLOAT_ZERO = DefaultFloatBound.ZERO(23, 8) +//val FLOAT_NAN = DefaultFloatBound.NAN(23, 8) +//val FLOAT_POSITIVE_INFINITY = DefaultFloatBound.POSITIVE_INFINITY(23, 8) +//val FLOAT_NEGATIVE_INFINITY = DefaultFloatBound.NEGATIVE_INFINITY(23, 8) +//val DOUBLE_ZERO = DefaultFloatBound.ZERO(52, 11) +//val DOUBLE_NAN = DefaultFloatBound.NAN(52, 11) +//val DOUBLE_POSITIVE_INFINITY = DefaultFloatBound.POSITIVE_INFINITY(52, 11) +//val DOUBLE_NEGATIVE_INFINITY = DefaultFloatBound.NEGATIVE_INFINITY(52, 11) + +class IEEE754Value : KnownValue { + + val isPositive: Boolean + get() = !vector[0] + + val bias: Int + get() = (2 shl (exponentSize - 2)) - 1 + + val exponent: Long + get() { + check(exponentSize <= 64) { "Exponent cannot be represented as long" } + var result = 0L + for (i in 0 until exponentSize) { + if (vector[exponentSize - i]) { + result += 1L shl i + } + } + return result - bias + } + + val mantissaSize: Int + val exponentSize: Int + override val lastMutation: Mutation? + override val mutatedFrom: IEEE754Value? + + private val vector: BitVectorValue + + constructor(mantissaSize: Int, exponentSize: Int, bound: FloatBound ) { + this.mantissaSize = mantissaSize + this.exponentSize = exponentSize + this.vector = BitVectorValue(1 + mantissaSize + exponentSize) { index, size -> + check(1 + exponentSize + mantissaSize == size) { "size exceeds" } + when { + index >= 1 + exponentSize + mantissaSize -> error("out of range") + index >= 1 + exponentSize -> bound.mantissa(index - 1 - exponentSize, mantissaSize) + index >= 1 -> bound.exponent(index - 1, exponentSize) + index == 0 -> bound.sign() + else -> error("out of range") + } + } + lastMutation = null + mutatedFrom = null + } + + constructor(value: IEEE754Value, mutation: Mutation? = null) { + this.vector = BitVectorValue(value.vector) + this.mantissaSize = value.mantissaSize + this.exponentSize = value.exponentSize + this.lastMutation = mutation + this.mutatedFrom = value + } + + fun getRaw(index: Int) = vector[index] + + fun setRaw(index: Int, value: Boolean) { + vector[index] = value + } + + fun toFloat(): Float { + DefaultFloatBound.values().forEach { + if (it.test(this)) when (it) { + DefaultFloatBound.ZERO -> return 0.0f + DefaultFloatBound.NAN -> return Float.NaN + DefaultFloatBound.POSITIVE_INFINITY -> return Float.POSITIVE_INFINITY + DefaultFloatBound.NEGATIVE_INFINITY -> return Float.NEGATIVE_INFINITY + else -> {} + } + } + var result = 0.0f + val e = exponent.toFloat() + result += 2.0f.pow(e) + for (i in 0 until mantissaSize) { + if (vector[1 + exponentSize + i]) { + result += 2.0f.pow(e - 1 - i) + } + } + return result * if (isPositive) 1.0f else -1.0f + } + + fun toDouble(): Double { + DefaultFloatBound.values().forEach { + if (it.test(this)) when (it) { + DefaultFloatBound.ZERO -> return 0.0 + DefaultFloatBound.NAN -> return Double.NaN + DefaultFloatBound.POSITIVE_INFINITY -> return Double.POSITIVE_INFINITY + DefaultFloatBound.NEGATIVE_INFINITY -> return Double.NEGATIVE_INFINITY + else -> {} + } + } + var result = 0.0 + val e = exponent.toDouble() + result += 2.0.pow(e) + for (i in 0 until mantissaSize) { + if (vector[1 + exponentSize + i]) { + result += 2.0.pow(e - 1 - i) + } + } + return result * if (isPositive) 1.0 else -1.0 + } + + fun is32Float(): Boolean { + return vector.size == 32 && mantissaSize == 23 && exponentSize == 8 + } + + fun is64Float(): Boolean { + return vector.size == 64 && mantissaSize == 52 && exponentSize == 11 + } + + override fun toString() = buildString { + for (i in 0 until vector.size) { + if (i == 1 || i == 1 + exponentSize) append(" ") + append(if (getRaw(i)) '1' else '0') + } + } + + override fun mutations() = listOf>( + IEEE754Mutations.ChangeSign, + IEEE754Mutations.Mantissa, + IEEE754Mutations.Exponent, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IEEE754Value + + if (mantissaSize != other.mantissaSize) return false + if (exponentSize != other.exponentSize) return false + if (vector != other.vector) return false + + return true + } + + override fun hashCode(): Int { + var result = mantissaSize + result = 31 * result + exponentSize + result = 31 * result + vector.hashCode() + return result + } + + companion object { + fun fromValue(value: Any): IEEE754Value { + return when (value) { + is Float -> fromFloat(value) + is Double -> fromDouble(value) + else -> error("unknown type of value $value (${value::class})") + } + } + + fun fromFloat(value: Float): IEEE754Value { + return IEEE754Value(23, 8, object : FloatBound { + + val rawInt = value.toRawBits() + + override fun sign(): Boolean { + return rawInt and (1 shl 31) != 0 + } + + override fun mantissa(index: Int, size: Int): Boolean { + return rawInt and (1 shl (size - 1 - index)) != 0 + } + + override fun exponent(index: Int, size: Int): Boolean { + return rawInt and (1 shl (30 - index)) != 0 + } + + }) + } + + fun fromDouble(value: Double): IEEE754Value { + return IEEE754Value(52, 11, object : FloatBound { + + val rawLong = value.toRawBits() + + override fun sign(): Boolean { + return rawLong and (1L shl 63) != 0L + } + + override fun mantissa(index: Int, size: Int): Boolean { + return rawLong and (1L shl (size - 1 - index)) != 0L + } + + override fun exponent(index: Int, size: Int): Boolean { + return rawLong and (1L shl (62 - index)) != 0L + } + + }) + } + } +} + +interface FloatBound { + fun sign(): Boolean + fun mantissa(index: Int, size: Int): Boolean + fun exponent(index: Int, size: Int): Boolean +} + +private class CopyBound(val vector: IEEE754Value) : FloatBound { + override fun sign(): Boolean = vector.getRaw(0) + override fun exponent(index: Int, size: Int): Boolean = vector.getRaw(1 + index) + override fun mantissa(index: Int, size: Int): Boolean = vector.getRaw(1 + vector.exponentSize + index) +} + +enum class DefaultFloatBound : FloatBound { + ZERO { + override fun sign() = false + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = false + }, + NAN { + override fun sign() = false + override fun mantissa(index: Int, size: Int) = index == size - 1 + override fun exponent(index: Int, size: Int) = true + }, + POSITIVE { + override fun sign() = false + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = index != 0 + }, + NEGATIVE { + override fun sign() = true + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = index != 0 + }, + POSITIVE_INFINITY { + override fun sign() = false + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = true + }, + NEGATIVE_INFINITY { + override fun sign() = true + override fun mantissa(index: Int, size: Int) = false + override fun exponent(index: Int, size: Int) = true + }, + ; + + operator fun invoke(mantissaSize: Int, exponentSize: Int): IEEE754Value { + return IEEE754Value(mantissaSize, exponentSize, this) + } + + fun test(value: IEEE754Value): Boolean { + for (i in 0 until 1 + value.exponentSize + value.mantissaSize) { + @Suppress("KotlinConstantConditions") + val res = when { + i >= 1 + value.exponentSize -> mantissa(i - 1 - value.exponentSize, value.mantissaSize) + i >= 1 -> exponent(i - 1, value.exponentSize) + i == 0 -> sign() + else -> error("bad index $i") + } + if (value.getRaw(i) != res) return false + } + return true + } +} + +//fun main() { +// println(IEEE754Value.fromDouble(8.75).toFloat()) +// println(IEEE754Value.fromFloat(28.7f).toDouble()) +// println(28.7f.toDouble()) +// DefaultFloatBound.values().forEach { +// println(IEEE754Value(3, 3, it).toFloat()) +// } +//} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/KnownValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/KnownValue.kt new file mode 100644 index 0000000000..19c5d45aab --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/KnownValue.kt @@ -0,0 +1,15 @@ +package org.utbot.fuzzing.seeds + +import org.utbot.fuzzing.Mutation + +interface KnownValue> { + val lastMutation: Mutation? + get() = null + + val mutatedFrom: T? + get() = null + + fun mutations(): List> { + return emptyList() + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/RegexValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/RegexValue.kt new file mode 100644 index 0000000000..eb95e34a9c --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/RegexValue.kt @@ -0,0 +1,35 @@ +package org.utbot.fuzzing.seeds + +import org.cornutum.regexpgen.js.Provider +import org.cornutum.regexpgen.random.RandomBoundsGen +import org.utbot.fuzzing.Mutation +import kotlin.random.Random +import kotlin.random.asJavaRandom + +class RegexValue( + val pattern: String, + val random: Random, + val maxLength: Int = listOf(16, 256, 2048).random(random) +) : StringValue( + valueProvider = { + val matchingExact = Provider.forEcmaScript().matchingExact(pattern) + matchingExact.generate(RandomBoundsGen(random.asJavaRandom()), 1, maxLength) + } +) { + + override fun mutations(): List> { + return super.mutations() + Mutation { source, random, _ -> + RegexValue(source.pattern, random) + } + } +} + +fun String.isSupportedPattern(): Boolean { + if (isEmpty()) return false + return try { + Provider.forEcmaScript().matchingExact(this) + true + } catch (_: Throwable) { + false + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt new file mode 100644 index 0000000000..cd054754b8 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/StringValue.kt @@ -0,0 +1,27 @@ +package org.utbot.fuzzing.seeds + +import org.utbot.fuzzing.Mutation +import org.utbot.fuzzing.StringMutations + +open class StringValue( + val valueProvider: () -> String, + override val lastMutation: Mutation? = null, + override val mutatedFrom: StringValue? = null, +) : KnownValue { + + constructor( + value: String, + lastMutation: Mutation? = null, + mutatedFrom: StringValue? = null + ) : this(valueProvider = { value }, lastMutation, mutatedFrom) + + val value by lazy { valueProvider() } + + override fun mutations(): List> { + return listOf( + StringMutations.AddCharacter, + StringMutations.RemoveCharacter, + StringMutations.ShuffleCharacters, + ) + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/CartesianProduct.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/CartesianProduct.kt new file mode 100644 index 0000000000..399f514c89 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/CartesianProduct.kt @@ -0,0 +1,101 @@ +package org.utbot.fuzzing.utils + +import kotlin.jvm.Throws +import kotlin.random.Random + +/** + * Creates iterable for all values of cartesian product of `lists`. + */ +class CartesianProduct( + private val lists: List>, + private val random: Random? = null +): Iterable> { + + /** + * Estimated number of all combinations. + */ + val estimatedSize: Long + get() = Combinations(*lists.map { it.size }.toIntArray()).size + + @Throws(TooManyCombinationsException::class) + fun asSequence(): Sequence> { + val combinations = Combinations(*lists.map { it.size }.toIntArray()) + val sequence = if (random != null) { + sequence { + forEachChunk(Int.MAX_VALUE, combinations.size) { startIndex, combinationSize, _ -> + val permutation = PseudoShuffledIntProgression(combinationSize, random) + val temp = IntArray(size = lists.size) + for (it in 0 until combinationSize) { + yield(combinations[permutation[it] + startIndex, temp]) + } + } + } + } else { + combinations.asSequence() + } + return sequence.map { combination -> + combination.mapIndexedTo(ArrayList(combination.size)) { index, value -> lists[index][value] } + } + } + + override fun iterator(): Iterator> = asSequence().iterator() + + companion object { + /** + * Consumer for processing blocks of input larger block. + * + * If source example is sized to 12 and every block is sized to 5 then consumer should be called 3 times with these values: + * + * 1. start = 0, size = 5, remain = 7 + * 2. start = 5, size = 5, remain = 2 + * 3. start = 10, size = 2, remain = 0 + * + * The sum of start, size and remain should be equal to source block size. + */ + internal inline fun forEachChunk( + chunkSize: Int, + totalSize: Long, + block: (start: Long, size: Int, remain: Long) -> Unit + ) { + val iterationsCount = totalSize / chunkSize + if (totalSize % chunkSize == 0L) 0 else 1 + (0L until iterationsCount).forEach { iteration -> + val start = iteration * chunkSize + val size = minOf(chunkSize.toLong(), totalSize - start).toInt() + val remain = totalSize - size - start + block(start, size, remain) + } + } + } +} + +inline fun List>.cartesian(block: (List) -> Unit) { + cartesian().forEach(block) +} + +fun List>.cartesian(): Sequence> = sequence { + cartesian(this@cartesian, 0, IntArray(size)) +} + +private suspend fun SequenceScope>.cartesian(lists: List>, iteration: Int, array: IntArray) { + if (iteration == lists.size) { + yield(array.mapIndexed { l, v -> lists[l][v] }) + } else { + check(iteration < lists.size) + for (j in lists[iteration].indices) { + array[iteration] = j + cartesian(lists, iteration + 1, array) + } + } +} + +//private suspend fun SequenceScope>.cartesian(lists: List>, head: List) { +// if (head.size == lists.size) { +// yield(head) +// } else { +// check(head.size < lists.size) +// lists[head.size].forEach { +// val copy = head + it +// cartesian(lists, copy) +// } +// } +//} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Combinations.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Combinations.kt similarity index 99% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Combinations.kt rename to utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Combinations.kt index c724022ae6..33908563a8 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Combinations.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Combinations.kt @@ -1,4 +1,4 @@ -package org.utbot.fuzzer +package org.utbot.fuzzing.utils /** * Enumerates all possible combinations for a given list of maximum numbers of elements for every position. diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Functions.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Functions.kt new file mode 100644 index 0000000000..c2f9add11b --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Functions.kt @@ -0,0 +1,96 @@ +@file:Suppress("unused") + +package org.utbot.fuzzing.utils + +import org.utbot.fuzzing.seeds.BitVectorValue + +internal fun T.toBinaryString( + size: Int, + endian: Endian = Endian.BE, + separator: String = " ", + format: (index: Int) -> Boolean = BinaryFormat.BYTE, + bit: (v: T, i: Int) -> Boolean +): String = buildString { + (endian.range(0, size - 1)).forEachIndexed { index, i -> + val b = if (bit(this@toBinaryString, i)) 1 else 0 + if (format(index)) append(separator) + append(b) + } + appendLine() +} + +internal fun UByte.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(8, endian, separator, format) { v, i -> v.toInt() and (1 shl i) != 0 } +} + +internal fun Byte.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(8, endian, separator, format) { v, i -> v.toInt() and (1 shl i) != 0 } +} + +internal fun UShort.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(16, endian, separator, format) { v, i -> v.toInt() and (1 shl i) != 0 } +} + +internal fun Short.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(16, endian, separator, format) { v, i -> v.toInt() and (1 shl i) != 0 } +} + +internal fun UInt.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(32, endian, separator, format) { v, i -> v and (1u shl i) != 0u } +} + +internal fun Int.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(32, endian, separator, format) { v, i -> v and (1 shl i) != 0 } +} + +internal fun ULong.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(64, endian, separator, format) { v, i -> v and (1uL shl i) != 0uL } +} + +internal fun Long.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = BinaryFormat.BYTE): String { + return toBinaryString(64, endian, separator, format) { v, i -> v and (1L shl i) != 0L } +} + +internal fun Float.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = { + when (endian) { + Endian.BE -> it == 1 || it == 9 + Endian.LE -> it == 31 || it == 23 + } +}): String { + return toRawBits().toBinaryString(endian, separator, format) +} + +internal fun Double.toBinaryString(endian: Endian = Endian.BE, separator: String = " ", format: (index: Int) -> Boolean = { + when (endian) { + Endian.BE -> it == 1 || it == 12 + Endian.LE -> it == 63 || it == 52 + } +}): String { + return toRawBits().toBinaryString(endian, separator, format) +} + +internal enum class Endian { + BE { override fun range(fromInclusive: Int, toInclusive: Int) = (toInclusive downTo fromInclusive) }, + LE { override fun range(fromInclusive: Int, toInclusive: Int) = (fromInclusive .. toInclusive) }; + abstract fun range(fromInclusive: Int, toInclusive: Int): IntProgression +} + +internal enum class BinaryFormat : (Int) -> Boolean { + HALF { override fun invoke(index: Int) = index % 4 == 0 && index != 0 }, + BYTE { override fun invoke(index: Int) = index % 8 == 0 && index != 0 }, + DOUBLE { override fun invoke(index: Int) = index % 16 == 0 && index != 0 }, +} + +internal fun MutableList.transformIfNotEmpty(transform: MutableList.() -> List): List { + return if (isNotEmpty()) transform() else this +} + +// todo move to tests +//fun main() { +// val endian = Endian.BE +// println(255.toUByte().toBinaryString(endian)) +// println(2.toBinaryString(endian)) +// println(BitVectorValue.fromInt(2).toBinaryString(endian)) +// print(8.75f.toBinaryString(endian)) +// print(8.75.toBinaryString(endian)) +//} \ No newline at end of file diff --git a/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/MissedSeed.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/MissedSeed.kt new file mode 100644 index 0000000000..2618238522 --- /dev/null +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/MissedSeed.kt @@ -0,0 +1,27 @@ +package org.utbot.fuzzing.utils + +import org.utbot.fuzzing.Seed + +class MissedSeed : Iterable { + + private val values = hashMapOf>() + + operator fun set(value: T, seed: Seed) { + values[value] = seed + } + + operator fun get(value: T): Seed? { + return values[value] + } + + fun isEmpty(): Boolean { + return values.size == 0 + } + + fun isNotEmpty(): Boolean = !isEmpty() + + override fun iterator(): Iterator { + return values.keys.iterator() + } + +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PseudoShuffledIntProgression.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgression.kt similarity index 99% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PseudoShuffledIntProgression.kt rename to utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgression.kt index 4ed08dd7a4..5d6ebdbce1 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PseudoShuffledIntProgression.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgression.kt @@ -1,4 +1,4 @@ -package org.utbot.fuzzer +package org.utbot.fuzzing.utils import kotlin.math.sqrt import kotlin.random.Random diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/RandomExtensions.kt similarity index 90% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt rename to utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/RandomExtensions.kt index c665f0bbf6..74e62210bc 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/RandomExtensions.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/RandomExtensions.kt @@ -1,4 +1,4 @@ -package org.utbot.fuzzer +package org.utbot.fuzzing.utils import kotlin.random.Random @@ -27,6 +27,8 @@ fun Random.chooseOne(frequencies: DoubleArray): Int { * If a random value is less than [probability] returns true. */ fun Random.flipCoin(probability: Int): Boolean { + if (probability == 0) return false + if (probability == 100) return true check(probability in 0 .. 100) { "probability must in range [0, 100] but $probability is provided" } return nextInt(1, 101) <= probability } diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Trie.kt b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt similarity index 94% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Trie.kt rename to utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt index fb3c8d6345..7bc4190f64 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Trie.kt +++ b/utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/utils/Trie.kt @@ -1,4 +1,4 @@ -package org.utbot.fuzzer +package org.utbot.fuzzing.utils fun trieOf(vararg values: Iterable): Trie = IdentityTrie().apply { values.forEach(this::add) @@ -169,4 +169,16 @@ open class Trie( override var count: Int = 0, val children: MutableMap> = HashMap(), ) : Node + + private object EmptyNode : Node { + override val data: Any + get() = error("empty node has no data") + override val count: Int + get() = 0 + } + + companion object { + @Suppress("UNCHECKED_CAST") + fun emptyNode() = EmptyNode as Node + } } \ No newline at end of file diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt new file mode 100644 index 0000000000..260b824942 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/FuzzerSmokeTest.kt @@ -0,0 +1,311 @@ +package org.utbot.fuzzing + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.flow +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.junit.jupiter.api.assertThrows +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.Signed +import java.util.concurrent.TimeUnit +import kotlin.reflect.KClass + +class FuzzerSmokeTest { + + @Test + fun `fuzzing runs with empty parameters`() { + runBlocking { + var count = 0 + runFuzzing, BaseFeedback>( + { _, _ -> sequenceOf() }, + Description(emptyList()) + ) { _, _ -> + count += 1 + BaseFeedback(Unit, Control.STOP) + } + Assertions.assertEquals(1, count) + } + } + + @Test + fun `fuzzing throws an exception if no values generated for some type`() { + assertThrows { + runBlocking { + var count = 0 + runFuzzing, BaseFeedback>( + provider = { _, _ -> sequenceOf() }, + description = Description(listOf(Unit)), + configuration = Configuration( + generateEmptyCollectionsForMissedTypes = false + ) + ) { _, _ -> + count += 1 + BaseFeedback(Unit, Control.STOP) + } + Assertions.assertEquals(0, count) + } + } + } + + @Test + fun `fuzzing stops on Control$STOP signal after one execution`() { + runBlocking { + var count = 0 + runFuzzing( + { _, _ -> sequenceOf(Seed.Simple(Unit)) }, + Description(listOf(Unit)) + ) { _, _ -> + count += 1 + BaseFeedback(Unit, Control.STOP) + } + Assertions.assertEquals(1, count) + } + } + + @Test + fun `fuzzing stops on Control$STOP signal after 3 execution`() { + runBlocking { + var count = 3 + var executions = 0 + runFuzzing( + { _, _ -> sequenceOf(Seed.Simple(Unit)) }, + Description(listOf(Unit)) + ) { _, _ -> + executions++ + BaseFeedback(Unit, if (--count == 0) Control.STOP else Control.CONTINUE) + } + Assertions.assertEquals(0, count) + Assertions.assertEquals(3, executions) + } + } + + @Test + fun `fuzzing runs value generation once per type by default`() { + runBlocking { + var count = 10 + var generations = 0 + runFuzzing( + { _, _ -> + generations++ + sequenceOf(Seed.Simple(Unit)) + }, + Description(listOf(Unit)) + ) { _, _ -> + BaseFeedback(Unit, if (--count == 0) Control.STOP else Control.CONTINUE) + } + Assertions.assertEquals(0, count) + Assertions.assertEquals(1, generations) + } + } + + @Test + fun `fuzzer rethrow exception from execution block`() { + class SpecialException : Exception() + runBlocking { + assertThrows { + withTimeout(1000) { + runFuzzing( + { _, _ -> sequenceOf(Seed.Simple(Unit)) }, + Description(listOf(Unit)) + ) { _, _ -> + throw SpecialException() + } + } + } + } + } + + @Test + fun `fuzzer generates recursive data with correct depth`() { + data class Node(val left: Node?, val right: Node?) + + runBlocking { + val configuration = Configuration( + recursionTreeDepth = 10 + ) + var depth = 0 + var count = 0 + runFuzzing( + ValueProvider, Node?, Description>> { _, _ -> + sequenceOf(Seed.Recursive( + construct = Routine.Create(listOf(Node::class, Node::class)) { v -> + Node(v[0], v[1]) + }, + empty = Routine.Empty { null } + )) + }, + Description(listOf(Node::class)), + configuration = configuration + ) { _, v -> + fun traverse(n: Node?, l: Int = 1) { + n ?: return + depth = maxOf(depth, l) + count++ + traverse(n.left, l + 1) + traverse(n.right, l + 1) + } + traverse(v.first()) + Assertions.assertEquals(configuration.recursionTreeDepth, depth) + Assertions.assertEquals((1 shl configuration.recursionTreeDepth) - 1, count) + BaseFeedback(this, Control.STOP) + } + } + } + + @Test + fun `fuzzer can be cancelled by timeout`() { + val start = System.currentTimeMillis() + runBlocking { + assertThrows { + withTimeout(1000) { + runFuzzing( + { _, _ -> sequenceOf(Seed.Simple(Unit)) }, + Description(listOf(Unit)) + ) { _, _ -> + if (System.currentTimeMillis() - start > 10_000) { + error("Fuzzer didn't stopped in 10 000 ms") + } + BaseFeedback(Unit, Control.CONTINUE) + } + } + } + } + } + + @Test + fun `fuzzer can be cancelled by coroutine`() { + runBlocking { + val deferred = async { + val start = System.currentTimeMillis() + runFuzzing( + { _, _ -> sequenceOf(Seed.Simple(Unit)) }, + Description(listOf(Unit)) + ) { _, _ -> + if (System.currentTimeMillis() - start > 10_000) { + error("Fuzzer didn't stopped in 10_000 ms") + } + BaseFeedback(Unit, Control.CONTINUE) + } + } + delay(1000) + deferred.cancel() + } + } + + @Test + fun `fuzzer generate same result when random is seeded`() { + data class B(var a: Int) + val provider = ValueProvider> { _, _ -> + sequenceOf( + Seed.Simple(B(0)) { p, r -> B(p.a + r.nextInt()) }, + Seed.Known(BitVectorValue(32, Signed.POSITIVE)) { B(it.toInt()) }, + Seed.Recursive( + construct = Routine.Create(listOf(Unit)) { _ -> B(2) }, + modify = sequenceOf( + Routine.Call(listOf(Unit)) { self, _ -> self.a = 3 }, + Routine.Call(listOf(Unit)) { self, _ -> self.a = 4 }, + Routine.Call(listOf(Unit)) { self, _ -> self.a = 5 }, + Routine.Call(listOf(Unit)) { self, _ -> self.a = 6 }, + ), + empty = Routine.Empty { B(7) } + ), + Seed.Collection( + construct = Routine.Collection { size -> B(size) }, + modify = Routine.ForEach(listOf(Unit)) { self, ind, v -> self.a = ind * self.a * v.first().a } + ) + ) + } + fun createValues(): MutableList { + val result = mutableListOf() + val probes = 1_000 + runBlocking { + runFuzzing(provider, Description(listOf(Unit))) { _, v -> + result.add(v.first().a) + BaseFeedback(Unit, if (result.size >= probes) Control.STOP else Control.CONTINUE) + } + } + return result + } + val firstRun = createValues() + val secondRun = createValues() + val thirdRun = createValues() + Assertions.assertEquals(firstRun, secondRun) + Assertions.assertEquals(firstRun, thirdRun) + Assertions.assertEquals(secondRun, thirdRun) + } + + @Test + fun `check flow invariant is not violated`() { + val timeConsumer: (Long) -> Unit = {} + runBlocking { + val deferred = async { + val start = System.currentTimeMillis() + flow { + runFuzzing( + { _, _ -> sequenceOf(Seed.Simple(Unit)) }, + Description(listOf(Unit)) + ) { _, _ -> + if (System.currentTimeMillis() - start > 10_000) { + error("Fuzzer didn't stopped in 10_000 ms") + } + emit(System.currentTimeMillis()) + BaseFeedback(Unit, Control.CONTINUE) + } + }.collect { + timeConsumer(it) + } + } + delay(1000) + deferred.cancel() + } + } + + @Test + fun `fuzzer can generate value without mutations`() { + runBlocking { + var seenEmpty = false + withTimeout(1000) { + runFuzzing( + { _, _ -> sequenceOf(Seed.Recursive( + construct = Routine.Create(emptyList()) { StringBuilder("") }, + modify = sequenceOf(Routine.Call(emptyList()) { s, _ -> s.append("1") }), + empty = Routine.Empty { fail("Empty is called despite construct requiring no args") } + )) }, + Description(listOf(Unit)) + ) { _, (s) -> + if (s.isEmpty()) { + seenEmpty = true + BaseFeedback(Unit, Control.STOP) + } else BaseFeedback(Unit, Control.CONTINUE) + } + } + Assertions.assertTrue(seenEmpty) { "Unmodified empty string wasn't generated" } + } + } + + @Test + @Timeout(10, unit = TimeUnit.SECONDS) // withTimeout(1000) works inconsistently + fun `fuzzer works when there are many recursive seeds`() { + class Node(val parent: Node?) + + runBlocking { + var seenAnything = false + withTimeout(1000) { + runFuzzing( + { _, _ -> List(100) {Seed.Recursive( + construct = Routine.Create(listOf(Unit)) { (parent) -> Node(parent) }, + modify = emptySequence(), + empty = Routine.Empty { null } + )}.asSequence() }, + Description(listOf(Unit)) + ) { _, _ -> + seenAnything = true + BaseFeedback(Unit, Control.STOP) + } + } + Assertions.assertTrue(seenAnything) { "Fuzzer hasn't generated any values" } + } + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt new file mode 100644 index 0000000000..a312bd0a61 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/ProvidersTest.kt @@ -0,0 +1,194 @@ +package org.utbot.fuzzing + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class ProvidersTest { + + private fun p(supplier: () -> T) : ValueProvider> { + return ValueProvider { _, _ -> sequenceOf(Seed.Simple(supplier())) } + } + + private fun p( + accept: () -> Boolean, + supplier: () -> T + ) : ValueProvider> { + return object : ValueProvider> { + override fun accept(type: Unit) = accept() + override fun generate(description: Description, type: Unit) = sequence> { + yield(Seed.Simple(supplier())) + } + } + } + + private val description = Description(listOf(Unit)) + + @Test + fun `test common provider API`() { + val provider = ValueProvider.of(listOf(p { 1 })) + (1..3).forEach { _ -> + val attempt = provider.generate(description, Unit).first() as Seed.Simple + Assertions.assertEquals(1, attempt.value) + } + } + + @Test + fun `test merging of providers`() { + var count = 0 + ValueProvider.of(listOf( + p { 1 }, p { 2 }, p { 3 } + )).generate(description, Unit) + .map { (it as Seed.Simple).value } + .forEachIndexed { index, result -> + count++ + Assertions.assertEquals(index + 1, result) + } + Assertions.assertEquals(3, count) + } + + @Test + fun `test merging of providers several times`() { + val p1 = p { 1 } + val p2 = p { 2 } + val p3 = p { 3 } + val p4 = p { 4 } + val m1 = ValueProvider.of(listOf(p1, p2)) + val m2 = p3 with p4 + val m3 = m1 with m2 + val m4 = m3 with ValueProvider.of(emptyList()) + + Assertions.assertEquals(1, p1.generate(description, Unit).count()) + Assertions.assertEquals(1, (p1.generate(description, Unit).first() as Seed.Simple).value) + + Assertions.assertEquals(1, p2.generate(description, Unit).count()) + Assertions.assertEquals(2, (p2.generate(description, Unit).first() as Seed.Simple).value) + + Assertions.assertEquals(1, p3.generate(description, Unit).count()) + Assertions.assertEquals(3, (p3.generate(description, Unit).first() as Seed.Simple).value) + + Assertions.assertEquals(1, p4.generate(description, Unit).count()) + Assertions.assertEquals(4, (p4.generate(description, Unit).first() as Seed.Simple).value) + + Assertions.assertEquals(2, m1.generate(description, Unit).count()) + Assertions.assertEquals(1, (m1.generate(description, Unit).first() as Seed.Simple).value) + Assertions.assertEquals(2, (m1.generate(description, Unit).drop(1).first() as Seed.Simple).value) + + Assertions.assertEquals(2, m2.generate(description, Unit).count()) + Assertions.assertEquals(3, (m2.generate(description, Unit).first() as Seed.Simple).value) + Assertions.assertEquals(4, (m2.generate(description, Unit).drop(1).first() as Seed.Simple).value) + + Assertions.assertEquals(4, m3.generate(description, Unit).count()) + Assertions.assertEquals(1, (m3.generate(description, Unit).first() as Seed.Simple).value) + Assertions.assertEquals(2, (m3.generate(description, Unit).drop(1).first() as Seed.Simple).value) + Assertions.assertEquals(3, (m3.generate(description, Unit).drop(2).first() as Seed.Simple).value) + Assertions.assertEquals(4, (m3.generate(description, Unit).drop(3).first() as Seed.Simple).value) + + Assertions.assertEquals(4, m4.generate(description, Unit).count()) + } + + @Test + fun `test merging of providers by with-method`() { + var count = 0 + p { 1 }.with(p { 2 }).with(p { 3 }).generate(description, Unit) + .map { (it as Seed.Simple).value } + .forEachIndexed { index, result -> + count++ + Assertions.assertEquals(index + 1, result) + } + Assertions.assertEquals(3, count) + } + + @Test + fun `test excepting from providers`() { + val provider = p { 2 } + val seq = ValueProvider.of(listOf( + p { 1 }, provider, p { 3 } + )).except { + it === provider + }.generate(description, Unit) + Assertions.assertEquals(2, seq.count()) + Assertions.assertEquals(1, (seq.first() as Seed.Simple).value) + Assertions.assertEquals(3, (seq.drop(1).first() as Seed.Simple).value) + } + + @Test + fun `test using except on provider with fallback`() { + val provider1 = p { 2 } + val provider2 = p { 3 } + val fallback = p { 4 } + val providers1 = ValueProvider.of(listOf( + provider1.withFallback(fallback), + provider2 + )) + val seq1 = providers1.generate(description, Unit).toSet() + Assertions.assertEquals(2, seq1.count()) + Assertions.assertEquals(2, (seq1.first() as Seed.Simple).value) + Assertions.assertEquals(3, (seq1.drop(1).first() as Seed.Simple).value) + + val providers2 = providers1.except(provider1) + + val seq2 = providers2.generate(description, Unit).toSet() + Assertions.assertEquals(2, seq2.count()) + Assertions.assertEquals(4, (seq2.first() as Seed.Simple).value) + Assertions.assertEquals(3, (seq2.drop(1).first() as Seed.Simple).value) + } + + @Test + fun `provider is not called when accept-method returns false`() { + val seq = ValueProvider.of(listOf( + p({ true }, { 1 }), p({ false }, { 2 }), p({ true }, { 3 }), + )).generate(description, Unit) + Assertions.assertEquals(2, seq.count()) + Assertions.assertEquals(1, (seq.first() as Seed.Simple).value) + Assertions.assertEquals(3, (seq.drop(1).first() as Seed.Simple).value) + } + + @Test + fun `provider doesnt call fallback when values is generated`() { + val seq = ValueProvider.of(listOf( + p({ true }, { 1 }), p({ false }, { 2 }), p({ true }, { 3 }), + )).withFallback { + Seed.Simple(4) + }.generate(description, Unit) + Assertions.assertEquals(2, seq.count()) + Assertions.assertEquals(1, (seq.first() as Seed.Simple).value) + Assertions.assertEquals(3, (seq.drop(1).first() as Seed.Simple).value) + } + + @Test + fun `provider calls fallback when values are not generated`() { + val seq = ValueProvider.of(listOf( + p({ false }, { 1 }), p({ false }, { 2 }), p({ false }, { 3 }), + )).withFallback { + Seed.Simple(4) + }.generate(description, Unit) + Assertions.assertEquals(1, seq.count()) + Assertions.assertEquals(4, (seq.first() as Seed.Simple).value) + } + + @Test + fun `provider generates no values when fallback cannot accept value`() { + val seq = ValueProvider.of(listOf( + p({ false }, { 1 }), p({ false }, { 2 }), p({ false }, { 3 }), + )).withFallback( + object : ValueProvider> { + override fun accept(type: Unit) = false + override fun generate(description: Description, type: Unit) = emptySequence>() + } + ).generate(description, Unit) + Assertions.assertEquals(0, seq.count()) + } + + @Test + fun `type providers check exactly the type`() { + val seq1 = TypeProvider>('A') { _, _ -> + yield(Seed.Simple(2)) + }.generate(Description(listOf('A')), 'A') + Assertions.assertEquals(1, seq1.count()) + + val seq2 = TypeProvider>('A') { _, _ -> + yield(Seed.Simple(2)) + }.generate(Description(listOf('A')), 'B') + Assertions.assertEquals(0, seq2.count()) + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/providers/known/BitVectorValueTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/providers/known/BitVectorValueTest.kt new file mode 100644 index 0000000000..dc447931a7 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/providers/known/BitVectorValueTest.kt @@ -0,0 +1,136 @@ +package org.utbot.fuzzing.providers.known + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.DefaultBound +import org.utbot.fuzzing.seeds.Signed +import org.utbot.fuzzing.seeds.Unsigned +import kotlin.random.Random + +class BitVectorValueTest { + + @Test + fun `convert default kotlin literals to vector`() { + for (i in Byte.MIN_VALUE..Byte.MAX_VALUE) { + assertEquals(i.toByte(), BitVectorValue(Byte.SIZE_BITS, DefaultBound.ofByte(i.toByte())).toByte()) + } + + for (i in Short.MIN_VALUE..Short.MAX_VALUE) { + assertEquals(i.toShort(), BitVectorValue(Short.SIZE_BITS, DefaultBound.ofShort(i.toShort())).toShort()) + } + + val randomIntSequence = with(Random(0)) { sequence { while (true) yield(nextInt()) } } + randomIntSequence.take(100_000).forEach { + assertEquals(it, BitVectorValue(Int.SIZE_BITS, DefaultBound.ofInt(it)).toInt()) + } + + val randomLongSequence = with(Random(0)) { sequence { while (true) yield(nextLong()) } } + randomLongSequence.take(100_000).forEach { + assertEquals(it, BitVectorValue(Long.SIZE_BITS, DefaultBound.ofLong(it)).toLong()) + } + } + + @Test + fun `test default bit vectors (byte)`() { + assertEquals(0, BitVectorValue(8, Signed.ZERO).toByte()) + assertEquals(-128, BitVectorValue(8, Signed.MIN).toByte()) + assertEquals(-1, BitVectorValue(8, Signed.NEGATIVE).toByte()) + assertEquals(1, BitVectorValue(8, Signed.POSITIVE).toByte()) + assertEquals(127, BitVectorValue(8, Signed.MAX).toByte()) + } + + @Test + fun `test default bit vectors (unsigned byte)`() { + assertEquals(0u.toUByte(), BitVectorValue(8, Unsigned.ZERO).toUByte()) + assertEquals(1u.toUByte(), BitVectorValue(8, Unsigned.POSITIVE).toUByte()) + assertEquals(255u.toUByte(), BitVectorValue(8, Unsigned.MAX).toUByte()) + } + + @Test + fun `test default bit vectors (short)`() { + assertEquals(0, BitVectorValue(16, Signed.ZERO).toShort()) + assertEquals(Short.MIN_VALUE, BitVectorValue(16, Signed.MIN).toShort()) + assertEquals(-1, BitVectorValue(16, Signed.NEGATIVE).toShort()) + assertEquals(1, BitVectorValue(16, Signed.POSITIVE).toShort()) + assertEquals(Short.MAX_VALUE, BitVectorValue(16, Signed.MAX).toShort()) + } + + @Test + fun `test default bit vectors (unsigned short)`() { + assertEquals(UShort.MIN_VALUE, BitVectorValue(16, Unsigned.ZERO).toUShort()) + assertEquals(1u.toUShort(), BitVectorValue(16, Unsigned.POSITIVE).toUShort()) + assertEquals(UShort.MAX_VALUE, BitVectorValue(16, Unsigned.MAX).toUShort()) + } + + @Test + fun `test default bit vectors (int)`() { + assertEquals(0, BitVectorValue(32, Signed.ZERO).toInt()) + assertEquals(Int.MIN_VALUE, BitVectorValue(32, Signed.MIN).toInt()) + assertEquals(-1, BitVectorValue(32, Signed.NEGATIVE).toInt()) + assertEquals(1, BitVectorValue(32, Signed.POSITIVE).toInt()) + assertEquals(Int.MAX_VALUE, BitVectorValue(32, Signed.MAX).toInt()) + } + + @Test + fun `test default bit vectors (unsigned int)`() { + assertEquals(UInt.MIN_VALUE, BitVectorValue(32, Unsigned.ZERO).toUInt()) + assertEquals(1u, BitVectorValue(32, Unsigned.POSITIVE).toUInt()) + assertEquals(UInt.MAX_VALUE, BitVectorValue(32, Unsigned.MAX).toUInt()) + } + + @Test + fun `test default bit vectors (long)`() { + assertEquals(0, BitVectorValue(64, Signed.ZERO).toLong()) + assertEquals(Long.MIN_VALUE, BitVectorValue(64, Signed.MIN).toLong()) + assertEquals(-1, BitVectorValue(64, Signed.NEGATIVE).toLong()) + assertEquals(1, BitVectorValue(64, Signed.POSITIVE).toLong()) + assertEquals(Long.MAX_VALUE, BitVectorValue(64, Signed.MAX).toLong()) + } + + @Test + fun `test default bit vectors (unsigned long)`() { + assertEquals(ULong.MIN_VALUE, BitVectorValue(64, Unsigned.ZERO).toULong()) + assertEquals(1uL, BitVectorValue(64, Unsigned.POSITIVE).toULong()) + assertEquals(ULong.MAX_VALUE, BitVectorValue(64, Unsigned.MAX).toULong()) + } + + @Test + fun `convert byte from and to`() { + for (i in Byte.MIN_VALUE..Byte.MAX_VALUE) { + assertEquals(i.toByte(), BitVectorValue.fromByte(i.toByte()).toByte()) + } + } + + @Test + fun `inc and dec byte`() { + for (i in Byte.MIN_VALUE..Byte.MAX_VALUE) { + val v = BitVectorValue.fromByte(i.toByte()) + assertEquals(i.toByte() == Byte.MAX_VALUE, v.inc()) { "$v" } + assertEquals((i + 1).toByte(), v.toByte()) + } + + for (i in Byte.MAX_VALUE downTo Byte.MIN_VALUE) { + val v = BitVectorValue.fromByte(i.toByte()) + assertEquals(i.toByte() == Byte.MIN_VALUE, v.dec()) { "$v" } + assertEquals((i - 1).toByte(), v.toByte()) + } + } + + @Test + fun `inc and dec long`() { + val r = with(Random(0)) { LongArray(1024) { nextLong() } } + r[0] = Long.MIN_VALUE + r[1] = Long.MAX_VALUE + r.forEach { l -> + val v = BitVectorValue.fromLong(l) + assertEquals(l == Long.MAX_VALUE, v.inc()) { "$v" } + assertEquals(l + 1, v.toLong()) + } + r.forEach { l -> + val v = BitVectorValue.fromLong(l) + assertEquals(l == Long.MIN_VALUE, v.dec()) { "$v" } + assertEquals(l - 1, v.toLong()) + } + } +} \ No newline at end of file diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Arrays.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Arrays.java new file mode 100644 index 0000000000..02520ef345 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Arrays.java @@ -0,0 +1,110 @@ +package org.utbot.fuzzing.samples; + +import java.util.HashSet; +import java.util.Set; + +@SuppressWarnings({"unused", "ForLoopReplaceableByForEach"}) +public class Arrays { + + // should find identity matrix + public boolean isIdentityMatrix(int[][] matrix) { + if (matrix.length < 3) { + throw new IllegalArgumentException("matrix.length < 3"); + } + + for (int i = 0; i < matrix.length; i++) { + if (matrix[i].length != matrix.length) { + return false; + } + for (int j = 0; j < matrix[i].length; j++) { + if (i == j && matrix[i][j] != 1) { + return false; + } + + if (i != j && matrix[i][j] != 0) { + return false; + } + } + } + + return true; + } + + // should fail with OOME and should reveal some branches + public boolean isIdentityMatrix(int[][][] matrix) { + if (matrix.length < 3) { + throw new IllegalArgumentException("matrix.length < 3"); + } + + for (int i = 0; i < matrix.length; i++) { + if (matrix[i].length != matrix.length) { + return false; + } else { + for (int j = 0; j < matrix.length; j++) { + if (matrix[i][j].length != matrix[i].length) { + return false; + } + } + } + for (int j = 0; j < matrix[i].length; j++) { + for (int k = 0; k < matrix[i][j].length; k++) { + if (i == j && j == k && matrix[i][j][k] != 1) { + return false; + } + + if ((i != j || j != k) && matrix[i][j][k] != 0) { + return false; + } + } + } + } + + return true; + } + + public static class Point { + int x, y; + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + boolean equals(Point other) { + return (other.x == this.x && other.y == this.y); + } + } + + boolean checkAllSame(Integer[] a) { // Fuzzer should find parameters giving true as well as parameters giving false + if (a.length < 1) return true; + Set s = new HashSet<>(java.util.Arrays.asList(a)); + return (s.size() <= 1); + } + + public boolean checkAllSamePoints(Point[] a) { // Also works for classes + if (a.length == 4) { + return false; // Test with array of size 4 should be generated by fuzzer + } + for (int i = 1; i < a.length; i++) { + if (!a[i].equals(a[i - 1])) + return false; + } + return true; + } + + public boolean checkRowsWithAllSame2D(int[][] a, int y) { + int cntSame = 0; + for (int i = 0; i < a.length; i++) { + boolean same = true; + for (int j = 1; j < a[i].length; j++) { + if (a[i][j] != a[i][j - 1]) { + same = false; + break; + } + } + if (same) + cntSame++; + } + return (cntSame == y && y > 0 && y < a.length); + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Collections.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Collections.java new file mode 100644 index 0000000000..e26c7b6d1a --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Collections.java @@ -0,0 +1,189 @@ +package org.utbot.fuzzing.samples; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@SuppressWarnings({"unused", "RedundantIfStatement"}) +public class Collections { + /** + * Should create unsorted list that will be sorted as a result. + */ + public static Collection sorted(Collection source) { + if (source.size() < 2) throw new IllegalArgumentException(); + return source.stream().sorted().collect(Collectors.toList()); + } + + /** + * Should create at least both answers: one that finds the key, and another returns null. + */ + public static String getKeyForValue(Map map, Number value) { + for (Map.Entry entry : map.entrySet()) { + if (java.util.Objects.equals(entry.getValue(), value)) { + return entry.getKey(); + } + } + return null; + } + + /** + * Should find a branch that returns true and size of array is greater than 1 (non-trivial). + */ + public static boolean isDiagonal(Collection> matrix) { + int cols = matrix.size(); + if (cols <= 1) { + return false; + } + int i = 0; + for (Collection col : matrix) { + if (col.size() != cols) { + return false; + } + int j = 0; + for (Double value : col) { + if (i == j && value == 0.0) return false; + if (i != j && value != 0.0) return false; + j++; + } + i++; + } + return true; + } + + /** + * Checks that different collections can be created. Part 1 + */ + public boolean allCollectionAreSameSize1( + Collection c, + List l, + Set s, + SortedSet ss, + Deque d, + Iterable i + ) { + if (c.size() != l.size()) { + return false; + } + if (l.size() != s.size()) { + return false; + } + if (s.size() != ss.size()) { + return false; + } + if (ss.size() != d.size()) { + return false; + } + if (d.size() != StreamSupport.stream(i.spliterator(), false).count()) { + return false; + } + return true; + } + + /** + * Checks that different collections can be created. Part 2 + */ + public boolean allCollectionAreSameSize2( + Iterable i, + Stack st, + NavigableSet ns, + Map m, + SortedMap sm, + NavigableMap nm + ) { + if (StreamSupport.stream(i.spliterator(), false).count() != st.size()) { + return false; + } + if (st.size() != ns.size()) { + return false; + } + if (ns.size() != m.size()) { + return false; + } + if (m.size() != sm.size()) { + return false; + } + if (sm.size() != nm.size()) { + return false; + } + return true; + } + + /** + * Should create TreeSet without any modifications as T extends Number is not Comparable + */ + public boolean testTreeSetWithoutComparable(NavigableSet set) { + if (set.size() > 3) { + return true; + } + return false; + } + + /** + * Should create TreeSet with modifications as Integer is Comparable + */ + public boolean testTreeSetWithComparable(NavigableSet set) { + if (set.size() > 3) { + return true; + } + return false; + } + + public static class ConcreteList extends LinkedList { + public boolean equals(Collection collection) { + if (collection.size() != size()) { + return false; + } + int i = 0; + for (T t : collection) { + if (!java.util.Objects.equals(get(i), t)) { + return false; + } + } + return true; + } + } + + /** + * Should create concrete class + */ + public boolean testConcreteCollectionIsCreated(ConcreteList list) { + if (list.size() > 3) { + return true; + } + return false; + } + + public static class ConcreteMap extends HashMap { } + + /** + * Should create concrete class + */ + public boolean testConcreteMapIsCreated(ConcreteMap map) { + if (map.size() > 3) { + return true; + } + return false; + } + + /** + * Should create test with no error + */ + public boolean testNoErrorWithHashMap(HashMap map) { + if (map.size() > 5) { + return true; + } + return false; + } + + /** + * Should generate iterators with recursions + */ + public static > int size(Iterator some) { + int r = 0; + while (some.hasNext()) { + some.next(); + r++; + } + return r; + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Dates.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Dates.java new file mode 100644 index 0000000000..3e0bb16aae --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Dates.java @@ -0,0 +1,21 @@ +package org.utbot.fuzzing.samples; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +@SuppressWarnings({"unused", "RedundantIfStatement"}) +public class Dates { + public boolean getTime(Date date) { + return date.getTime() == 100; + } + + public boolean getTimeFormatted(Date date) throws ParseException { + boolean after = new SimpleDateFormat("dd-MM-yyyy").parse("10-06-2012").after(date); + if (after) { + return true; + } else { + return false; + } + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Enums.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Enums.java new file mode 100644 index 0000000000..0a216f4af1 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Enums.java @@ -0,0 +1,61 @@ +package org.utbot.fuzzing.samples; + +@SuppressWarnings("unused") +public class Enums { + + public int test(int x) { + switch(x) { + case 1: + return 1; + case 2: + return 2; + case 3: + return 3; + } + return 0; + } + + public int test3(int x) { + switch(x) { + case 11: + return 1; + case 25: + return 2; + case 32: + return 3; + } + return 0; + } + + public int test4(S x) { + switch(x) { + case A: + return 1; + case B: + return 2; + case C: + return 3; + } + return 0; + } + + public enum S { + A, B, C + } + + public int test2(int x) { + int a = 0; + switch(x) { + case 1: + a = 1; + break; + case 2: + a = 2; + break; + case 3: + a = 3; + } + return a; + } + +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Floats.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Floats.java new file mode 100644 index 0000000000..0f0917ffe9 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Floats.java @@ -0,0 +1,44 @@ +package org.utbot.fuzzing.samples; + +@SuppressWarnings("unused") +public class Floats { + + // Should find the value between 0 and -1. + public int floatToInt(float x) { + if (x < 0) { + if ((int) x < 0) { + return 1; + } + return 2; // smth small to int zero + } + return 3; + } + + // should find all branches that return -2, -1, 0, 1, 2. + public int numberOfRootsInSquareFunction(double a, double b, double c) { + if (!Double.isFinite(a) || !Double.isFinite(b) || !Double.isFinite(c)) return -1; + if (a == 0.0 || b == 0.0 || c == 0.0) return -2; + double result = b * b - 4 * a * c; + if (result > 0) { + return 2; + } else if (result == 0) { + return 1; + } + return 0; + } + + // will never be succeeded to find the value because of floating precision + public void floatEq(float v) { + if (v == 28.7) { + throw new IllegalArgumentException(); + } + } + + // should generate double for constant float that equals to 28.700000762939453 + public void floatEq(double v) { + if (v == 28.7f) { + throw new IllegalArgumentException(); + } + } + +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Generics.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Generics.java new file mode 100644 index 0000000000..381d71a70d --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Generics.java @@ -0,0 +1,27 @@ +package org.utbot.fuzzing.samples; + +@SuppressWarnings("unused") +public class Generics> { + + private final T[] value; + + public Generics(T[] value) { + this.value = value; + } + + // should generate data with numbers to sum it + public double getSum() { + double sum = 0; + for (T numbers : value) { + for (Number number : numbers) { + if (number.doubleValue() > 0) { + sum += number.doubleValue(); + } + } + } + if (sum == 0.0) { + throw new IllegalStateException(); + } + return sum; + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Integers.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Integers.java new file mode 100644 index 0000000000..d65dcbdfa9 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Integers.java @@ -0,0 +1,66 @@ +package org.utbot.fuzzing.samples; + +@SuppressWarnings({"unused", "RedundantIfStatement"}) +public class Integers { + + public byte biting(byte a, byte b, boolean s) { + if (s) { + return a; + } else { + return b; + } + } + + // should cover 100% + public static void diff(int a) { + a = Math.abs(a); + while (a > 0) { + a = a % 2; + } + if (a < 0) { + throw new IllegalArgumentException(); + } + throw new RuntimeException(); + } + + // should cover 100% and better when values are close to constants, + // also should generate "this" empty object + public String extent(int a) { + if (a < -2.0) { + return "-1"; + } + if (a > 5) { + return "-2"; + } + if (a == 3) { + return "-3"; + } + if (4L < a) { + return "-4"; + } + return "0"; + } + + // should cover 100% with 3 tests + public static boolean isGreater(long a, short b, int c) { + if (b > a && a < c) { + return true; + } + return false; + } + + // should find a bad value with integer overflow + public boolean unreachable(int x) { + int y = x * x - 2 * x + 1; + if (y < 0) throw new IllegalArgumentException(); + return true; + } + + public boolean chars(char a) { + if (a >= 'a' && a <= 'z') { + return true; + } + return false; + } + +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Objects.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Objects.java new file mode 100644 index 0000000000..a6d640cc48 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Objects.java @@ -0,0 +1,36 @@ +package org.utbot.fuzzing.samples; + +import org.utbot.fuzzing.samples.data.Recursive; +import org.utbot.fuzzing.samples.data.UserDefinedClass; + +@SuppressWarnings({"unused", "RedundantIfStatement"}) +public class Objects { + + public int test(UserDefinedClass userDefinedClass) { + if (userDefinedClass.getX() > 5 && userDefinedClass.getY() < 10) { + return 1; + } else if (userDefinedClass.getZ() < 20) { + return 2; + } else { + return 3; + } + } + + public boolean testMe(Recursive r) { + if (r.val() > 10) { + return true; + } + return false; + } + + private int data; + + public static void foo(Objects a, Objects b) { + a.data = 1; + b.data = 2; + //noinspection ConstantValue + if (a.data == b.data) { + throw new IllegalArgumentException(); + } + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Strings.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Strings.java new file mode 100644 index 0000000000..49ef688ac5 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/Strings.java @@ -0,0 +1,67 @@ +package org.utbot.fuzzing.samples; + +import java.util.regex.Pattern; + +@SuppressWarnings({"unused", "SimplifiableConditionalExpression"}) +public class Strings { + + // should find a string that starts with "bad!" prefix + public static void test(String s) { + if (s.charAt(0) == "b".charAt(0)) { + if (s.charAt(1) == "a".charAt(0)) { + if (s.charAt(2) == "d".charAt(0)) { + if (s.charAt(3) == "!".charAt(0)) { + throw new IllegalArgumentException(); + } + } + } + } + } + + // should try to find the string with size 6 and with "!" in the end + public static void testStrRem(String str) { + if (!"world???".equals(str) && str.charAt(5) == '!' && str.length() == 6) { + throw new RuntimeException(); + } + } + + public boolean isValidUuid(String uuid) { + return isNotBlank(uuid) && uuid + .matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } + + public boolean isValidUuidShortVersion(String uuid) { + return uuid != null && uuid.matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } + + public boolean isValidDouble(String value) { + return value.matches("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?([fFdD]?)") && value.contains("E") && value.contains("-"); + } + + static final String pattern = "\\d+"; + + public boolean isNumber(String s) { + return Pattern.matches(pattern, s) ? true : false; + } + + private static boolean isNotBlank(CharSequence cs) { + return !isBlank(cs); + } + + private static boolean isBlank(CharSequence cs) { + int strLen = length(cs); + if (strLen != 0) { + for (int i = 0; i < strLen; ++i) { + if (!Character.isWhitespace(cs.charAt(i))) { + return false; + } + } + + } + return true; + } + + private static int length(CharSequence cs) { + return cs == null ? 0 : cs.length(); + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/Recursive.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/Recursive.java new file mode 100644 index 0000000000..9f3ab45d71 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/Recursive.java @@ -0,0 +1,19 @@ +package org.utbot.fuzzing.samples.data; + +@SuppressWarnings("unused") +public class Recursive { + + private final int a; + + public Recursive(int a) { + this.a = a; + } + + public Recursive(Recursive r) { + this.a = r.a; + } + + public int val() { + return a; + } +} diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/UserDefinedClass.java b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/UserDefinedClass.java new file mode 100644 index 0000000000..5733a78c55 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/samples/data/UserDefinedClass.java @@ -0,0 +1,44 @@ +package org.utbot.fuzzing.samples.data; + +@SuppressWarnings("unused") +public class UserDefinedClass { + public int x; + private int y; + public int z; + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public UserDefinedClass setY(int y) { + this.y = y; + return this; + } + + public void setZ(int z) { + this.z = z; + } + + public int getZ() { + return z; + } + + public UserDefinedClass() { + } + + public UserDefinedClass(int x, int y) { + this.x = x; + this.y = y; + this.z = 0; + } + + public UserDefinedClass(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } +} diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CombinationsTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/CombinationsTest.kt similarity index 92% rename from utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CombinationsTest.kt rename to utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/CombinationsTest.kt index bddde87d50..a1ae742b0c 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/CombinationsTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/CombinationsTest.kt @@ -1,7 +1,5 @@ -package org.utbot.framework.plugin.api +package org.utbot.fuzzing.utils -import org.utbot.fuzzer.CartesianProduct -import org.utbot.fuzzer.Combinations import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse @@ -11,7 +9,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import org.utbot.fuzzer.TooManyCombinationsException import java.util.BitSet import kotlin.math.pow import kotlin.random.Random @@ -270,4 +267,33 @@ class CombinationsTest { assertArrayEquals(intArrayOf(1, 1), combinations[Int.MAX_VALUE + 1L]) assertArrayEquals(intArrayOf(1, Int.MAX_VALUE - 1), combinations[Int.MAX_VALUE * 2L - 1]) } + + @Test + fun testLazyCartesian() { + val source = listOf( + (1..10).toList(), + listOf("a", "b", "c"), + listOf(true, false) + ) + val eager = CartesianProduct(source).asSequence().toList() + val lazy = source.cartesian().toList() + assertEquals(eager.size, lazy.size) + for (i in eager.indices) { + assertEquals(eager[i], lazy[i]) + } + } + + @Test + fun testLazyCartesian2() { + val source = listOf( + (1..10).toList(), + listOf("a", "b", "c"), + listOf(true, false) + ) + val eager = CartesianProduct(source).asSequence().toList() + var index = 0 + source.cartesian { + assertEquals(eager[index++], it) + } + } } \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/PseudoShuffledIntProgressionTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgressionTest.kt similarity index 98% rename from utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/PseudoShuffledIntProgressionTest.kt rename to utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgressionTest.kt index 6f1f6fd64c..af48867f06 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/PseudoShuffledIntProgressionTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/PseudoShuffledIntProgressionTest.kt @@ -1,10 +1,9 @@ -package org.utbot.framework.plugin.api +package org.utbot.fuzzing.utils 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 { diff --git a/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/RandomExtensionsTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/RandomExtensionsTest.kt new file mode 100644 index 0000000000..ab83236b09 --- /dev/null +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/RandomExtensionsTest.kt @@ -0,0 +1,100 @@ +package org.utbot.fuzzing.utils + +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 kotlin.math.abs +import kotlin.random.Random + +class RandomExtensionsTest { + + @Test + fun `default implementation always returns 0`() { + val frequencies = doubleArrayOf(1.0) + (0 until 1000).forEach { _ -> + assertEquals(0, Random.chooseOne(frequencies)) + } + } + + @ParameterizedTest(name = "seed{arguments}") + @ValueSource(ints = [0, 100, -123, 99999, 84]) + fun `with default forEach function frequencies is equal`(seed: Int) { + val random = Random(seed) + val frequencies = doubleArrayOf(10.0, 20.0, 30.0, 40.0) + val result = IntArray(frequencies.size) + assertEquals(100.0, frequencies.sum()) { "In this test every frequency value represents a percent. The sum must be equal to 100" } + val tries = 100_000 + val errors = tries / 100 // 1% + (0 until tries).forEach { _ -> + result[random.chooseOne(frequencies)]++ + } + val expected = frequencies.map { tries * it / 100} + result.forEachIndexed { index, value -> + assertTrue(abs(expected[index] - value) < errors) { + "The error should not extent $errors for $tries cases, but ${expected[index]} and $value too far" + } + } + } + + @Test + fun `inverting probabilities from the documentation`() { + val frequencies = doubleArrayOf(20.0, 80.0) + val random = Random(0) + val result = IntArray(frequencies.size) + val tries = 10_000 + val errors = tries / 100 // 1% + (0 until tries).forEach { _ -> + result[random.chooseOne(DoubleArray(frequencies.size) { 100.0 - frequencies[it] })]++ + } + result.forEachIndexed { index, value -> + val expected = frequencies[frequencies.size - 1 - index] * errors + assertTrue(abs(value - expected) < errors) { + "The error should not extent 100 for 10 000 cases, but $expected and $value too far" + } + } + } + + @Test + fun `flip the coin is fair enough`() { + val random = Random(0) + var result = 0 + val probability = 20 + val experiments = 1_000_000 + for (i in 0 until experiments) { + if (random.flipCoin(probability)) { + result++ + } + } + val error = experiments / 1000 // 0.1 % + assertTrue(abs(result - experiments * probability / 100) < error) + } + + @Test + fun `invert bit works for long`() { + var attempts = 100_000 + val random = Random(2210) + sequence { + while (true) { + yield(random.nextLong()) + } + }.forEach { value -> + if (attempts-- <= 0) { return } + for (bit in 0 until Long.SIZE_BITS) { + val newValue = value.invertBit(bit) + val oldBinary = value.toBinaryString() + val newBinary = newValue.toBinaryString() + assertEquals(oldBinary.length, newBinary.length) + for (test in Long.SIZE_BITS - 1 downTo 0) { + if (test != Long.SIZE_BITS - 1 - bit) { + assertEquals(oldBinary[test], newBinary[test]) { "$oldBinary : $newBinary for value $value" } + } else { + assertNotEquals(oldBinary[test], newBinary[test]) { "$oldBinary : $newBinary for value $value" } + } + } + } + } + } + + private fun Long.toBinaryString() = java.lang.Long.toBinaryString(this).padStart(Long.SIZE_BITS, '0') +} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/TrieTest.kt b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/TrieTest.kt similarity index 96% rename from utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/TrieTest.kt rename to utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/TrieTest.kt index a5cd8f62ab..cc3a6ba360 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/TrieTest.kt +++ b/utbot-fuzzing/src/test/kotlin/org/utbot/fuzzing/utils/TrieTest.kt @@ -1,10 +1,7 @@ -package org.utbot.framework.plugin.api +package org.utbot.fuzzing.utils import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test -import org.utbot.fuzzer.Trie -import org.utbot.fuzzer.stringTrieOf -import org.utbot.fuzzer.trieOf class TrieTest { diff --git a/utbot-go/build.gradle.kts b/utbot-go/build.gradle.kts new file mode 100644 index 0000000000..3a31276f7b --- /dev/null +++ b/utbot-go/build.gradle.kts @@ -0,0 +1,31 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + test { + useJUnitPlatform() + } +} + +dependencies { + api(project(":utbot-fuzzing")) + api(project(":utbot-framework")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("com.beust:klaxon:5.5") + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) +} \ No newline at end of file diff --git a/utbot-go/docs/DEVELOPER_GUIDE.md b/utbot-go/docs/DEVELOPER_GUIDE.md new file mode 100644 index 0000000000..86aea31c0d --- /dev/null +++ b/utbot-go/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,61 @@ +# UnitTestBot Go Developer Guide + +## Overview + +Here are the main stages of UnitTestBot Go test generation pipeline. + +```mermaid +flowchart TB + A["Target selection and configuration (IntelliJ IDEA plugin or CLI)"]:::someclass --> B(Go source code analysis) + classDef someclass fill:#b810 + + B --> C(Code instrumentation) + C --> D(Generation of arguments) + D --> E(Executing functions) + E --> F(Getting results) + F --> D + D ---> G(Test file generation) +``` + +### Target selection and configuration + +A user manually selects the target source file and functions to generate the tests for. +Test generation settings are also configured manually. + +### Code instrumentation + +After each line in a function, UnitTestBot Go adds logging about this line traversal during the execution. + +### Go source code analysis + +UnitTestBot Go gains information about the `int` or `uint` value size in bits, and information about the target functions: +* signatures and type information; +* constants in the function bodies. + +As a result, UnitTestBot Go gets an internal +representation of the target functions. + +### Generation of arguments + +At this stage, UnitTestBot Go generates the input values for the target functions. + +### Executing target functions + +UnitTestBot Go executes the target functions with the generated values as arguments. + +### Getting results + +The result of target function execution is sent to the fuzzer for analysis: +coverage rate information guides the following value generation process. +Based on remaining time, +the fuzzer decides whether it should continue or stop generating input values. + +### Test code generation + +Generating test source code requires support for Go language features (for example, the necessity to cast +constants to the desired type). +Extended support for various Go constructs is a future plan for us. + +## How to test UnitTestBot Go + +_**TODO**_ diff --git a/utbot-go/docs/FUTURE_PLANS.md b/utbot-go/docs/FUTURE_PLANS.md new file mode 100644 index 0000000000..d841237f73 --- /dev/null +++ b/utbot-go/docs/FUTURE_PLANS.md @@ -0,0 +1,13 @@ +# UTBot Go: Future plans + +## Primarily + +_**TODO**_ + +## Afterwards + +_**TODO**_ + +## Maybe in the future + +_**TODO**_ diff --git a/utbot-go/docs/images/install-intellij-plugin-from-disk.png b/utbot-go/docs/images/install-intellij-plugin-from-disk.png new file mode 100644 index 0000000000..1f0ceee756 Binary files /dev/null and b/utbot-go/docs/images/install-intellij-plugin-from-disk.png differ diff --git a/utbot-go/go-samples/go.mod b/utbot-go/go-samples/go.mod new file mode 100644 index 0000000000..8f59b4476d --- /dev/null +++ b/utbot-go/go-samples/go.mod @@ -0,0 +1,13 @@ +module go-samples + +go 1.19 + +require ( + github.com/pmezard/go-difflib v1.0.0 + github.com/stretchr/testify v1.8.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/utbot-go/go-samples/go.sum b/utbot-go/go-samples/go.sum new file mode 100644 index 0000000000..2ec90f70f8 --- /dev/null +++ b/utbot-go/go-samples/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/utbot-go/go-samples/simple/nested/nested.go b/utbot-go/go-samples/simple/nested/nested.go new file mode 100644 index 0000000000..772d6cc2a6 --- /dev/null +++ b/utbot-go/go-samples/simple/nested/nested.go @@ -0,0 +1,9 @@ +package nested + +type unexportedStruct struct { + int +} + +type ExportedStruct struct { + unexportedStruct +} diff --git a/utbot-go/go-samples/simple/samples.go b/utbot-go/go-samples/simple/samples.go new file mode 100644 index 0000000000..52e337bf63 --- /dev/null +++ b/utbot-go/go-samples/simple/samples.go @@ -0,0 +1,154 @@ +// Most of the algorithm examples are taken from https://github.com/TheAlgorithms/Go + +package simple + +import ( + "errors" + "math" +) + +// DivOrPanic divides x by y or panics if y is 0 +func DivOrPanic(x int, y int) int { + if y == 0 { + panic("div by 0") + } + return x / y +} + +// Extended simple extended gcd +func Extended(a, b int64) (int64, int64, int64) { + if a == 0 { + return b, 0, 1 + } + gcd, xPrime, yPrime := Extended(b%a, a) + return gcd, yPrime - (b/a)*xPrime, xPrime +} + +func ArraySum(array [5]int) int { + sum := 0 + for _, elem := range array { + sum += elem + } + return sum +} + +func GenerateArrayOfIntegers(num int) [10]int { + result := [10]int{} + for i := range result { + result[i] = num + } + return result +} + +type Point struct { + x, y float64 +} + +func DistanceBetweenTwoPoints(a, b Point) float64 { + return math.Sqrt(math.Pow(a.x-b.x, 2) + math.Pow(a.y-b.y, 2)) +} + +func GetCoordinatesOfMiddleBetweenTwoPoints(a, b Point) (float64, float64) { + return (a.x + b.x) / 2, (a.y + b.y) / 2 +} + +func GetCoordinateSumOfPoints(points []Point) (float64, float64) { + sumX := 0.0 + sumY := 0.0 + for _, point := range points { + sumX += point.x + sumY += point.y + } + return sumX, sumY +} + +type Circle struct { + Center Point + Radius float64 +} + +func GetAreaOfCircle(circle Circle) float64 { + return math.Pi * math.Pow(circle.Radius, 2) +} + +func IsIdentity(matrix [3][3]int) bool { + for i := 0; i < 3; i++ { + for j := 0; j < 3; j++ { + if i == j && matrix[i][j] != 1 { + return false + } + + if i != j && matrix[i][j] != 0 { + return false + } + } + } + return true +} + +var ErrNotFound = errors.New("target not found in array") + +// Binary search for target within a sorted array by repeatedly dividing the array in half and comparing the midpoint with the target. +// This function uses recursive call to itself. +// If a target is found, the index of the target is returned. Else the function return -1 and ErrNotFound. +func Binary(array []int, target int, lowIndex int, highIndex int) (int, error) { + if highIndex < lowIndex { + return -1, ErrNotFound + } + mid := lowIndex + (highIndex-lowIndex)/2 + if array[mid] > target { + return Binary(array, target, lowIndex, mid-1) + } else if array[mid] < target { + return Binary(array, target, mid+1, highIndex) + } else { + return mid, nil + } +} + +func StringSearch(str string) bool { + if len(str) != 3 { + return false + } + if str[0] == 'A' { + if str[1] == 'B' { + if str[2] == 'C' { + return true + } + } + } + return false +} + +func SumOfChanElements(c <-chan int) int { + sum := 0 + for val := range c { + sum += val + } + return sum +} + +type List struct { + tail *List + val int +} + +func LenOfList(l *List) int { + if l == nil { + return 0 + } + length := 1 + for ; l.tail != nil; l = l.tail { + length++ + } + return length +} + +func GetLastNode(n *Node) *Node { + if n == nil { + return nil + } + for ; n.next != nil; n = n.next { + + } + return n +} diff --git a/utbot-go/go-samples/simple/samples_go_ut_test.go b/utbot-go/go-samples/simple/samples_go_ut_test.go new file mode 100644 index 0000000000..1c5f5e9986 --- /dev/null +++ b/utbot-go/go-samples/simple/samples_go_ut_test.go @@ -0,0 +1,459 @@ +package simple + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDivOrPanicByUtGoFuzzer(t *testing.T) { + x := 0 + y := 1 + + actualVal := DivOrPanic(x, y) + + expectedVal := 0 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestDivOrPanicPanicsByUtGoFuzzer(t *testing.T) { + x := 1 + y := 0 + + expectedVal := "div by 0" + + assert.PanicsWithValue(t, expectedVal, func() { + _ = DivOrPanic(x, y) + }) +} + +func TestExtendedByUtGoFuzzer1(t *testing.T) { + a := int64(1) + b := int64(3) + + actualVal0, actualVal1, actualVal2 := Extended(a, b) + + expectedVal0 := int64(1) + expectedVal1 := int64(1) + expectedVal2 := int64(0) + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal0, actualVal0) + assertMultiple.Equal(expectedVal1, actualVal1) + assertMultiple.Equal(expectedVal2, actualVal2) +} + +func TestExtendedByUtGoFuzzer2(t *testing.T) { + a := int64(0) + b := int64(0) + + actualVal0, actualVal1, actualVal2 := Extended(a, b) + + expectedVal0 := int64(0) + expectedVal1 := int64(0) + expectedVal2 := int64(1) + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal0, actualVal0) + assertMultiple.Equal(expectedVal1, actualVal1) + assertMultiple.Equal(expectedVal2, actualVal2) +} + +func TestArraySumByUtGoFuzzer(t *testing.T) { + array := [5]int{1, 1, 0, 0, 0} + + actualVal := ArraySum(array) + + expectedVal := 2 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestGenerateArrayOfIntegersByUtGoFuzzer(t *testing.T) { + num := 0 + + actualVal := GenerateArrayOfIntegers(num) + + expectedVal := [10]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestDistanceBetweenTwoPointsByUtGoFuzzer(t *testing.T) { + a := Point{x: 2.0, y: 2.0} + b := Point{x: 2.0, y: 2.0} + + actualVal := DistanceBetweenTwoPoints(a, b) + + expectedVal := 0.0 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestGetCoordinatesOfMiddleBetweenTwoPointsByUtGoFuzzer(t *testing.T) { + a := Point{x: 2.0, y: 2.0} + b := Point{x: 2.0, y: 2.0} + + actualVal0, actualVal1 := GetCoordinatesOfMiddleBetweenTwoPoints(a, b) + + expectedVal0 := 2.0 + expectedVal1 := 2.0 + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal0, actualVal0) + assertMultiple.Equal(expectedVal1, actualVal1) +} + +func TestGetCoordinateSumOfPointsByUtGoFuzzer1(t *testing.T) { + points := ([]Point)(nil) + + actualVal0, actualVal1 := GetCoordinateSumOfPoints(points) + + expectedVal0 := 0.0 + expectedVal1 := 0.0 + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal0, actualVal0) + assertMultiple.Equal(expectedVal1, actualVal1) +} + +func TestGetCoordinateSumOfPointsByUtGoFuzzer2(t *testing.T) { + points := []Point{{}} + + actualVal0, actualVal1 := GetCoordinateSumOfPoints(points) + + expectedVal0 := 0.0 + expectedVal1 := 0.0 + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal0, actualVal0) + assertMultiple.Equal(expectedVal1, actualVal1) +} + +func TestGetAreaOfCircleByUtGoFuzzer(t *testing.T) { + circle := Circle{Center: Point{x: 2.0, y: 2.0}, Radius: 2.0} + + actualVal := GetAreaOfCircle(circle) + + expectedVal := 12.566370614359172 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestIsIdentityByUtGoFuzzer1(t *testing.T) { + matrix := [3][3]int{{3, 0, 3}, {1, 0, 1}, {0, 0, 0}} + + actualVal := IsIdentity(matrix) + + assert.False(t, actualVal) +} + +func TestIsIdentityByUtGoFuzzer2(t *testing.T) { + matrix := [3][3]int{{1, 3, 0}, {0, 3, 0}, {0, 0, 3}} + + actualVal := IsIdentity(matrix) + + assert.False(t, actualVal) +} + +func TestIsIdentityByUtGoFuzzer3(t *testing.T) { + matrix := [3][3]int{{1, 0, 0}, {0, 0, 3}, {0, 3, 1}} + + actualVal := IsIdentity(matrix) + + assert.False(t, actualVal) +} + +func TestIsIdentityByUtGoFuzzer4(t *testing.T) { + matrix := [3][3]int{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}} + + actualVal := IsIdentity(matrix) + + assert.True(t, actualVal) +} + +func TestBinaryWithNonNilErrorByUtGoFuzzer1(t *testing.T) { + array := ([]int)(nil) + target := 2 + lowIndex := 2 + highIndex := 1 + + actualVal, actualErr := Binary(array, target, lowIndex, highIndex) + + expectedVal := -1 + expectedErrorMessage := "target not found in array" + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal, actualVal) + assertMultiple.ErrorContains(actualErr, expectedErrorMessage) +} + +func TestBinaryWithNonNilErrorByUtGoFuzzer2(t *testing.T) { + array := []int{1} + target := 2 + lowIndex := 0 + highIndex := 0 + + actualVal, actualErr := Binary(array, target, lowIndex, highIndex) + + expectedVal := -1 + expectedErrorMessage := "target not found in array" + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal, actualVal) + assertMultiple.ErrorContains(actualErr, expectedErrorMessage) +} + +func TestBinaryWithNonNilErrorByUtGoFuzzer3(t *testing.T) { + array := []int{1} + target := 0 + lowIndex := 0 + highIndex := 0 + + actualVal, actualErr := Binary(array, target, lowIndex, highIndex) + + expectedVal := -1 + expectedErrorMessage := "target not found in array" + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal, actualVal) + assertMultiple.ErrorContains(actualErr, expectedErrorMessage) +} + +func TestBinaryWithNonNilErrorByUtGoFuzzer4(t *testing.T) { + array := []int{1, 33554433} + target := 16385 + lowIndex := 0 + highIndex := 1 + + actualVal, actualErr := Binary(array, target, lowIndex, highIndex) + + expectedVal := -1 + expectedErrorMessage := "target not found in array" + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal, actualVal) + assertMultiple.ErrorContains(actualErr, expectedErrorMessage) +} + +func TestBinaryByUtGoFuzzer5(t *testing.T) { + array := []int{0} + target := 0 + lowIndex := 0 + highIndex := 0 + + actualVal, actualErr := Binary(array, target, lowIndex, highIndex) + + expectedVal := 0 + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal, actualVal) + assertMultiple.Nil(actualErr) +} + +func TestBinaryByUtGoFuzzer6(t *testing.T) { + array := []int{1, 65} + target := 1 + lowIndex := 0 + highIndex := 2 + + actualVal, actualErr := Binary(array, target, lowIndex, highIndex) + + expectedVal := 0 + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal, actualVal) + assertMultiple.Nil(actualErr) +} + +func TestBinaryByUtGoFuzzer7(t *testing.T) { + array := []int{0, 1, 2} + target := 1 + lowIndex := -1 + highIndex := 2 + + actualVal, actualErr := Binary(array, target, lowIndex, highIndex) + + expectedVal := 1 + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal, actualVal) + assertMultiple.Nil(actualErr) +} + +func TestBinaryByUtGoFuzzer8(t *testing.T) { + array := []int{-9151296850630803456, 0, 70} + target := 0 + lowIndex := -1 + highIndex := 5 + + actualVal, actualErr := Binary(array, target, lowIndex, highIndex) + + expectedVal := 1 + + assertMultiple := assert.New(t) + assertMultiple.Equal(expectedVal, actualVal) + assertMultiple.Nil(actualErr) +} + +func TestBinaryPanicsByUtGoFuzzer1(t *testing.T) { + array := ([]int)(nil) + target := 0 + lowIndex := 1 + highIndex := 1 + + expectedErrorMessage := "runtime error: index out of range [1] with length 0" + + assert.PanicsWithError(t, expectedErrorMessage, func() { + _, _ = Binary(array, target, lowIndex, highIndex) + }) +} + +func TestBinaryPanicsByUtGoFuzzer2(t *testing.T) { + array := []int{2} + target := 1 + lowIndex := -1 + highIndex := 1 + + expectedErrorMessage := "runtime error: index out of range [-1]" + + assert.PanicsWithError(t, expectedErrorMessage, func() { + _, _ = Binary(array, target, lowIndex, highIndex) + }) +} + +func TestBinaryPanicsByUtGoFuzzer3(t *testing.T) { + array := []int{0} + target := 2 + lowIndex := 0 + highIndex := 1 + + expectedErrorMessage := "runtime error: index out of range [1] with length 1" + + assert.PanicsWithError(t, expectedErrorMessage, func() { + _, _ = Binary(array, target, lowIndex, highIndex) + }) +} + +func TestStringSearchByUtGoFuzzer1(t *testing.T) { + str := "" + + actualVal := StringSearch(str) + + assert.False(t, actualVal) +} + +func TestStringSearchByUtGoFuzzer2(t *testing.T) { + str := "￸" + + actualVal := StringSearch(str) + + assert.False(t, actualVal) +} + +func TestStringSearchByUtGoFuzzer3(t *testing.T) { + str := "Aa." + + actualVal := StringSearch(str) + + assert.False(t, actualVal) +} + +func TestStringSearchByUtGoFuzzer4(t *testing.T) { + str := "AB~" + + actualVal := StringSearch(str) + + assert.False(t, actualVal) +} + +func TestStringSearchByUtGoFuzzer5(t *testing.T) { + str := "ABC" + + actualVal := StringSearch(str) + + assert.True(t, actualVal) +} + +func TestSumOfChanElementsByUtGoFuzzer1(t *testing.T) { + c := make(chan int, 1) + c <- 0 + close(c) + + actualVal := SumOfChanElements(c) + + expectedVal := 0 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestSumOfChanElementsByUtGoFuzzer2(t *testing.T) { + c := make(chan int, 0) + close(c) + + actualVal := SumOfChanElements(c) + + expectedVal := 0 + + assert.Equal(t, expectedVal, actualVal) +} + +// SumOfChanElements((<-chan int)(nil)) exceeded 1000 ms timeout + +func TestLenOfListByUtGoFuzzer1(t *testing.T) { + l := (*List)(nil) + + actualVal := LenOfList(l) + + expectedVal := 0 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestLenOfListByUtGoFuzzer2(t *testing.T) { + l := &List{val: 1} + + actualVal := LenOfList(l) + + expectedVal := 1 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestLenOfListByUtGoFuzzer3(t *testing.T) { + l := &List{tail: &List{}} + + actualVal := LenOfList(l) + + expectedVal := 2 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestGetLastNodeByUtGoFuzzer1(t *testing.T) { + n := (*Node)(nil) + + actualVal := GetLastNode(n) + + assert.Nil(t, actualVal) +} + +func TestGetLastNodeByUtGoFuzzer2(t *testing.T) { + n := &Node{val: 1} + + actualVal := GetLastNode(n) + + expectedVal := &Node{val: 1} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestGetLastNodeByUtGoFuzzer3(t *testing.T) { + n := &Node{next: &Node{}} + + actualVal := GetLastNode(n) + + expectedVal := &Node{} + + assert.Equal(t, expectedVal, actualVal) +} diff --git a/utbot-go/go-samples/simple/supported_types.go b/utbot-go/go-samples/simple/supported_types.go new file mode 100644 index 0000000000..ebd0142206 --- /dev/null +++ b/utbot-go/go-samples/simple/supported_types.go @@ -0,0 +1,425 @@ +package simple + +import ( + "errors" + "fmt" + "github.com/pmezard/go-difflib/difflib" + dif "github.com/pmezard/go-difflib/difflib" + "go-samples/simple/nested" + "math" +) + +func WithoutParametersAndReturnValues() { + print("Hello World") +} + +func Int(n int) int { + if n < 0 { + return -n + } + return n +} + +func Int8(n int8) int8 { + if n < 0 { + return -n + } + return n +} + +func Int16(n int16) int16 { + if n < 0 { + return -n + } + return n +} + +func Int32(n int32) int32 { + if n < 0 { + return -n + } + return n +} + +func Int64(n int64) int64 { + if n < 0 { + return -n + } + return n +} + +func Uint(n uint) uint { + return n +} + +func Uint8(n uint8) uint8 { + return n +} + +func Uint16(n uint16) uint16 { + return n +} + +func Uint32(n uint32) uint32 { + return n +} + +func Uint64(n uint64) uint64 { + return n +} + +func UintPtr(n uintptr) uintptr { + return n +} + +func Float32(n float32) float32 { + if math.IsInf(float64(n), 1) { + return n + } else if math.IsInf(float64(n), -1) { + return n + } else if math.IsNaN(float64(n)) { + return n + } + return n +} + +func Float64(n float64) float64 { + if math.IsInf(n, 1) { + return n + } else if math.IsInf(n, -1) { + return n + } else if math.IsNaN(n) { + return n + } + return n +} + +func Complex64(n complex64) complex64 { + return n +} + +func Complex128(n complex128) complex128 { + return n +} + +func Byte(n byte) byte { + return n +} + +func Rune(n rune) rune { + return n +} + +func String(n string) string { + return n +} + +func Bool(n bool) bool { + return n +} + +type Structure struct { + int + int8 + int16 + int32 + int64 + uint + uint8 + uint16 + uint32 + uint64 + uintptr + float32 + float64 + complex64 + complex128 + byte + rune + string + bool +} + +func Struct(s Structure) Structure { + return s +} + +func StructWithNan(s Structure) Structure { + s.float64 = math.NaN() + return s +} + +func ArrayOfInt(array [10]int) [10]int { + return array +} + +func ArrayOfUintPtr(array [10]uintptr) [10]uintptr { + return array +} + +func ArrayOfString(array [10]string) [10]string { + return array +} + +func ArrayOfStructs(array [10]Structure) [10]Structure { + return array +} + +func ArrayOfStructsWithNan(array [10]Structure) [10]Structure { + array[0].float64 = math.NaN() + return array +} + +func ArrayOfArrayOfUint(array [5][5]uint) [5][5]uint { + return array +} + +func ArrayOfArrayOfStructs(array [5][5]Structure) [5][5]Structure { + return array +} + +func ArrayOfSliceOfUint(array [5][]uint) [5][]uint { + return array +} + +func returnErrorOrNil(n int) error { + if n > 0 { + return errors.New("error") + } else { + return nil + } +} + +func ExternalStruct(match difflib.Match, structure Structure) Structure { + return structure +} + +func ExternalStructWithAlias(match dif.Match) difflib.Match { + return match +} + +func SliceOfInt(slice []int) []int { + if slice == nil { + return slice + } + return slice +} + +func SliceOfUintPtr(slice []uintptr) []uintptr { + if slice == nil { + return slice + } + return slice +} + +func SliceOfString(slice []string) []string { + if slice == nil { + return slice + } + return slice +} + +func SliceOfStructs(slice []Structure) []Structure { + if slice == nil { + return slice + } + return slice +} + +func SliceOfStructsWithNan(slice []Structure) []Structure { + if slice == nil { + return slice + } + slice[0].float64 = math.NaN() + return slice +} + +func SliceOfSliceOfByte(slice [][]byte) [][]byte { + if slice == nil { + return slice + } + return slice +} + +func SliceOfSliceOfStructs(slice [][]Structure) [][]Structure { + if slice == nil { + return slice + } + return slice +} + +func SliceOfArrayOfInt(slice [][5]int) [][5]int { + if slice == nil { + return slice + } + return slice +} + +func ExportedStructWithEmbeddedUnexportedStruct(exportedStruct nested.ExportedStruct) nested.ExportedStruct { + return exportedStruct +} + +type Type byte + +func NamedType(n Type) Type { + return n +} + +func ArrayOfNamedType(array [5]Type) [5]Type { + return array +} + +type T [5][5]Type + +func ArrayOfArrayOfNamedType(array [5][5]Type) T { + return array +} + +func SliceOfNamedType(slice []Type) []Type { + if slice == nil { + return slice + } + return slice +} + +type NA [5]uintptr + +func NamedArray(array NA) NA { + return array +} + +type NS []int + +func NamedSlice(slice NS) NS { + if slice == nil { + return slice + } + return slice +} + +type S struct { + t Type + T + n NA + NS +} + +func StructWithFieldsOfNamedTypes(s S) S { + return s +} + +func Map(table map[string]int) map[string]int { + return table +} + +func MapOfStructures(table map[Structure]Structure) map[Structure]Structure { + return table +} + +func MapOfSliceOfInt(table map[string][]int) map[string][]int { + return table +} + +func MapOfNamedType(table map[int]Type) map[int]Type { + return table +} + +func MapOfNamedSlice(table map[uint]NS) map[uint]NS { + return table +} + +type NM map[string]NA + +func NamedMap(n NM) NM { + return n +} + +func Channel(c chan Structure) { + if c == nil { + return + } +} + +func SendOnlyChannel(c chan<- int) { + if c == nil { + return + } +} + +func RecvOnlyChannel(c <-chan NM) { + if c == nil { + return + } +} + +func PointerToInt(n *int) *int { + if n == nil { + return n + } + return n +} + +func PointerToSlice(n *[]int) *[]int { + if n == nil { + return n + } + return n +} + +func PointerToArray(n *[3]int) *[3]int { + if n == nil { + return n + } + return n +} + +func PointerToMap(n *map[string]int) *map[string]int { + if n == nil { + return n + } + return n +} + +func PointerToStructure(n *Structure) *Structure { + if n == nil { + return n + } + return n +} + +func PointerToNamedType(n *Type) *Type { + if n == nil { + return n + } + return n +} + +type Node struct { + prev, next *Node + val int +} + +func PointerToRecursiveStruct(n *Node) *Node { + if n == nil { + return n + } + return n +} + +type I interface { + String() string +} + +func Interface(i I) { + if i != nil { + return + } + return +} + +func ExternalInterface(i fmt.Stringer) { + if i != nil { + return + } + return +} diff --git a/utbot-go/go-samples/simple/supported_types_go_ut_test.go b/utbot-go/go-samples/simple/supported_types_go_ut_test.go new file mode 100644 index 0000000000..3a5fcf35b5 --- /dev/null +++ b/utbot-go/go-samples/simple/supported_types_go_ut_test.go @@ -0,0 +1,941 @@ +package simple + +import ( + "fmt" + "github.com/pmezard/go-difflib/difflib" + "github.com/stretchr/testify/assert" + "go-samples/simple/nested" + "math" + "testing" + "time" +) + +func TestWithoutParametersAndReturnValuesByUtGoFuzzer(t *testing.T) { + assert.NotPanics(t, func() { + WithoutParametersAndReturnValues() + }) +} + +func TestIntByUtGoFuzzer1(t *testing.T) { + n := 4 + + actualVal := Int(n) + + expectedVal := 4 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestIntByUtGoFuzzer2(t *testing.T) { + n := -1 + + actualVal := Int(n) + + expectedVal := 1 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestInt8ByUtGoFuzzer1(t *testing.T) { + n := int8(1) + + actualVal := Int8(n) + + expectedVal := int8(1) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestInt8ByUtGoFuzzer2(t *testing.T) { + n := int8(-3) + + actualVal := Int8(n) + + expectedVal := int8(3) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestInt16ByUtGoFuzzer1(t *testing.T) { + n := int16(2) + + actualVal := Int16(n) + + expectedVal := int16(2) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestInt16ByUtGoFuzzer2(t *testing.T) { + n := int16(-9) + + actualVal := Int16(n) + + expectedVal := int16(9) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestInt32ByUtGoFuzzer1(t *testing.T) { + n := int32(2) + + actualVal := Int32(n) + + expectedVal := int32(2) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestInt32ByUtGoFuzzer2(t *testing.T) { + n := int32(-3) + + actualVal := Int32(n) + + expectedVal := int32(3) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestInt64ByUtGoFuzzer1(t *testing.T) { + n := int64(-1) + + actualVal := Int64(n) + + expectedVal := int64(1) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestInt64ByUtGoFuzzer2(t *testing.T) { + n := int64(1) + + actualVal := Int64(n) + + expectedVal := int64(1) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestUintByUtGoFuzzer(t *testing.T) { + n := uint(5) + + actualVal := Uint(n) + + expectedVal := uint(5) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestUint8ByUtGoFuzzer(t *testing.T) { + n := uint8(2) + + actualVal := Uint8(n) + + expectedVal := uint8(2) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestUint16ByUtGoFuzzer(t *testing.T) { + n := uint16(1) + + actualVal := Uint16(n) + + expectedVal := uint16(1) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestUint32ByUtGoFuzzer(t *testing.T) { + n := uint32(8) + + actualVal := Uint32(n) + + expectedVal := uint32(8) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestUint64ByUtGoFuzzer(t *testing.T) { + n := uint64(4) + + actualVal := Uint64(n) + + expectedVal := uint64(4) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestUintPtrByUtGoFuzzer(t *testing.T) { + n := uintptr(4) + + actualVal := UintPtr(n) + + expectedVal := uintptr(4) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestFloat32ByUtGoFuzzer1(t *testing.T) { + n := float32(0.143228) + + actualVal := Float32(n) + + expectedVal := float32(0.143228) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestFloat32ByUtGoFuzzer2(t *testing.T) { + n := float32(math.Inf(-1)) + + actualVal := Float32(n) + + assert.True(t, math.IsInf(float64(actualVal), -1)) +} + +func TestFloat32ByUtGoFuzzer3(t *testing.T) { + n := float32(math.Inf(1)) + + actualVal := Float32(n) + + assert.True(t, math.IsInf(float64(actualVal), 1)) +} + +func TestFloat64ByUtGoFuzzer1(t *testing.T) { + n := 0.61258062787271 + + actualVal := Float64(n) + + expectedVal := 0.61258062787271 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestFloat64ByUtGoFuzzer2(t *testing.T) { + n := math.Inf(1) + + actualVal := Float64(n) + + assert.True(t, math.IsInf(actualVal, 1)) +} + +func TestFloat64ByUtGoFuzzer3(t *testing.T) { + n := math.Inf(-1) + + actualVal := Float64(n) + + assert.True(t, math.IsInf(actualVal, -1)) +} + +func TestComplex64ByUtGoFuzzer(t *testing.T) { + n := complex(float32(math.Inf(1)), float32(0.415752)) + + actualVal := Complex64(n) + + expectedVal := complex(float32(math.Inf(1)), float32(0.415752)) + + assertMultiple := assert.New(t) + assertMultiple.Equal(real(expectedVal), real(actualVal)) + assertMultiple.Equal(imag(expectedVal), imag(actualVal)) +} + +func TestComplex128ByUtGoFuzzer(t *testing.T) { + n := complex(0.2112353749298962, math.Inf(1)) + + actualVal := Complex128(n) + + expectedVal := complex(0.2112353749298962, math.Inf(1)) + + assertMultiple := assert.New(t) + assertMultiple.Equal(real(expectedVal), real(actualVal)) + assertMultiple.Equal(imag(expectedVal), imag(actualVal)) +} + +func TestByteByUtGoFuzzer(t *testing.T) { + n := byte(1) + + actualVal := Byte(n) + + expectedVal := byte(1) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestRuneByUtGoFuzzer(t *testing.T) { + n := rune(1) + + actualVal := Rune(n) + + expectedVal := rune(1) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestStringByUtGoFuzzer(t *testing.T) { + n := "" + + actualVal := String(n) + + expectedVal := "" + + assert.Equal(t, expectedVal, actualVal) +} + +func TestBoolByUtGoFuzzer(t *testing.T) { + n := true + + actualVal := Bool(n) + + assert.True(t, actualVal) +} + +func TestStructByUtGoFuzzer(t *testing.T) { + s := Structure{int32: int32(1), int64: int64(1), uint8: uint8(1), uint16: uint16(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.4714877), float64: 0.501834850205735, complex64: complex(float32(0.4714877), float32(0.4714877)), complex128: complex(0.501834850205735, 0.501834850205735), byte: byte(1)} + + actualVal := Struct(s) + + expectedVal := Structure{int32: int32(1), int64: int64(1), uint8: uint8(1), uint16: uint16(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.4714877), float64: 0.501834850205735, complex64: complex(float32(0.4714877), float32(0.4714877)), complex128: complex(0.501834850205735, 0.501834850205735), byte: byte(1)} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestStructWithNanByUtGoFuzzer(t *testing.T) { + s := Structure{int8: int8(1), int16: int16(-1), int32: int32(-1), int64: int64(1), uint8: uint8(1), uint16: uint16(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.63904), float64: 0.2555253108964435, complex64: complex(float32(0.63904), float32(0.63904)), complex128: complex(0.2555253108964435, 0.2555253108964435), byte: byte(255), rune: rune(-1)} + + actualVal := StructWithNan(s) + + expectedVal := Structure{int8: int8(1), int16: int16(-1), int32: int32(-1), int64: int64(1), uint8: uint8(1), uint16: uint16(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.63904), float64: math.NaN(), complex64: complex(float32(0.63904), float32(0.63904)), complex128: complex(0.2555253108964435, 0.2555253108964435), byte: byte(255), rune: rune(-1)} + + assert.NotEqual(t, expectedVal, actualVal) +} + +func TestArrayOfIntByUtGoFuzzer(t *testing.T) { + array := [10]int{0, 1, 1, 0, 1, 1, 0, 64, 0, 1} + + actualVal := ArrayOfInt(array) + + expectedVal := [10]int{0, 1, 1, 0, 1, 1, 0, 64, 0, 1} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestArrayOfUintPtrByUtGoFuzzer(t *testing.T) { + array := [10]uintptr{1, 0, 1, 0, 1, 1, 0, 0, 1, 0} + + actualVal := ArrayOfUintPtr(array) + + expectedVal := [10]uintptr{1, 0, 1, 0, 1, 1, 0, 0, 1, 0} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestArrayOfStringByUtGoFuzzer(t *testing.T) { + array := [10]string{"", "", "", "", "", "", "", "", "", ""} + + actualVal := ArrayOfString(array) + + expectedVal := [10]string{"", "", "", "", "", "", "", "", "", ""} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestArrayOfStructsByUtGoFuzzer(t *testing.T) { + array := [10]Structure{{int8: int8(-1), int16: int16(-1), int32: int32(1), uint: uint(1), uint8: uint8(255), uint16: uint16(1), uint32: uint32(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1), rune: rune(1), string: "hello"}, {int: -9223372036854775808, int8: int8(-1), int16: int16(1), int32: int32(1), int64: int64(-1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(1), uint64: uint64(18446744073709551615), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1), string: "hello"}, {int: -1, int8: int8(1), int16: int16(-1), int32: int32(-1), int64: int64(1), uint: uint(18446744073709551615), uint32: uint32(1), uint64: uint64(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(255), rune: rune(1), string: "hello", bool: true}, {int: 9223372036854775807, int8: int8(1), int16: int16(-1), int32: int32(-1), int64: int64(-9223372036854775808), uint8: uint8(1), uint16: uint16(65535), uint32: uint32(4294967295), uintptr: uintptr(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1)}, {int: -9223372036854775808, int8: int8(1), int16: int16(-1), int32: int32(-1), int64: int64(-1), uint: uint(1), uint8: uint8(1), uint32: uint32(1), uintptr: uintptr(18446744073709551615), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1), rune: rune(-2147483648)}, {int: -1, int16: int16(1), int32: int32(1), int64: int64(1), uint: uint(1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(4294967295), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(255), rune: rune(1), string: "hello", bool: true}, {int: -9223372036854775808, int16: int16(1), int64: int64(1), uint: uint(1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1), rune: rune(2147483647), string: "hello"}, {int: 1, int8: int8(1), int16: int16(32767), int32: int32(2147483647), int64: int64(-9223372036854775808), uint: uint(1), uint8: uint8(255), uint16: uint16(65535), uint32: uint32(1), uint64: uint64(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(255), rune: rune(-1), string: "hello", bool: true}, {int8: int8(127), int64: int64(-1), uint: uint(1), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), string: "hello"}, {int: -9223372036854775808, int8: int8(-1), int16: int16(32767), int32: int32(1), int64: int64(-1), uint: uint(18446744073709551615), uint8: uint8(255), uint32: uint32(1), uintptr: uintptr(18446744073709551615), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), rune: rune(2147483647), bool: true}} + + actualVal := ArrayOfStructs(array) + + expectedVal := [10]Structure{{int8: int8(-1), int16: int16(-1), int32: int32(1), uint: uint(1), uint8: uint8(255), uint16: uint16(1), uint32: uint32(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1), rune: rune(1), string: "hello"}, {int: -9223372036854775808, int8: int8(-1), int16: int16(1), int32: int32(1), int64: int64(-1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(1), uint64: uint64(18446744073709551615), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1), string: "hello"}, {int: -1, int8: int8(1), int16: int16(-1), int32: int32(-1), int64: int64(1), uint: uint(18446744073709551615), uint32: uint32(1), uint64: uint64(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(255), rune: rune(1), string: "hello", bool: true}, {int: 9223372036854775807, int8: int8(1), int16: int16(-1), int32: int32(-1), int64: int64(-9223372036854775808), uint8: uint8(1), uint16: uint16(65535), uint32: uint32(4294967295), uintptr: uintptr(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1)}, {int: -9223372036854775808, int8: int8(1), int16: int16(-1), int32: int32(-1), int64: int64(-1), uint: uint(1), uint8: uint8(1), uint32: uint32(1), uintptr: uintptr(18446744073709551615), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1), rune: rune(-2147483648)}, {int: -1, int16: int16(1), int32: int32(1), int64: int64(1), uint: uint(1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(4294967295), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(255), rune: rune(1), string: "hello", bool: true}, {int: -9223372036854775808, int16: int16(1), int64: int64(1), uint: uint(1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(1), rune: rune(2147483647), string: "hello"}, {int: 1, int8: int8(1), int16: int16(32767), int32: int32(2147483647), int64: int64(-9223372036854775808), uint: uint(1), uint8: uint8(255), uint16: uint16(65535), uint32: uint32(1), uint64: uint64(1), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), byte: byte(255), rune: rune(-1), string: "hello", bool: true}, {int8: int8(127), int64: int64(-1), uint: uint(1), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), string: "hello"}, {int: -9223372036854775808, int8: int8(-1), int16: int16(32767), int32: int32(1), int64: int64(-1), uint: uint(18446744073709551615), uint8: uint8(255), uint32: uint32(1), uintptr: uintptr(18446744073709551615), float32: float32(0.5316085), float64: 0.3025369185032576, complex64: complex(float32(0.5316085), float32(0.5316085)), complex128: complex(0.3025369185032576, 0.3025369185032576), rune: rune(2147483647), bool: true}} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestArrayOfStructsWithNanByUtGoFuzzer(t *testing.T) { + array := [10]Structure{{int: -9223372036854775808, int8: int8(-128), int16: int16(-1), int32: int32(-1), int64: int64(-9223372036854775808), uint: uint(18446744073709551615), uint8: uint8(1), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(-2147483648), bool: true}, {int: -1, int16: int16(-32768), int32: int32(2147483647), uint8: uint8(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754)}, {int: -1, int16: int16(-1), int64: int64(1), uint8: uint8(1), uint32: uint32(4294967295), uintptr: uintptr(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(2147483647), string: "hello", bool: true}, {int16: int16(-1), uint: uint(18446744073709551615), uint16: uint16(65535), uint32: uint32(4294967295), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(1), rune: rune(-2147483648)}, {int8: int8(1), int16: int16(-1), int32: int32(2147483647), uint: uint(18446744073709551615), uint32: uint32(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(1)}, {int32: int32(-1), int64: int64(1), uint8: uint8(1), uint16: uint16(1), uintptr: uintptr(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), rune: rune(2147483647)}, {int: -9223372036854775808, int8: int8(-128), int16: int16(32767), int64: int64(1), uint: uint(18446744073709551615), uint16: uint16(65535), uint32: uint32(1), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(-1), bool: true}, {int: 1, int8: int8(127), int16: int16(32767), int32: int32(1), int64: int64(-1), uint: uint(18446744073709551615), uint8: uint8(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(1), rune: rune(2147483647), string: "hello", bool: true}, {int: 1, int8: int8(127), int32: int32(1), uint: uint(18446744073709551615), uint8: uint8(1), uint64: uint64(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(1), string: "hello"}, {int: -9223372036854775808, int16: int16(32767), int32: int32(-2147483648), int64: int64(-1), uint: uint(1), uint16: uint16(65535), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), rune: rune(1)}} + + actualVal := ArrayOfStructsWithNan(array) + + expectedVal := [10]Structure{{int: -9223372036854775808, int8: int8(-128), int16: int16(-1), int32: int32(-1), int64: int64(-9223372036854775808), uint: uint(18446744073709551615), uint8: uint8(1), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.9086392), float64: math.NaN(), complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(-2147483648), bool: true}, {int: -1, int16: int16(-32768), int32: int32(2147483647), uint8: uint8(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754)}, {int: -1, int16: int16(-1), int64: int64(1), uint8: uint8(1), uint32: uint32(4294967295), uintptr: uintptr(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(2147483647), string: "hello", bool: true}, {int16: int16(-1), uint: uint(18446744073709551615), uint16: uint16(65535), uint32: uint32(4294967295), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(1), rune: rune(-2147483648)}, {int8: int8(1), int16: int16(-1), int32: int32(2147483647), uint: uint(18446744073709551615), uint32: uint32(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(1)}, {int32: int32(-1), int64: int64(1), uint8: uint8(1), uint16: uint16(1), uintptr: uintptr(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), rune: rune(2147483647)}, {int: -9223372036854775808, int8: int8(-128), int16: int16(32767), int64: int64(1), uint: uint(18446744073709551615), uint16: uint16(65535), uint32: uint32(1), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(-1), bool: true}, {int: 1, int8: int8(127), int16: int16(32767), int32: int32(1), int64: int64(-1), uint: uint(18446744073709551615), uint8: uint8(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(1), rune: rune(2147483647), string: "hello", bool: true}, {int: 1, int8: int8(127), int32: int32(1), uint: uint(18446744073709551615), uint8: uint8(1), uint64: uint64(1), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), byte: byte(255), rune: rune(1), string: "hello"}, {int: -9223372036854775808, int16: int16(32767), int32: int32(-2147483648), int64: int64(-1), uint: uint(1), uint16: uint16(65535), float32: float32(0.9086392), float64: 0.977378020386754, complex64: complex(float32(0.9086392), float32(0.9086392)), complex128: complex(0.977378020386754, 0.977378020386754), rune: rune(1)}} + + assert.NotEqual(t, expectedVal, actualVal) +} + +func TestArrayOfArrayOfUintByUtGoFuzzer(t *testing.T) { + array := [5][5]uint{{0, 1, 0, 1, 1}, {1, 1, 0, 0, 0}, {1, 1, 0, 0, 0}, {0, 1, 0, 0, 18446744073709551615}, {1, 35184372088832, 1, 1, 0}} + + actualVal := ArrayOfArrayOfUint(array) + + expectedVal := [5][5]uint{{0, 1, 0, 1, 1}, {1, 1, 0, 0, 0}, {1, 1, 0, 0, 0}, {0, 1, 0, 0, 18446744073709551615}, {1, 35184372088832, 1, 1, 0}} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestArrayOfArrayOfStructsByUtGoFuzzer(t *testing.T) { + array := [5][5]Structure{{{int: -9223372036854775808, int8: int8(-128), int16: int16(-32768), int32: int32(2147483647), int64: int64(1), uint64: uint64(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), rune: rune(1)}, {int8: int8(-1), int16: int16(-32768), int64: int64(9223372036854775807), uint8: uint8(255), uint16: uint16(65535), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(255), rune: rune(-1), bool: true}, {int: 1, int8: int8(-1), int16: int16(32767), int32: int32(2147483647), uint: uint(18446744073709551615), uint8: uint8(1), uint16: uint16(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), rune: rune(1), bool: true}, {int8: int8(127), int16: int16(-1), int32: int32(-1), int64: int64(-1), uint: uint(18446744073709551615), uint8: uint8(1), uint32: uint32(4294967295), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(255), rune: rune(-2147483648)}, {int: 1, int8: int8(-128), int16: int16(32767), int64: int64(-1), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), rune: rune(-1), string: "hello"}}, {{int: -9223372036854775808, int8: int8(127), int16: int16(-1), int32: int32(1), int64: int64(1), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(65535), uint64: uint64(18446744073709551615), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), rune: rune(1), string: "hello"}, {int: -1, int8: int8(127), int16: int16(-1), int32: int32(2147483647), int64: int64(9223372036854775807), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(65535), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), rune: rune(-2147483648), bool: true}, {int: -9223372036854775808, int8: int8(-128), int16: int16(32767), int32: int32(1), int64: int64(1), uint: uint(1), uint16: uint16(65535), uint32: uint32(4294967295), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(255), rune: rune(-1), bool: true}, {int: -1, int8: int8(1), int32: int32(-2147483648), int64: int64(1), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(65535), uint32: uint32(1), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), string: "hello"}, {int: 1, int8: int8(-1), int16: int16(32767), int64: int64(-9223372036854775808), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(65535), uint32: uint32(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), rune: rune(2147483647), string: "hello", bool: true}}, {{int: -9223372036854775808, int8: int8(1), int16: int16(1), int64: int64(1), uint: uint(1), uint16: uint16(65535), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(255), rune: rune(1)}, {int: 1, int8: int8(127), int16: int16(-1), int32: int32(1), int64: int64(-9223372036854775808), uint: uint(1), uint8: uint8(255), uint16: uint16(1), uint32: uint32(4294967295), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), rune: rune(2147483647), string: "hello"}, {int: 1, int8: int8(-128), int32: int32(-1), int64: int64(-1), uint: uint(18446744073709551615), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), rune: rune(2147483647), string: "hello"}, {int: -1, int16: int16(1), int32: int32(1), uint: uint(1), uint16: uint16(65535), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0))}, {int8: int8(-1), int16: int16(32767), int32: int32(-2147483648), int64: int64(-1), uint: uint(1), uint8: uint8(255), uint16: uint16(1), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), rune: rune(1), bool: true}}, {{int: -1, int8: int8(1), int16: int16(1), int32: int32(2147483647), int64: int64(-1), uint: uint(1), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), bool: true}, {int: -9223372036854775808, int16: int16(32767), int32: int32(-1), int64: int64(9223372036854775807), uint16: uint16(65535), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), rune: rune(2147483647)}, {int: 1, int8: int8(127), int16: int16(1), int32: int32(-2147483648), int64: int64(-1), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(1), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), bool: true}, {int: -9223372036854775808, int16: int16(1), int32: int32(1), uint: uint(1), uint8: uint8(1), uint16: uint16(65535), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), rune: rune(1), string: "hello", bool: true}, {int: -1, int8: int8(1), int16: int16(-32768), int32: int32(2147483647), int64: int64(1), uint16: uint16(65535), uint32: uint32(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), rune: rune(-1), string: "hello"}}, {{int: -1, int8: int8(-1), int16: int16(-1), int32: int32(-2147483648), int64: int64(9223372036854775807), uint: uint(1), uint8: uint8(255), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), rune: rune(-2147483648), string: "hello", bool: true}, {int: 1, int8: int8(-128), int16: int16(32767), int32: int32(2147483647), uint8: uint8(255), uint32: uint32(4294967295), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(1), rune: rune(1), bool: true}, {int16: int16(32767), int32: int32(2147483647), int64: int64(-1), uint32: uint32(1), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(255), rune: rune(-2147483648), string: "hello", bool: true}, {int: 1, int8: int8(-128), int16: int16(1), int32: int32(-2147483648), int64: int64(1), uint8: uint8(255), uint16: uint16(1), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), byte: byte(255), rune: rune(1)}, {int: 9223372036854775807, int8: int8(1), int16: int16(-32768), int32: int32(-2147483648), int64: int64(-9223372036854775808), uint8: uint8(1), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, complex64: complex(float32(0.0), float32(0.0)), string: "hello", bool: true}}} + + actualVal := ArrayOfArrayOfStructs(array) + + expectedVal := [5][5]Structure{{{int: -9223372036854775808, int8: int8(-128), int16: int16(-32768), int32: int32(2147483647), int64: int64(1), uint64: uint64(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), rune: rune(1)}, {int8: int8(-1), int16: int16(-32768), int64: int64(9223372036854775807), uint8: uint8(255), uint16: uint16(65535), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(255), rune: rune(-1), bool: true}, {int: 1, int8: int8(-1), int16: int16(32767), int32: int32(2147483647), uint: uint(18446744073709551615), uint8: uint8(1), uint16: uint16(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), rune: rune(1), bool: true}, {int8: int8(127), int16: int16(-1), int32: int32(-1), int64: int64(-1), uint: uint(18446744073709551615), uint8: uint8(1), uint32: uint32(4294967295), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(255), rune: rune(-2147483648)}, {int: 1, int8: int8(-128), int16: int16(32767), int64: int64(-1), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, rune: rune(-1), string: "hello"}}, {{int: -9223372036854775808, int8: int8(127), int16: int16(-1), int32: int32(1), int64: int64(1), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(65535), uint64: uint64(18446744073709551615), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, rune: rune(1), string: "hello"}, {int: -1, int8: int8(127), int16: int16(-1), int32: int32(2147483647), int64: int64(9223372036854775807), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(65535), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), rune: rune(-2147483648), bool: true}, {int: -9223372036854775808, int8: int8(-128), int16: int16(32767), int32: int32(1), int64: int64(1), uint: uint(1), uint16: uint16(65535), uint32: uint32(4294967295), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(255), rune: rune(-1), bool: true}, {int: -1, int8: int8(1), int32: int32(-2147483648), int64: int64(1), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(65535), uint32: uint32(1), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, string: "hello"}, {int: 1, int8: int8(-1), int16: int16(32767), int64: int64(-9223372036854775808), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(65535), uint32: uint32(1), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), rune: rune(2147483647), string: "hello", bool: true}}, {{int: -9223372036854775808, int8: int8(1), int16: int16(1), int64: int64(1), uint: uint(1), uint16: uint16(65535), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(255), rune: rune(1)}, {int: 1, int8: int8(127), int16: int16(-1), int32: int32(1), int64: int64(-9223372036854775808), uint: uint(1), uint8: uint8(255), uint16: uint16(1), uint32: uint32(4294967295), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, rune: rune(2147483647), string: "hello"}, {int: 1, int8: int8(-128), int32: int32(-1), int64: int64(-1), uint: uint(18446744073709551615), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), rune: rune(2147483647), string: "hello"}, {int: -1, int16: int16(1), int32: int32(1), uint: uint(1), uint16: uint16(65535), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884}, {int8: int8(-1), int16: int16(32767), int32: int32(-2147483648), int64: int64(-1), uint: uint(1), uint8: uint8(255), uint16: uint16(1), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, rune: rune(1), bool: true}}, {{int: -1, int8: int8(1), int16: int16(1), int32: int32(2147483647), int64: int64(-1), uint: uint(1), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), bool: true}, {int: -9223372036854775808, int16: int16(32767), int32: int32(-1), int64: int64(9223372036854775807), uint16: uint16(65535), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), rune: rune(2147483647)}, {int: 1, int8: int8(127), int16: int16(1), int32: int32(-2147483648), int64: int64(-1), uint: uint(18446744073709551615), uint8: uint8(255), uint16: uint16(1), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), bool: true}, {int: -9223372036854775808, int16: int16(1), int32: int32(1), uint: uint(1), uint8: uint8(1), uint16: uint16(65535), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), rune: rune(1), string: "hello", bool: true}, {int: -1, int8: int8(1), int16: int16(-32768), int32: int32(2147483647), int64: int64(1), uint16: uint16(65535), uint32: uint32(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), rune: rune(-1), string: "hello"}}, {{int: -1, int8: int8(-1), int16: int16(-1), int32: int32(-2147483648), int64: int64(9223372036854775807), uint: uint(1), uint8: uint8(255), float32: float32(0.33522367), float64: 0.003913530617220884, rune: rune(-2147483648), string: "hello", bool: true}, {int: 1, int8: int8(-128), int16: int16(32767), int32: int32(2147483647), uint8: uint8(255), uint32: uint32(4294967295), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(1), rune: rune(1), bool: true}, {int16: int16(32767), int32: int32(2147483647), int64: int64(-1), uint32: uint32(1), uint64: uint64(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(255), rune: rune(-2147483648), string: "hello", bool: true}, {int: 1, int8: int8(-128), int16: int16(1), int32: int32(-2147483648), int64: int64(1), uint8: uint8(255), uint16: uint16(1), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.33522367), float64: 0.003913530617220884, byte: byte(255), rune: rune(1)}, {int: 9223372036854775807, int8: int8(1), int16: int16(-32768), int32: int32(-2147483648), int64: int64(-9223372036854775808), uint8: uint8(1), uint32: uint32(4294967295), uint64: uint64(1), uintptr: uintptr(18446744073709551615), float32: float32(0.33522367), float64: 0.003913530617220884, string: "hello", bool: true}}} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestArrayOfSliceOfUintByUtGoFuzzer(t *testing.T) { + array := [5][]uint{nil, nil, nil, nil, nil} + + actualVal := ArrayOfSliceOfUint(array) + + expectedVal := [5][]uint{nil, nil, nil, nil, nil} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestReturnErrorOrNilWithNonNilErrorByUtGoFuzzer1(t *testing.T) { + n := 4 + + actualErr := returnErrorOrNil(n) + + expectedErrorMessage := "error" + + assert.ErrorContains(t, actualErr, expectedErrorMessage) +} + +func TestReturnErrorOrNilByUtGoFuzzer2(t *testing.T) { + n := 0 + + actualErr := returnErrorOrNil(n) + + assert.Nil(t, actualErr) +} + +func TestExternalStructByUtGoFuzzer(t *testing.T) { + match := difflib.Match{B: 1, Size: -1} + structure := Structure{int8: int8(1), int16: int16(32767), int32: int32(-1), int64: int64(1), uint: uint(1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.37985808), float64: 0.4084964347010259, complex64: complex(float32(0.37985808), float32(0.37985808)), complex128: complex(0.4084964347010259, 0.4084964347010259), byte: byte(1), rune: rune(1), string: "hello"} + + actualVal := ExternalStruct(match, structure) + + expectedVal := Structure{int8: int8(1), int16: int16(32767), int32: int32(-1), int64: int64(1), uint: uint(1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(1), uintptr: uintptr(1), float32: float32(0.37985808), float64: 0.4084964347010259, complex64: complex(float32(0.37985808), float32(0.37985808)), complex128: complex(0.4084964347010259, 0.4084964347010259), byte: byte(1), rune: rune(1), string: "hello"} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestExternalStructWithAliasByUtGoFuzzer(t *testing.T) { + match := difflib.Match{A: 1, Size: 1} + + actualVal := ExternalStructWithAlias(match) + + expectedVal := difflib.Match{A: 1, Size: 1} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestSliceOfIntByUtGoFuzzer1(t *testing.T) { + slice := ([]int)(nil) + + actualVal := SliceOfInt(slice) + + assert.Nil(t, actualVal) +} + +func TestSliceOfIntByUtGoFuzzer2(t *testing.T) { + slice := []int{} + + actualVal := SliceOfInt(slice) + + expectedVal := []int{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestSliceOfUintPtrByUtGoFuzzer1(t *testing.T) { + slice := []uintptr{} + + actualVal := SliceOfUintPtr(slice) + + expectedVal := []uintptr{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestSliceOfUintPtrByUtGoFuzzer2(t *testing.T) { + slice := ([]uintptr)(nil) + + actualVal := SliceOfUintPtr(slice) + + assert.Nil(t, actualVal) +} + +func TestSliceOfStringByUtGoFuzzer1(t *testing.T) { + slice := ([]string)(nil) + + actualVal := SliceOfString(slice) + + assert.Nil(t, actualVal) +} + +func TestSliceOfStringByUtGoFuzzer2(t *testing.T) { + slice := []string{} + + actualVal := SliceOfString(slice) + + expectedVal := []string{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestSliceOfStructsByUtGoFuzzer1(t *testing.T) { + slice := ([]Structure)(nil) + + actualVal := SliceOfStructs(slice) + + assert.Nil(t, actualVal) +} + +func TestSliceOfStructsByUtGoFuzzer2(t *testing.T) { + slice := []Structure{} + + actualVal := SliceOfStructs(slice) + + expectedVal := []Structure{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestSliceOfStructsWithNanByUtGoFuzzer1(t *testing.T) { + slice := ([]Structure)(nil) + + actualVal := SliceOfStructsWithNan(slice) + + assert.Nil(t, actualVal) +} + +func TestSliceOfStructsWithNanByUtGoFuzzer2(t *testing.T) { + slice := []Structure{{int8: int8(-1), int32: int32(1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(1), uint64: uint64(1), float32: float32(0.6586717), float64: 0.5295327756124881, complex64: complex(float32(0.6586717), float32(0.6586717)), complex128: complex(0.5295327756124881, 0.5295327756124881), rune: rune(-1)}} + + actualVal := SliceOfStructsWithNan(slice) + + expectedVal := []Structure{{int8: int8(-1), int32: int32(1), uint8: uint8(1), uint16: uint16(1), uint32: uint32(1), uint64: uint64(1), float32: float32(0.6586717), float64: math.NaN(), complex64: complex(float32(0.6586717), float32(0.6586717)), complex128: complex(0.5295327756124881, 0.5295327756124881), rune: rune(-1)}} + + assert.NotEqual(t, expectedVal, actualVal) +} + +func TestSliceOfStructsWithNanPanicsByUtGoFuzzer(t *testing.T) { + slice := []Structure{} + + expectedErrorMessage := "runtime error: index out of range [0] with length 0" + + assert.PanicsWithError(t, expectedErrorMessage, func() { + _ = SliceOfStructsWithNan(slice) + }) +} + +func TestSliceOfSliceOfByteByUtGoFuzzer1(t *testing.T) { + slice := [][]byte{} + + actualVal := SliceOfSliceOfByte(slice) + + expectedVal := [][]byte{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestSliceOfSliceOfByteByUtGoFuzzer2(t *testing.T) { + slice := ([][]byte)(nil) + + actualVal := SliceOfSliceOfByte(slice) + + assert.Nil(t, actualVal) +} + +func TestSliceOfSliceOfStructsByUtGoFuzzer1(t *testing.T) { + slice := [][]Structure{} + + actualVal := SliceOfSliceOfStructs(slice) + + expectedVal := [][]Structure{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestSliceOfSliceOfStructsByUtGoFuzzer2(t *testing.T) { + slice := ([][]Structure)(nil) + + actualVal := SliceOfSliceOfStructs(slice) + + assert.Nil(t, actualVal) +} + +func TestSliceOfArrayOfIntByUtGoFuzzer1(t *testing.T) { + slice := ([][5]int)(nil) + + actualVal := SliceOfArrayOfInt(slice) + + assert.Nil(t, actualVal) +} + +func TestSliceOfArrayOfIntByUtGoFuzzer2(t *testing.T) { + slice := [][5]int{} + + actualVal := SliceOfArrayOfInt(slice) + + expectedVal := [][5]int{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestExportedStructWithEmbeddedUnexportedStructByUtGoFuzzer(t *testing.T) { + exportedStruct := nested.ExportedStruct{} + + actualVal := ExportedStructWithEmbeddedUnexportedStruct(exportedStruct) + + expectedVal := nested.ExportedStruct{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestNamedTypeByUtGoFuzzer(t *testing.T) { + n := Type(1) + + actualVal := NamedType(n) + + expectedVal := Type(1) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestArrayOfNamedTypeByUtGoFuzzer(t *testing.T) { + array := [5]Type{0, 0, 0, 1, 0} + + actualVal := ArrayOfNamedType(array) + + expectedVal := [5]Type{0, 0, 0, 1, 0} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestArrayOfArrayOfNamedTypeByUtGoFuzzer(t *testing.T) { + array := [5][5]Type{{255, 1, 1, 0, 1}, {0, 0, 0, 0, 0}, {1, 0, 0, 0, 1}, {1, 1, 255, 1, 0}, {1, 1, 0, 0, 0}} + + actualVal := ArrayOfArrayOfNamedType(array) + + expectedVal := T{{255, 1, 1, 0, 1}, {0, 0, 0, 0, 0}, {1, 0, 0, 0, 1}, {1, 1, 255, 1, 0}, {1, 1, 0, 0, 0}} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestSliceOfNamedTypeByUtGoFuzzer1(t *testing.T) { + slice := ([]Type)(nil) + + actualVal := SliceOfNamedType(slice) + + assert.Nil(t, actualVal) +} + +func TestSliceOfNamedTypeByUtGoFuzzer2(t *testing.T) { + slice := []Type{} + + actualVal := SliceOfNamedType(slice) + + expectedVal := []Type{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestNamedArrayByUtGoFuzzer(t *testing.T) { + array := NA{1, 1, 0, 1, 0} + + actualVal := NamedArray(array) + + expectedVal := NA{1, 1, 0, 1, 0} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestNamedSliceByUtGoFuzzer1(t *testing.T) { + slice := NS{} + + actualVal := NamedSlice(slice) + + expectedVal := NS{} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestNamedSliceByUtGoFuzzer2(t *testing.T) { + slice := NS(nil) + + actualVal := NamedSlice(slice) + + assert.Nil(t, actualVal) +} + +func TestStructWithFieldsOfNamedTypesByUtGoFuzzer(t *testing.T) { + s := S{n: NA{1, 0, 1, 0, 1}} + + actualVal := StructWithFieldsOfNamedTypes(s) + + expectedVal := S{n: NA{1, 0, 1, 0, 1}} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestMapByUtGoFuzzer(t *testing.T) { + table := (map[string]int)(nil) + + actualVal := Map(table) + + assert.Nil(t, actualVal) +} + +func TestMapOfStructuresByUtGoFuzzer(t *testing.T) { + table := (map[Structure]Structure)(nil) + + actualVal := MapOfStructures(table) + + assert.Nil(t, actualVal) +} + +func TestMapOfSliceOfIntByUtGoFuzzer(t *testing.T) { + table := (map[string][]int)(nil) + + actualVal := MapOfSliceOfInt(table) + + assert.Nil(t, actualVal) +} + +func TestMapOfNamedTypeByUtGoFuzzer(t *testing.T) { + table := (map[int]Type)(nil) + + actualVal := MapOfNamedType(table) + + assert.Nil(t, actualVal) +} + +func TestMapOfNamedSliceByUtGoFuzzer(t *testing.T) { + table := (map[uint]NS)(nil) + + actualVal := MapOfNamedSlice(table) + + assert.Nil(t, actualVal) +} + +func TestNamedMapByUtGoFuzzer(t *testing.T) { + n := NM(nil) + + actualVal := NamedMap(n) + + assert.Nil(t, actualVal) +} + +func TestChannelByUtGoFuzzer1(t *testing.T) { + c := make(chan Structure, 0) + close(c) + + assert.NotPanics(t, func() { + Channel(c) + }) +} + +func TestChannelByUtGoFuzzer2(t *testing.T) { + c := (chan Structure)(nil) + + assert.NotPanics(t, func() { + Channel(c) + }) +} + +func TestSendOnlyChannelByUtGoFuzzer1(t *testing.T) { + c := (chan<- int)(nil) + + assert.NotPanics(t, func() { + SendOnlyChannel(c) + }) +} + +func TestSendOnlyChannelByUtGoFuzzer2(t *testing.T) { + c := make(chan int, 0) + close(c) + + assert.NotPanics(t, func() { + SendOnlyChannel(c) + }) +} + +func TestRecvOnlyChannelByUtGoFuzzer1(t *testing.T) { + c := make(chan NM, 0) + close(c) + + assert.NotPanics(t, func() { + RecvOnlyChannel(c) + }) +} + +func TestRecvOnlyChannelByUtGoFuzzer2(t *testing.T) { + c := (<-chan NM)(nil) + + assert.NotPanics(t, func() { + RecvOnlyChannel(c) + }) +} + +func TestPointerToIntByUtGoFuzzer1(t *testing.T) { + n := (*int)(nil) + + actualVal := PointerToInt(n) + + assert.Nil(t, actualVal) +} + +func TestPointerToIntByUtGoFuzzer2(t *testing.T) { + n := new(int) + *n = 0 + + actualVal := PointerToInt(n) + + expectedVal := new(int) + *expectedVal = 0 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestPointerToSliceByUtGoFuzzer1(t *testing.T) { + n := (*[]int)(nil) + + actualVal := PointerToSlice(n) + + assert.Nil(t, actualVal) +} + +func TestPointerToSliceByUtGoFuzzer2(t *testing.T) { + n := new([]int) + + actualVal := PointerToSlice(n) + + expectedVal := new([]int) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestPointerToArrayByUtGoFuzzer1(t *testing.T) { + n := &[3]int{0, 1, 1} + + actualVal := PointerToArray(n) + + expectedVal := &[3]int{0, 1, 1} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestPointerToArrayByUtGoFuzzer2(t *testing.T) { + n := (*[3]int)(nil) + + actualVal := PointerToArray(n) + + assert.Nil(t, actualVal) +} + +func TestPointerToMapByUtGoFuzzer1(t *testing.T) { + n := (*map[string]int)(nil) + + actualVal := PointerToMap(n) + + assert.Nil(t, actualVal) +} + +func TestPointerToMapByUtGoFuzzer2(t *testing.T) { + n := new(map[string]int) + + actualVal := PointerToMap(n) + + expectedVal := new(map[string]int) + + assert.Equal(t, expectedVal, actualVal) +} + +func TestPointerToStructureByUtGoFuzzer1(t *testing.T) { + n := &Structure{int8: int8(-128), int16: int16(1), int64: int64(-1), uint: uint(1), uint8: uint8(255), float32: float32(0.5587146), float64: 0.745147167878201, complex64: complex(float32(0.5587146), float32(0.5587146)), complex128: complex(0.745147167878201, 0.745147167878201), byte: byte(1)} + + actualVal := PointerToStructure(n) + + expectedVal := &Structure{int8: int8(-128), int16: int16(1), int64: int64(-1), uint: uint(1), uint8: uint8(255), float32: float32(0.5587146), float64: 0.745147167878201, complex64: complex(float32(0.5587146), float32(0.5587146)), complex128: complex(0.745147167878201, 0.745147167878201), byte: byte(1)} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestPointerToStructureByUtGoFuzzer2(t *testing.T) { + n := (*Structure)(nil) + + actualVal := PointerToStructure(n) + + assert.Nil(t, actualVal) +} + +func TestPointerToNamedTypeByUtGoFuzzer1(t *testing.T) { + n := new(Type) + *n = 1 + + actualVal := PointerToNamedType(n) + + expectedVal := new(Type) + *expectedVal = 1 + + assert.Equal(t, expectedVal, actualVal) +} + +func TestPointerToNamedTypeByUtGoFuzzer2(t *testing.T) { + n := (*Type)(nil) + + actualVal := PointerToNamedType(n) + + assert.Nil(t, actualVal) +} + +func TestPointerToRecursiveStructByUtGoFuzzer1(t *testing.T) { + n := &Node{val: 4} + + actualVal := PointerToRecursiveStruct(n) + + expectedVal := &Node{val: 4} + + assert.Equal(t, expectedVal, actualVal) +} + +func TestPointerToRecursiveStructByUtGoFuzzer2(t *testing.T) { + n := (*Node)(nil) + + actualVal := PointerToRecursiveStruct(n) + + assert.Nil(t, actualVal) +} + +func TestInterfaceByUtGoFuzzer1(t *testing.T) { + i := I(nil) + + assert.NotPanics(t, func() { + Interface(i) + }) +} + +func TestInterfaceByUtGoFuzzer2(t *testing.T) { + i := time.Duration(1) + + assert.NotPanics(t, func() { + Interface(i) + }) +} + +func TestExternalInterfaceByUtGoFuzzer1(t *testing.T) { + i := time.Duration(1) + + assert.NotPanics(t, func() { + ExternalInterface(i) + }) +} + +func TestExternalInterfaceByUtGoFuzzer2(t *testing.T) { + i := fmt.Stringer(nil) + + assert.NotPanics(t, func() { + ExternalInterface(i) + }) +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/GoEngine.kt b/utbot-go/src/main/kotlin/org/utbot/go/GoEngine.kt new file mode 100644 index 0000000000..0f2393f466 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/GoEngine.kt @@ -0,0 +1,138 @@ +package org.utbot.go + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch +import mu.KotlinLogging +import org.utbot.fuzzing.BaseFeedback +import org.utbot.fuzzing.Control +import org.utbot.fuzzing.utils.Trie +import org.utbot.go.api.* +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.logic.TestsGenerationMode +import org.utbot.go.worker.GoWorker +import org.utbot.go.worker.convertRawExecutionResultToExecutionResult +import java.net.SocketException +import java.net.SocketTimeoutException +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +val logger = KotlinLogging.logger {} + +class GoEngine( + private var workers: List, + private val functionUnderTest: GoUtFunction, + private val needToCoverLines: Set, + private val aliases: Map, + private val functionExecutionTimeoutMillis: Long, + private val mode: TestsGenerationMode, + private val timeoutExceededOrIsCanceled: () -> Boolean +) { + var numberOfFunctionExecutions: AtomicInteger = AtomicInteger(0) + + fun fuzzing(): Flow> = channelFlow { + if (!functionUnderTest.isMethod && functionUnderTest.parameters.isEmpty()) { + val lengthOfParameters = workers[0].sendFuzzedParametersValues(functionUnderTest, emptyList(), emptyMap()) + val (executionResult, coverTab) = run { + val rawExecutionResult = workers[0].receiveRawExecutionResult() + numberOfFunctionExecutions.incrementAndGet() + convertRawExecutionResultToExecutionResult( + rawExecutionResult = rawExecutionResult, + functionResultTypes = functionUnderTest.results.map { it.type }, + timeoutMillis = functionExecutionTimeoutMillis, + ) to rawExecutionResult.coverTab + } + val fuzzedFunction = GoUtFuzzedFunction(functionUnderTest, emptyList()) + val testCase = GoUtFuzzedFunctionTestCase( + fuzzedFunction, executionResult + ) + send(mapOf(CoveredLines(coverTab.keys) to ExecutionResults(testCase, lengthOfParameters))) + } else { + val needToStop = AtomicBoolean() + workers.mapIndexed { index, worker -> + launch(Dispatchers.IO) { + val testCases = mutableMapOf() + try { + fuzzingProcessRoutine(needToStop, testCases, worker, index) + } finally { + send(testCases) + } + } + } + } + } + + private suspend fun fuzzingProcessRoutine( + needToStop: AtomicBoolean, + testCases: MutableMap, + worker: GoWorker, + index: Int + ) { + var attempts = 0 + val attemptsLimit = Int.MAX_VALUE + runGoFuzzing(functionUnderTest, worker, index) { description, values -> + try { + if (needToStop.get() || timeoutExceededOrIsCanceled()) { + return@runGoFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) + } + + val lengthOfParameters = + description.worker.sendFuzzedParametersValues(functionUnderTest, values, aliases) + val (executionResult, coverTab) = run { + val rawExecutionResult = description.worker.receiveRawExecutionResult() + numberOfFunctionExecutions.incrementAndGet() + convertRawExecutionResultToExecutionResult( + rawExecutionResult = rawExecutionResult, + functionResultTypes = functionUnderTest.results.map { it.type }, + timeoutMillis = functionExecutionTimeoutMillis, + ) to rawExecutionResult.coverTab + } + + if (coverTab.isEmpty()) { + logger.error { "Coverage is empty for [${functionUnderTest.name}] with $values}" } + return@runGoFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + } + + val coveredLines = CoveredLines(needToCoverLines.intersect(coverTab.keys)) + val fuzzedFunction = GoUtFuzzedFunction(functionUnderTest, values) + val testCase = GoUtFuzzedFunctionTestCase(fuzzedFunction, executionResult) + if (mode != TestsGenerationMode.FUZZING_MODE) { + if (testCases[coveredLines] == null) { + testCases[coveredLines] = ExecutionResults(testCase, lengthOfParameters) + } else { + testCases[coveredLines]!!.update(testCase, lengthOfParameters) + } + } + + val trieNode = description.coverage.add(coveredLines.lines.sorted()) + if (executionResult is GoUtTimeoutExceeded) { + description.worker.restartWorker() + return@runGoFuzzing BaseFeedback(result = trieNode, control = Control.PASS) + } + if (trieNode.count > 1) { + if (++attempts >= attemptsLimit) { + return@runGoFuzzing BaseFeedback( + result = Trie.emptyNode(), + control = Control.STOP + ) + } + return@runGoFuzzing BaseFeedback(result = trieNode, control = Control.CONTINUE) + } + + if (mode == TestsGenerationMode.FUZZING_MODE && executionResult !is GoUtExecutionSuccess) { + needToStop.set(true) + testCases[coveredLines] = ExecutionResults(testCase, lengthOfParameters) + return@runGoFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) + } + BaseFeedback(result = trieNode, control = Control.CONTINUE) + } catch (e: SocketTimeoutException) { + description.worker.restartWorker() + return@runGoFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + } catch (e: SocketException) { + description.worker.restartWorker() + return@runGoFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + } + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/GoLanguage.kt b/utbot-go/src/main/kotlin/org/utbot/go/GoLanguage.kt new file mode 100644 index 0000000000..99459ffb02 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/GoLanguage.kt @@ -0,0 +1,61 @@ +package org.utbot.go + +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.IdentityTrie +import org.utbot.fuzzing.utils.Trie +import org.utbot.go.api.GoUtFunction +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import org.utbot.go.fuzzer.providers.* +import org.utbot.go.worker.GoWorker +import kotlin.random.Random + + +fun goDefaultValueProviders() = listOf( + GoPrimitivesValueProvider, + GoArrayValueProvider, + GoSliceValueProvider, + GoMapValueProvider, + GoChanValueProvider, + GoStructValueProvider, + GoConstantValueProvider, + GoNamedValueProvider, + GoNilValueProvider, + GoPointerValueProvider, + GoInterfaceValueProvider +) + +data class CoveredLines(val lines: Set) + +class GoDescription( + val worker: GoWorker, + val functionUnderTest: GoUtFunction, + val coverage: Trie, + val configuration: Configuration, +) : Description( + if (functionUnderTest.isMethod) { + listOf(functionUnderTest.receiver!!.type) + functionUnderTest.parameters.map { it.type }.toList() + } else { + functionUnderTest.parameters.map { it.type }.toList() + } +) + +suspend fun runGoFuzzing( + function: GoUtFunction, + worker: GoWorker, + index: Int, + providers: List> = goDefaultValueProviders(), + exec: suspend (description: GoDescription, values: List) -> BaseFeedback, GoTypeId, GoUtModel> +) { + val config = Configuration() + BaseFuzzing(providers, exec).fuzz( + description = GoDescription( + worker = worker, + functionUnderTest = function, + coverage = IdentityTrie(), + configuration = config + ), + random = Random(index), + configuration = config, + ) +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt new file mode 100644 index 0000000000..9244f8194f --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt @@ -0,0 +1,238 @@ +package org.utbot.go.api + +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoTypeId + +/** + * Represents real Go primitive type. + */ +class GoPrimitiveTypeId(name: String) : GoTypeId(name) { + override val canonicalName: String = when (name) { + "byte" -> "uint8" + "rune" -> "int32" + else -> name + } + + override fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String = name + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoPrimitiveTypeId) return false + + return canonicalName == other.canonicalName + } + + override fun hashCode(): Int = name.hashCode() +} + +/** + * Class for Go struct field. + */ +data class GoFieldId( + val declaringType: GoTypeId, + val name: String, + val isExported: Boolean +) + +/** + * Represents real Go struct type. + */ +class GoStructTypeId( + name: String, + var fields: List, +) : GoTypeId(name) { + override val canonicalName: String = fields.joinToString(separator = ";", prefix = "struct{", postfix = "}") { + "${it.name} ${it.declaringType}" + } + + override fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String = name + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoStructTypeId) return false + + return fields == other.fields + } + + override fun hashCode(): Int = fields.hashCode() +} + +/** + * Represents real Go array type. + */ +class GoArrayTypeId( + name: String, elementTypeId: GoTypeId, val length: Int +) : GoTypeId(name, elementTypeId = elementTypeId) { + override val canonicalName: String = "[$length]${elementTypeId}" + + override fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String = + "[$length]${elementTypeId!!.getRelativeName(destinationPackage, aliases)}" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoArrayTypeId) return false + + return elementTypeId == other.elementTypeId && length == other.length + } + + override fun hashCode(): Int = 31 * elementTypeId.hashCode() + length +} + +/** + * Represents real Go slice type. + */ +class GoSliceTypeId( + name: String, elementTypeId: GoTypeId, +) : GoTypeId(name, elementTypeId = elementTypeId) { + override val canonicalName: String = "[]${elementTypeId}" + + override fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String = + "[]${elementTypeId!!.getRelativeName(destinationPackage, aliases)}" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoSliceTypeId) return false + + return elementTypeId == other.elementTypeId + } + + override fun hashCode(): Int = elementTypeId.hashCode() +} + +/** + * Represents real Go map type. + */ +class GoMapTypeId( + name: String, val keyTypeId: GoTypeId, elementTypeId: GoTypeId, +) : GoTypeId(name, elementTypeId = elementTypeId) { + override val canonicalName: String = "map[${keyTypeId.canonicalName}]${elementTypeId.canonicalName}" + + override fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String { + val keyType = keyTypeId.getRelativeName(destinationPackage, aliases) + val elementType = elementTypeId!!.getRelativeName(destinationPackage, aliases) + return "map[$keyType]$elementType" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoMapTypeId) return false + + return keyTypeId == other.keyTypeId && elementTypeId == other.elementTypeId + } + + override fun hashCode(): Int = 31 * keyTypeId.hashCode() + elementTypeId.hashCode() +} + +/** + * Represents real Go chan type. + */ +class GoChanTypeId( + name: String, elementTypeId: GoTypeId, val direction: Direction, +) : GoTypeId(name, elementTypeId = elementTypeId) { + enum class Direction { + SENDONLY, RECVONLY, SENDRECV + } + + private val typeWithDirection = when (direction) { + Direction.RECVONLY -> "<-chan" + Direction.SENDONLY -> "chan<-" + Direction.SENDRECV -> "chan" + } + + override val canonicalName: String = "$typeWithDirection $elementTypeId" + + override fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String { + val elementType = elementTypeId!!.getRelativeName(destinationPackage, aliases) + return "$typeWithDirection $elementType" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoChanTypeId) return false + + return elementTypeId == other.elementTypeId && direction == other.direction + } + + override fun hashCode(): Int = 31 * elementTypeId.hashCode() + direction.hashCode() +} + +/** + * Represents real Go interface type. + */ +class GoInterfaceTypeId(name: String, val implementations: List) : GoTypeId(name) { + override val canonicalName: String = name + + override fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String = name + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoInterfaceTypeId) return false + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() +} + +/** + * Represents real Go named type. + */ +class GoNamedTypeId( + name: String, override val sourcePackage: GoPackage, implementsError: Boolean, val underlyingTypeId: GoTypeId +) : GoTypeId(name, implementsError = implementsError) { + private val packageName: String = sourcePackage.name + private val packagePath: String = sourcePackage.path + override val canonicalName: String = if (sourcePackage.isBuiltin) { + name + } else { + "$packagePath/$packageName.$name" + } + + fun exported(): Boolean = name.first().isUpperCase() + + override fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String { + val alias = aliases[sourcePackage] + return if (sourcePackage.isBuiltin || sourcePackage == destinationPackage || alias == ".") { + name + } else if (alias == null) { + "${packageName}.${name}" + } else { + "${alias}.${name}" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoNamedTypeId) return false + + return sourcePackage == other.sourcePackage && name == other.name + } + + override fun hashCode(): Int { + var result = packagePath.hashCode() + result = 31 * result + packageName.hashCode() + result = 31 * result + name.hashCode() + return result + } +} + +/** + * Represents real Go pointer type. + */ +class GoPointerTypeId(name: String, elementTypeId: GoTypeId) : GoTypeId(name, elementTypeId = elementTypeId) { + override val canonicalName: String = "*$elementTypeId" + + override fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String { + val elementType = elementTypeId!!.getRelativeName(destinationPackage, aliases) + return "*$elementType" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoPointerTypeId) return false + + return elementTypeId == other.elementTypeId + } + + override fun hashCode(): Int = elementTypeId.hashCode() +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt new file mode 100644 index 0000000000..4a4a07879f --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt @@ -0,0 +1,84 @@ +package org.utbot.go.api + +import org.utbot.go.framework.api.go.GoUtModel + +interface GoUtExecutionResult + +interface GoUtExecutionCompleted : GoUtExecutionResult { + val models: List +} + +data class GoUtExecutionSuccess(override val models: List) : GoUtExecutionCompleted + +data class GoUtExecutionWithNonNilError(override val models: List) : GoUtExecutionCompleted + +data class GoUtPanicFailure(val panicValue: GoUtModel, val panicValueIsErrorMessage: Boolean) : GoUtExecutionResult + +data class GoUtTimeoutExceeded(val timeoutMillis: Long) : GoUtExecutionResult + +class ExecutionResults(testCase: GoUtFuzzedFunctionTestCase, length: Int) { + private var successfulExecutionTestCaseWithLengthOfParameters: Pair? = null + private var executionWithErrorTestCaseWithLengthOfParameters: Pair? = null + private var panicFailureTestCaseWithLengthOfParameters: Pair? = null + private var timeoutExceededTestCaseWithLengthOfParameters: Pair? = null + + private fun Pair?.relax( + testCase: GoUtFuzzedFunctionTestCase, + length: Int + ): Pair = if (this == null || this.second > length) { + testCase to length + } else { + this + } + + private fun Pair?.relax( + testCaseAndLength: Pair?, + ): Pair? = if (testCaseAndLength != null) { + this.relax(testCaseAndLength.first, testCaseAndLength.second) + } else { + this + } + + init { + when (testCase.executionResult) { + is GoUtExecutionSuccess -> successfulExecutionTestCaseWithLengthOfParameters = testCase to length + is GoUtExecutionWithNonNilError -> executionWithErrorTestCaseWithLengthOfParameters = testCase to length + is GoUtPanicFailure -> panicFailureTestCaseWithLengthOfParameters = testCase to length + is GoUtTimeoutExceeded -> timeoutExceededTestCaseWithLengthOfParameters = testCase to length + } + } + + fun update(testCase: GoUtFuzzedFunctionTestCase, length: Int) = when (testCase.executionResult) { + is GoUtExecutionSuccess -> successfulExecutionTestCaseWithLengthOfParameters = + successfulExecutionTestCaseWithLengthOfParameters.relax(testCase, length) + + is GoUtExecutionWithNonNilError -> executionWithErrorTestCaseWithLengthOfParameters = + executionWithErrorTestCaseWithLengthOfParameters.relax(testCase, length) + + is GoUtPanicFailure -> panicFailureTestCaseWithLengthOfParameters = + panicFailureTestCaseWithLengthOfParameters.relax(testCase, length) + + is GoUtTimeoutExceeded -> timeoutExceededTestCaseWithLengthOfParameters = + timeoutExceededTestCaseWithLengthOfParameters.relax(testCase, length) + + else -> error("${testCase.executionResult.javaClass.name} is not supported") + } + + fun update(executionResults: ExecutionResults) { + successfulExecutionTestCaseWithLengthOfParameters = + successfulExecutionTestCaseWithLengthOfParameters.relax(executionResults.successfulExecutionTestCaseWithLengthOfParameters) + executionWithErrorTestCaseWithLengthOfParameters = + executionWithErrorTestCaseWithLengthOfParameters.relax(executionResults.executionWithErrorTestCaseWithLengthOfParameters) + panicFailureTestCaseWithLengthOfParameters = + panicFailureTestCaseWithLengthOfParameters.relax(executionResults.panicFailureTestCaseWithLengthOfParameters) + timeoutExceededTestCaseWithLengthOfParameters = + timeoutExceededTestCaseWithLengthOfParameters.relax(executionResults.timeoutExceededTestCaseWithLengthOfParameters) + } + + fun getTestCases(): List = listOfNotNull( + successfulExecutionTestCaseWithLengthOfParameters?.first, + executionWithErrorTestCaseWithLengthOfParameters?.first, + panicFailureTestCaseWithLengthOfParameters?.first, + timeoutExceededTestCaseWithLengthOfParameters?.first + ) +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt new file mode 100644 index 0000000000..0371178375 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt @@ -0,0 +1,36 @@ +package org.utbot.go.api + +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import java.io.File +import java.nio.file.Paths + +data class GoUtFile(val absolutePath: String, val sourcePackage: GoPackage) { + val fileName: String get() = File(absolutePath).name + val fileNameWithoutExtension: String get() = File(absolutePath).nameWithoutExtension + val absoluteDirectoryPath: String get() = Paths.get(absolutePath).parent.toString() +} + +data class GoUtDeclaredVariable(val name: String, val type: GoTypeId) + +data class GoUtFunction( + val name: String, + val receiver: GoUtDeclaredVariable?, + val parameters: List, + val results: List, + val constants: Map>, + val sourceFile: GoUtFile +) { + val isMethod: Boolean = receiver != null +} + +data class GoUtFuzzedFunction(val function: GoUtFunction, val parametersValues: List) + +data class GoUtFuzzedFunctionTestCase( + val fuzzedFunction: GoUtFuzzedFunction, + val executionResult: GoUtExecutionResult, +) { + val function: GoUtFunction get() = fuzzedFunction.function + val parametersValues: List get() = fuzzedFunction.parametersValues +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt new file mode 100644 index 0000000000..cdc1fd5c42 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt @@ -0,0 +1,383 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "CanBeParameter") + +package org.utbot.go.api + +import org.utbot.go.api.util.* +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +// NEVER and DEPENDS difference is useful in code generation of assert.Equals(...). +enum class ExplicitCastMode { + REQUIRED, NEVER, DEPENDS +} + +/** + * Class for Go primitive model. + */ +open class GoUtPrimitiveModel( + val value: Any, + typeId: GoPrimitiveTypeId, + val explicitCastMode: ExplicitCastMode = if (typeId.neverRequiresExplicitCast) { + ExplicitCastMode.NEVER + } else { + ExplicitCastMode.DEPENDS + }, + private val requiredPackages: Set = emptySet(), +) : GoUtModel(typeId) { + override val typeId: GoPrimitiveTypeId + get() = super.typeId as GoPrimitiveTypeId + + override fun getRequiredPackages(destinationPackage: GoPackage): Set = requiredPackages + + override fun isComparable(): Boolean = true + + override fun toString(): String = if (typeId == goStringTypeId) "\"${value}\"" else "$value" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtPrimitiveModel) return false + + return typeId == other.typeId && value == other.value + } + + override fun hashCode(): Int = 31 * value.hashCode() + typeId.hashCode() +} + +/** + * Class for Go struct model. + */ +class GoUtStructModel( + val value: LinkedHashMap, + typeId: GoStructTypeId, +) : GoUtModel(typeId) { + override val typeId: GoStructTypeId + get() = super.typeId as GoStructTypeId + + override fun getRequiredPackages(destinationPackage: GoPackage): Set = + value.values.fold(emptySet()) { acc, fieldModel -> + acc + fieldModel.getRequiredPackages(destinationPackage) + } + + override fun isComparable(): Boolean = value.values.all { it.isComparable() } + + override fun toString(): String = + value.entries.joinToString(prefix = "struct{", postfix = "}") { (fieldId, model) -> + "${fieldId.name}: $model" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtStructModel) return false + + return typeId == other.typeId && value == other.value + } + + override fun hashCode(): Int = 31 * value.hashCode() + typeId.hashCode() +} + +/** + * Class for Go array model. + */ +class GoUtArrayModel( + val value: Array, + typeId: GoArrayTypeId, +) : GoUtModel(typeId) { + val length: Int = typeId.length + + override val typeId: GoArrayTypeId + get() = super.typeId as GoArrayTypeId + + override fun getRequiredPackages(destinationPackage: GoPackage): Set { + val elementNamedTypeId = typeId.elementTypeId as? GoNamedTypeId + val imports = elementNamedTypeId?.getRequiredPackages(destinationPackage) ?: emptySet() + return value.fold(imports) { acc, model -> + acc + model.getRequiredPackages(destinationPackage) + } + } + + override fun isComparable(): Boolean = value.all { it.isComparable() } + + fun getElements(): List = value.toList() + + override fun toString(): String = getElements().joinToString(prefix = "$typeId{", postfix = "}") { + it.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtArrayModel) return false + + return typeId == other.typeId && value.contentEquals(other.value) && length == other.length + } + + override fun hashCode(): Int { + var result = value.hashCode() + result = 31 * result + length + result = 31 * result + typeId.hashCode() + return result + } +} + +/** + * Class for Go slice model. + */ +class GoUtSliceModel( + val value: Array, + typeId: GoSliceTypeId, + val length: Int, +) : GoUtModel(typeId) { + override val typeId: GoSliceTypeId + get() = super.typeId as GoSliceTypeId + + override fun getRequiredPackages(destinationPackage: GoPackage): Set { + val elementNamedTypeId = typeId.elementTypeId as? GoNamedTypeId + val imports = elementNamedTypeId?.getRequiredPackages(destinationPackage) ?: emptySet() + return value.filterNotNull().fold(imports) { acc, model -> + acc + model.getRequiredPackages(destinationPackage) + } + } + + override fun isComparable(): Boolean = value.all { it?.isComparable() ?: true } + + fun getElements(): List = value.map { + it ?: typeId.elementTypeId!!.goDefaultValueModel() + } + + override fun toString(): String = getElements().joinToString(prefix = "$typeId{", postfix = "}") { + it.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtSliceModel) return false + + return typeId == other.typeId && value.contentEquals(other.value) && length == other.length + } + + override fun hashCode(): Int { + var result = value.hashCode() + result = 31 * result + length + result = 31 * result + typeId.hashCode() + return result + } +} + +/** + * Class for Go map model. + */ +class GoUtMapModel( + val value: MutableMap, + typeId: GoMapTypeId, +) : GoUtModel(typeId) { + override val typeId: GoMapTypeId + get() = super.typeId as GoMapTypeId + + override fun getRequiredPackages(destinationPackage: GoPackage): Set { + val keyNamedTypeId = typeId.keyTypeId as? GoNamedTypeId + var imports = keyNamedTypeId?.getRequiredPackages(destinationPackage) ?: emptySet() + val elementNamedTypeId = typeId.elementTypeId as? GoNamedTypeId + imports = imports + (elementNamedTypeId?.getRequiredPackages(destinationPackage) ?: emptySet()) + return value.values.fold(imports) { acc, model -> + acc + model.getRequiredPackages(destinationPackage) + } + } + + override fun isComparable(): Boolean = + value.keys.all { it.isComparable() } && value.values.all { it.isComparable() } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtMapModel) return false + + return typeId == other.typeId && value == other.value + } + + override fun hashCode(): Int = 31 * value.hashCode() + typeId.hashCode() +} + +/** + * Class for Go chan model. + */ +class GoUtChanModel( + val value: Array, + typeId: GoChanTypeId +) : GoUtModel(typeId) { + override val typeId: GoChanTypeId + get() = super.typeId as GoChanTypeId + + override fun getRequiredPackages(destinationPackage: GoPackage): Set { + val elementNamedTypeId = typeId.elementTypeId as? GoNamedTypeId + val imports = elementNamedTypeId?.getRequiredPackages(destinationPackage) ?: emptySet() + return value.filterNotNull().fold(imports) { acc, model -> + acc + model.getRequiredPackages(destinationPackage) + } + } + + override fun isComparable(): Boolean = value.all { it?.isComparable() ?: true } + + fun getElements(): List = value.filterNotNull() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtChanModel) return false + + return typeId == other.typeId && value.contentEquals(other.value) + } + + override fun hashCode(): Int = 31 * value.hashCode() + typeId.hashCode() +} + +/** + * Class for Go model with the IEEE 754 “not-a-number” value. + */ +class GoUtFloatNaNModel( + typeId: GoPrimitiveTypeId +) : GoUtPrimitiveModel( + "math.NaN()", + typeId, + explicitCastMode = if (typeId != goFloat64TypeId) { + ExplicitCastMode.REQUIRED + } else { + ExplicitCastMode.NEVER + }, + requiredPackages = setOf(GoPackage("math", "math")), +) { + override fun isComparable(): Boolean = false + + override fun equals(other: Any?): Boolean = this === other || other is GoUtFloatNaNModel + + override fun hashCode(): Int = typeId.hashCode() + + override fun toString(): String = "NaN" +} + +/** + * Class for Go model with infinity. + */ +class GoUtFloatInfModel( + val sign: Int, typeId: GoPrimitiveTypeId +) : GoUtPrimitiveModel( + "math.Inf($sign)", + typeId, + explicitCastMode = if (typeId != goFloat64TypeId) { + ExplicitCastMode.REQUIRED + } else { + ExplicitCastMode.NEVER + }, + requiredPackages = setOf(GoPackage("math", "math")), +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtFloatInfModel) return false + + return sign == other.sign + } + + override fun hashCode(): Int = sign.hashCode() + + override fun toString(): String = if (sign >= 0) { + "+Inf" + } else { + "-Inf" + } +} + +/** + * Class for Go models with complex numbers. + */ +class GoUtComplexModel( + var realValue: GoUtPrimitiveModel, + var imagValue: GoUtPrimitiveModel, + typeId: GoPrimitiveTypeId, +) : GoUtPrimitiveModel( + "complex($realValue, $imagValue)", + typeId, + explicitCastMode = ExplicitCastMode.NEVER +) { + override fun getRequiredPackages(destinationPackage: GoPackage): Set = + realValue.getRequiredPackages(destinationPackage) + imagValue.getRequiredPackages(destinationPackage) + + override fun isComparable(): Boolean = realValue.isComparable() && imagValue.isComparable() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtComplexModel) return false + + return realValue == other.realValue && imagValue == other.imagValue + } + + override fun hashCode(): Int = 31 * realValue.hashCode() + imagValue.hashCode() +} + +/** + * Class for Go model with nil. + */ +class GoUtNilModel( + typeId: GoTypeId +) : GoUtModel(typeId) { + override fun getRequiredPackages(destinationPackage: GoPackage): Set = emptySet() + + override fun isComparable(): Boolean = true + + override fun toString() = "nil" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtNilModel) return false + + return typeId == other.typeId + } + + override fun hashCode(): Int = typeId.hashCode() +} + +/** + * Class for Go named model. + */ +class GoUtNamedModel( + var value: GoUtModel, + typeId: GoNamedTypeId, +) : GoUtModel(typeId) { + override val typeId: GoNamedTypeId + get() = super.typeId as GoNamedTypeId + + override fun getRequiredPackages(destinationPackage: GoPackage): Set = + typeId.getRequiredPackages(destinationPackage) + value.getRequiredPackages(destinationPackage) + + override fun isComparable(): Boolean = value.isComparable() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtNamedModel) return false + + return typeId == other.typeId && value == other.value + } + + override fun hashCode(): Int = 31 * value.hashCode() + typeId.hashCode() +} + +/** + * Class for Go pointer model. + */ +class GoUtPointerModel( + var value: GoUtModel, + typeId: GoPointerTypeId +) : GoUtModel(typeId) { + override val typeId: GoPointerTypeId + get() = super.typeId as GoPointerTypeId + + override fun getRequiredPackages(destinationPackage: GoPackage): Set = + value.getRequiredPackages(destinationPackage) + + override fun isComparable(): Boolean = value.isComparable() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoUtPointerModel) return false + + return typeId == other.typeId && value == other.value + } + + override fun hashCode(): Int = 31 * value.hashCode() + typeId.hashCode() +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt new file mode 100644 index 0000000000..df2eba4a1d --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt @@ -0,0 +1,236 @@ +package org.utbot.go.api.util + +import org.utbot.go.api.* +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import kotlin.properties.Delegates +import kotlin.reflect.KClass + +var intSize by Delegates.notNull() + +val goByteTypeId = GoPrimitiveTypeId("byte") +val goBoolTypeId = GoPrimitiveTypeId("bool") + +val goComplex64TypeId = GoPrimitiveTypeId("complex64") +val goComplex128TypeId = GoPrimitiveTypeId("complex128") + +val goFloat32TypeId = GoPrimitiveTypeId("float32") +val goFloat64TypeId = GoPrimitiveTypeId("float64") + +val goInt8TypeId = GoPrimitiveTypeId("int8") +val goInt16TypeId = GoPrimitiveTypeId("int16") +val goInt32TypeId = GoPrimitiveTypeId("int32") +val goIntTypeId = GoPrimitiveTypeId("int") +val goInt64TypeId = GoPrimitiveTypeId("int64") + +val goRuneTypeId = GoPrimitiveTypeId("rune") // = int32 +val goStringTypeId = GoPrimitiveTypeId("string") + +val goUint8TypeId = GoPrimitiveTypeId("uint8") +val goUint16TypeId = GoPrimitiveTypeId("uint16") +val goUint32TypeId = GoPrimitiveTypeId("uint32") +val goUintTypeId = GoPrimitiveTypeId("uint") +val goUint64TypeId = GoPrimitiveTypeId("uint64") +val goUintPtrTypeId = GoPrimitiveTypeId("uintptr") + +val goPrimitives = setOf( + goByteTypeId, + goBoolTypeId, + goComplex128TypeId, + goComplex64TypeId, + goFloat32TypeId, + goFloat64TypeId, + goIntTypeId, + goInt16TypeId, + goInt32TypeId, + goInt64TypeId, + goInt8TypeId, + goRuneTypeId, + goStringTypeId, + goUintTypeId, + goUint16TypeId, + goUint32TypeId, + goUint64TypeId, + goUint8TypeId, + goUintPtrTypeId, +) + +val goSupportedConstantTypes = setOf( + goByteTypeId, + goFloat32TypeId, + goFloat64TypeId, + goIntTypeId, + goInt8TypeId, + goInt16TypeId, + goInt32TypeId, + goInt64TypeId, + goRuneTypeId, + goStringTypeId, + goUintTypeId, + goUint8TypeId, + goUint16TypeId, + goUint32TypeId, + goUint64TypeId, + goUintPtrTypeId, +) + +private val goTypesNeverRequireExplicitCast = setOf( + goBoolTypeId, + goComplex128TypeId, + goComplex64TypeId, + goFloat64TypeId, + goIntTypeId, + goStringTypeId, +) + +val GoPrimitiveTypeId.neverRequiresExplicitCast: Boolean + get() = this in goTypesNeverRequireExplicitCast + +/** + * This method is useful for converting the string representation of a Go value to its more accurate representation. + */ +private fun GoPrimitiveTypeId.correspondingKClass(): KClass = when (this) { + goBoolTypeId -> Boolean::class + goFloat32TypeId -> Float::class + goFloat64TypeId -> Double::class + goInt8TypeId -> Byte::class + goInt16TypeId -> Short::class + goInt32TypeId, goRuneTypeId -> Int::class + goIntTypeId -> if (intSize == 32) Int::class else Long::class + goInt64TypeId -> Long::class + goStringTypeId -> String::class + goUint8TypeId, goByteTypeId -> UByte::class + goUint16TypeId -> UShort::class + goUint32TypeId -> UInt::class + goUintTypeId -> if (intSize == 32) UInt::class else ULong::class + goUint64TypeId -> ULong::class + goUintPtrTypeId -> if (intSize == 32) UInt::class else ULong::class + else -> String::class // default way to hold GoUtPrimitiveModel's value is to use String +} + +fun rawValueOfGoPrimitiveTypeToValue(typeId: GoPrimitiveTypeId, rawValue: String): Any = + when (typeId.correspondingKClass()) { + UByte::class -> rawValue.toUByte() + Boolean::class -> rawValue.toBoolean() + Float::class -> rawValue.toFloat() + Double::class -> rawValue.toDouble() + Int::class -> rawValue.toInt() + Short::class -> rawValue.toShort() + Long::class -> rawValue.toLong() + Byte::class -> rawValue.toByte() + UInt::class -> rawValue.toUInt() + UShort::class -> rawValue.toUShort() + ULong::class -> rawValue.toULong() + else -> rawValue + } + +/** + * This method is useful for creating a GoUtModel with a default value. + */ +fun GoTypeId.goDefaultValueModel(): GoUtModel = when (this) { + is GoPrimitiveTypeId -> when (this) { + goByteTypeId -> GoUtPrimitiveModel("0".toUByte(), this) + goBoolTypeId -> GoUtPrimitiveModel(false, this) + goComplex64TypeId -> GoUtComplexModel( + goFloat32TypeId.goDefaultValueModel() as GoUtPrimitiveModel, + goFloat32TypeId.goDefaultValueModel() as GoUtPrimitiveModel, + this + ) + + goComplex128TypeId -> GoUtComplexModel( + goFloat64TypeId.goDefaultValueModel() as GoUtPrimitiveModel, + goFloat64TypeId.goDefaultValueModel() as GoUtPrimitiveModel, + this + ) + + goFloat32TypeId -> GoUtPrimitiveModel(0.0f, this) + goFloat64TypeId -> GoUtPrimitiveModel(0.0, this) + goInt8TypeId -> GoUtPrimitiveModel("0".toByte(), this) + goInt16TypeId -> GoUtPrimitiveModel("0".toShort(), this) + goInt32TypeId -> GoUtPrimitiveModel("0".toInt(), this) + goIntTypeId -> if (intSize == 32) { + GoUtPrimitiveModel("0".toInt(), this) + } else { + GoUtPrimitiveModel("0".toLong(), this) + } + + goInt64TypeId -> GoUtPrimitiveModel("0".toLong(), this) + + goRuneTypeId -> GoUtPrimitiveModel("0".toInt(), this) + goStringTypeId -> GoUtPrimitiveModel("", this) + goUint8TypeId -> GoUtPrimitiveModel("0".toUByte(), this) + goUint16TypeId -> GoUtPrimitiveModel("0".toUShort(), this) + goUint32TypeId -> GoUtPrimitiveModel("0".toUInt(), this) + goUintTypeId -> if (intSize == 32) { + GoUtPrimitiveModel("0".toUInt(), this) + } else { + GoUtPrimitiveModel("0".toULong(), this) + } + + goUint64TypeId -> GoUtPrimitiveModel("0".toULong(), this) + goUintPtrTypeId -> GoUtPrimitiveModel("0".toULong(), this) + + else -> error("Generating Go default value model for ${this.javaClass} is not supported") + } + + is GoArrayTypeId -> GoUtArrayModel( + value = (0 until this.length) + .map { this.elementTypeId!!.goDefaultValueModel() } + .toTypedArray(), + typeId = this, + ) + + is GoStructTypeId -> GoUtStructModel(linkedMapOf(), this) + is GoSliceTypeId -> GoUtNilModel(this) + is GoMapTypeId -> GoUtNilModel(this) + is GoChanTypeId -> GoUtNilModel(this) + is GoPointerTypeId -> GoUtNilModel(this) + is GoNamedTypeId -> GoUtNamedModel(this.underlyingTypeId.goDefaultValueModel(), this) + is GoInterfaceTypeId -> GoUtNilModel(this) + else -> error("Generating Go default value model for ${this.javaClass} is not supported") +} + +fun GoTypeId.getAllVisibleNamedTypes(goPackage: GoPackage, visitedTypes: MutableSet): Set { + if (visitedTypes.contains(this)) { + return emptySet() + } + visitedTypes.add(this) + return when (this) { + is GoNamedTypeId -> if (this.sourcePackage == goPackage || this.sourcePackage.isBuiltin || this.exported()) { + setOf(this) + underlyingTypeId.getAllVisibleNamedTypes(goPackage, visitedTypes) + } else { + emptySet() + } + + is GoStructTypeId -> fields.fold(emptySet()) { acc: Set, field -> + acc + (field.declaringType).getAllVisibleNamedTypes(goPackage, visitedTypes) + } + + is GoArrayTypeId, is GoSliceTypeId, is GoChanTypeId, is GoPointerTypeId -> + elementTypeId!!.getAllVisibleNamedTypes(goPackage, visitedTypes) + + is GoMapTypeId -> keyTypeId.getAllVisibleNamedTypes(goPackage, visitedTypes) + + elementTypeId!!.getAllVisibleNamedTypes(goPackage, visitedTypes) + + is GoInterfaceTypeId -> implementations.fold(emptySet()) { acc, type -> + acc + type.getAllVisibleNamedTypes(goPackage, visitedTypes) + } + + else -> emptySet() + } +} + +fun List.getAllVisibleNamedTypes(goPackage: GoPackage): Set { + val visitedTypes = mutableSetOf() + return this.fold(emptySet()) { acc, type -> + acc + type.getAllVisibleNamedTypes(goPackage, visitedTypes) + } +} + +fun GoNamedTypeId.getRequiredPackages(destinationPackage: GoPackage): Set = + if (!this.sourcePackage.isBuiltin && this.sourcePackage != destinationPackage) { + setOf(this.sourcePackage) + } else { + emptySet() + } \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt new file mode 100644 index 0000000000..e5a8635f27 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt @@ -0,0 +1,93 @@ +package org.utbot.go.api.util + +import org.utbot.go.api.* +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoUtModel +import org.utbot.go.worker.* + +fun GoUtModel.isNaNOrInf(): Boolean = this is GoUtFloatNaNModel || this is GoUtFloatInfModel + +fun GoUtModel.doesNotContainNaNOrInf(): Boolean { + if (this.isNaNOrInf()) return false + val asComplexModel = (this as? GoUtComplexModel) ?: return true + return !(asComplexModel.realValue.isNaNOrInf() || asComplexModel.imagValue.isNaNOrInf()) +} + +fun GoUtModel.containsNaNOrInf(): Boolean = !this.doesNotContainNaNOrInf() + +fun GoUtModel.convertToRawValue(destinationPackage: GoPackage, aliases: Map): RawValue = + when (val model = this) { + is GoUtComplexModel -> PrimitiveValue( + model.typeId.getRelativeName(destinationPackage, aliases), + "${model.realValue}@${model.imagValue}" + ) + + is GoUtFloatInfModel -> PrimitiveValue( + model.typeId.getRelativeName(destinationPackage, aliases), + this.toString() + ) + + is GoUtFloatNaNModel -> PrimitiveValue( + model.typeId.getRelativeName(destinationPackage, aliases), + this.toString() + ) + + is GoUtNamedModel -> NamedValue( + model.typeId.getRelativeName(destinationPackage, aliases), + model.value.convertToRawValue(destinationPackage, aliases) + ) + + is GoUtArrayModel -> ArrayValue( + model.typeId.getRelativeName(destinationPackage, aliases), + model.typeId.elementTypeId!!.getRelativeName(destinationPackage, aliases), + model.length, + model.getElements().map { it.convertToRawValue(destinationPackage, aliases) } + ) + + is GoUtSliceModel -> SliceValue( + model.typeId.getRelativeName(destinationPackage, aliases), + model.typeId.elementTypeId!!.getRelativeName(destinationPackage, aliases), + model.length, + model.getElements().map { it.convertToRawValue(destinationPackage, aliases) } + ) + + is GoUtMapModel -> MapValue( + model.typeId.getRelativeName(destinationPackage, aliases), + model.typeId.keyTypeId.getRelativeName(destinationPackage, aliases), + model.typeId.elementTypeId!!.getRelativeName(destinationPackage, aliases), + model.value.entries.map { + val key = it.key.convertToRawValue(destinationPackage, aliases) + val value = it.value.convertToRawValue(destinationPackage, aliases) + MapValue.KeyValue(key, value) + } + ) + + is GoUtStructModel -> StructValue( + model.typeId.getRelativeName(destinationPackage, aliases), + model.value.map { (fieldId, model) -> + StructValue.FieldValue( + fieldId.name, + model.convertToRawValue(destinationPackage, aliases), + fieldId.isExported + ) + } + ) + + is GoUtChanModel -> ChanValue( + model.typeId.getRelativeName(destinationPackage, aliases), + model.typeId.elementTypeId!!.getRelativeName(destinationPackage, aliases), + model.typeId.direction.name, + model.value.size, + model.getElements().map { it.convertToRawValue(destinationPackage, aliases) } + ) + + is GoUtPointerModel -> PointerValue( + model.typeId.getRelativeName(destinationPackage, aliases), + model.typeId.elementTypeId!!.getRelativeName(destinationPackage, aliases), + model.value.convertToRawValue(destinationPackage, aliases) + ) + + is GoUtPrimitiveModel -> PrimitiveValue(model.typeId.name, model.value.toString()) + is GoUtNilModel -> NilValue(model.typeId.getRelativeName(destinationPackage, aliases)) + else -> error("Converting ${model.javaClass} to RawValue is not supported") + } \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/framework/api/go/GoApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/framework/api/go/GoApi.kt new file mode 100644 index 0000000000..2602ed62e2 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/framework/api/go/GoApi.kt @@ -0,0 +1,55 @@ +package org.utbot.go.framework.api.go + +/** + * Parent class for all Go types. + * + * To see its children check GoTypesApi.kt at org.utbot.go.api. + */ +abstract class GoTypeId( + val name: String, + val elementTypeId: GoTypeId? = null, + val implementsError: Boolean = false +) { + open val sourcePackage: GoPackage = GoPackage("", "") + abstract val canonicalName: String + + abstract fun getRelativeName(destinationPackage: GoPackage, aliases: Map): String + override fun toString(): String = canonicalName +} + +/** + * Parent class for all Go models. + * + * To see its children check GoUtModelsApi.kt at org.utbot.go.api. + */ +abstract class GoUtModel( + open val typeId: GoTypeId, +) { + abstract fun getRequiredPackages(destinationPackage: GoPackage): Set + abstract fun isComparable(): Boolean +} + +/** + * Class for Go package. + */ +data class GoPackage( + val name: String, + val path: String +) { + val isBuiltin = name == "" && path == "" +} + +/** + * Class for Go import. + */ +data class GoImport( + val goPackage: GoPackage, + val alias: String? = null +) { + override fun toString(): String { + if (alias == null) { + return "\"${goPackage.path}\"" + } + return "$alias \"${goPackage.path}\"" + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoArrayValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoArrayValueProvider.kt new file mode 100644 index 0000000000..5f68f1a868 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoArrayValueProvider.kt @@ -0,0 +1,58 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoArrayTypeId +import org.utbot.go.api.GoUtArrayModel +import org.utbot.go.api.util.goDefaultValueModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoArrayValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoArrayTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequence { + type.let { it as GoArrayTypeId }.also { arrayType -> + val elementType = arrayType.elementTypeId!! + yield( + Seed.Recursive( + construct = Routine.Create((0 until arrayType.length).map { elementType }) { values -> + GoUtArrayModel( + value = values.toTypedArray(), + typeId = arrayType, + ) + }, + modify = sequence { + val probShuffle = description.configuration.probCollectionShuffleInsteadResultMutation + val numberOfShuffles = if (probShuffle != 100) { + arrayType.length * probShuffle / (100 - probShuffle) + } else { + 1 + } + if (probShuffle != 100) { + (0 until arrayType.length).forEach { index -> + yield(Routine.Call(listOf(elementType)) { self, values -> + val model = self as GoUtArrayModel + val value = values.first() + model.value[index] = value + }) + } + } + repeat(numberOfShuffles) { + yield(Routine.Call(emptyList()) { self, _ -> + val model = self as GoUtArrayModel + model.value.shuffle() + }) + } + }, + empty = Routine.Empty { + arrayType.goDefaultValueModel() + } + ) + ) + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoChanValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoChanValueProvider.kt new file mode 100644 index 0000000000..2a5d211b24 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoChanValueProvider.kt @@ -0,0 +1,34 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoChanTypeId +import org.utbot.go.api.GoUtChanModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoChanValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoChanTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequence { + type.let { it as GoChanTypeId }.also { chanType -> + yield( + Seed.Collection( + construct = Routine.Collection { + GoUtChanModel( + value = arrayOfNulls(it), + typeId = chanType, + ) + }, + modify = Routine.ForEach(listOf(chanType.elementTypeId!!)) { self, i, values -> + val model = self as GoUtChanModel + model.value[i] = values.first() + } + ) + ) + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoConstantValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoConstantValueProvider.kt new file mode 100644 index 0000000000..7103ff0a49 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoConstantValueProvider.kt @@ -0,0 +1,148 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.IEEE754Value +import org.utbot.fuzzing.seeds.StringValue +import org.utbot.go.GoDescription +import org.utbot.go.api.GoPrimitiveTypeId +import org.utbot.go.api.GoUtPrimitiveModel +import org.utbot.go.api.util.* +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoConstantValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type in goSupportedConstantTypes + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = sequence { + type.let { it as GoPrimitiveTypeId }.also { primitiveType -> + val constants = description.functionUnderTest.constants + val primitives: List> = (constants[primitiveType] ?: emptyList()).mapNotNull { + when (primitiveType) { + goRuneTypeId, goIntTypeId, goInt8TypeId, goInt16TypeId, goInt32TypeId, goInt64TypeId -> + when (primitiveType) { + goInt8TypeId -> Seed.Known(BitVectorValue.fromValue(it)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toByte(), + primitiveType + ) + } + + goInt16TypeId -> Seed.Known(BitVectorValue.fromValue(it)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toShort(), + primitiveType + ) + } + + goInt32TypeId, goRuneTypeId -> Seed.Known(BitVectorValue.fromValue(it)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toInt(), + primitiveType + ) + } + + goIntTypeId -> Seed.Known(BitVectorValue.fromValue(it)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + if (intSize == 32) obj.toInt() else obj.toLong(), + primitiveType + ) + } + + goInt64TypeId -> Seed.Known(BitVectorValue.fromValue(it)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toLong(), + primitiveType + ) + } + + else -> return@sequence + } + + goByteTypeId, goUintTypeId, goUintPtrTypeId, goUint8TypeId, goUint16TypeId, goUint32TypeId, goUint64TypeId -> + when (primitiveType) { + goByteTypeId, goUint8TypeId -> { + val uint8AsLong = (it as UByte).toLong() + Seed.Known(BitVectorValue.fromValue(uint8AsLong)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toUByte(), + primitiveType + ) + } + } + + goUint16TypeId -> { + val uint16AsLong = (it as UShort).toLong() + Seed.Known(BitVectorValue.fromValue(uint16AsLong)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toUShort(), + primitiveType + ) + } + } + + goUint32TypeId -> { + val uint32AsLong = (it as UInt).toLong() + Seed.Known(BitVectorValue.fromValue(uint32AsLong)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toUInt(), + primitiveType + ) + } + } + + goUintTypeId, goUintPtrTypeId -> { + val uintAsLong = if (intSize == 32) (it as UInt).toLong() else (it as ULong).toLong() + Seed.Known(BitVectorValue.fromValue(uintAsLong)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + if (intSize == 32) obj.toUInt() else obj.toULong(), + primitiveType + ) + } + } + + goUint64TypeId -> { + val uint64AsLong = (it as ULong).toLong() + Seed.Known(BitVectorValue.fromValue(uint64AsLong)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toULong(), + primitiveType + ) + } + } + + else -> return@sequence + } + + goFloat32TypeId -> Seed.Known(IEEE754Value.fromValue(it)) { obj: IEEE754Value -> + GoUtPrimitiveModel( + obj.toFloat(), + primitiveType + ) + } + + goFloat64TypeId -> Seed.Known(IEEE754Value.fromValue(it)) { obj: IEEE754Value -> + GoUtPrimitiveModel( + obj.toDouble(), + primitiveType + ) + } + + goStringTypeId -> Seed.Known(StringValue(it as String)) { obj: StringValue -> + GoUtPrimitiveModel( + obj.value, + primitiveType + ) + } + + else -> null + } + } + + primitives.forEach { seed -> + yield(seed) + } + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoInterfaceValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoInterfaceValueProvider.kt new file mode 100644 index 0000000000..205488c724 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoInterfaceValueProvider.kt @@ -0,0 +1,30 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoInterfaceTypeId +import org.utbot.go.api.GoUtNilModel +import org.utbot.go.api.util.goDefaultValueModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoInterfaceValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoInterfaceTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = sequence { + type.let { it as GoInterfaceTypeId }.also { interfaceTypeId -> + interfaceTypeId.implementations.forEach { + yield(Seed.Recursive( + construct = Routine.Create(listOf(it)) { values -> + values.first() + }, + empty = Routine.Empty { + interfaceTypeId.goDefaultValueModel() + } + )) + } + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoMapValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoMapValueProvider.kt new file mode 100644 index 0000000000..bcf2ab1af2 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoMapValueProvider.kt @@ -0,0 +1,39 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoMapTypeId +import org.utbot.go.api.GoUtMapModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoMapValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoMapTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequence { + type.let { it as GoMapTypeId }.also { mapType -> + yield( + Seed.Collection( + construct = Routine.Collection { + GoUtMapModel( + value = mutableMapOf(), + typeId = mapType + ) + }, + modify = Routine.ForEach( + listOf( + mapType.keyTypeId, + mapType.elementTypeId!! + ) + ) { self, _, values -> + val model = self as GoUtMapModel + model.value[values[0]] = values[1] + } + ) + ) + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoNamedValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoNamedValueProvider.kt new file mode 100644 index 0000000000..b4455a0eec --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoNamedValueProvider.kt @@ -0,0 +1,34 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoNamedTypeId +import org.utbot.go.api.GoUtNamedModel +import org.utbot.go.api.util.goDefaultValueModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoNamedValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoNamedTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = sequence { + type.let { it as GoNamedTypeId }.also { namedType -> + yield(Seed.Recursive(construct = Routine.Create(listOf(namedType.underlyingTypeId)) { values -> + GoUtNamedModel( + value = values.first(), + typeId = namedType, + ) + }, modify = sequence { + yield(Routine.Call(listOf(namedType.underlyingTypeId)) { self, values -> + val model = self as GoUtNamedModel + val value = values.first() + model.value = value + }) + }, empty = Routine.Empty { + namedType.goDefaultValueModel() + })) + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoNilValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoNilValueProvider.kt new file mode 100644 index 0000000000..cf9b9b5595 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoNilValueProvider.kt @@ -0,0 +1,21 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.* +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoNilValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = setOf( + GoSliceTypeId::class, + GoMapTypeId::class, + GoChanTypeId::class, + GoPointerTypeId::class, + GoInterfaceTypeId::class + ).any { type::class == it } + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequenceOf(Seed.Simple(GoUtNilModel(type))) +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPointerValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPointerValueProvider.kt new file mode 100644 index 0000000000..17ca9812f0 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPointerValueProvider.kt @@ -0,0 +1,31 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoPointerTypeId +import org.utbot.go.api.GoUtNilModel +import org.utbot.go.api.GoUtPointerModel +import org.utbot.go.api.util.goDefaultValueModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoPointerValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoPointerTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = sequence { + type.let { it as GoPointerTypeId }.also { pointerType -> + yield( + Seed.Recursive( + construct = Routine.Create(listOf(pointerType.elementTypeId!!)) { values -> + GoUtPointerModel(value = values.first(), typeId = pointerType) + }, + empty = Routine.Empty { + pointerType.goDefaultValueModel() + } + ) + ) + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesValueProvider.kt new file mode 100644 index 0000000000..b2b5eab50e --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesValueProvider.kt @@ -0,0 +1,214 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.seeds.* +import org.utbot.go.GoDescription +import org.utbot.go.api.* +import org.utbot.go.api.util.* +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import java.util.* +import kotlin.math.sign + +object GoPrimitivesValueProvider : ValueProvider { + private val random = Random(0) + + override fun accept(type: GoTypeId): Boolean = type in goPrimitives + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequence { + type.let { it as GoPrimitiveTypeId }.also { primitiveType -> + val primitives: List> = when (primitiveType) { + goBoolTypeId -> listOf( + Seed.Known(Bool.FALSE.invoke()) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toBoolean(), + primitiveType + ) + }, + Seed.Known(Bool.TRUE.invoke()) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toBoolean(), + primitiveType + ) + } + ) + + goRuneTypeId, goIntTypeId, goInt8TypeId, goInt16TypeId, goInt32TypeId, goInt64TypeId -> Signed.values() + .map { + when (type) { + goInt8TypeId -> Seed.Known(it.invoke(8)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toByte(), + primitiveType + ) + } + + goInt16TypeId -> Seed.Known(it.invoke(16)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toShort(), + primitiveType + ) + } + + goInt32TypeId, goRuneTypeId -> Seed.Known(it.invoke(32)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toInt(), + primitiveType + ) + } + + goIntTypeId -> Seed.Known(it.invoke(intSize)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + if (intSize == 32) obj.toInt() else obj.toLong(), + primitiveType + ) + } + + goInt64TypeId -> Seed.Known(it.invoke(64)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toLong(), + primitiveType + ) + } + + else -> return@sequence + } + } + + goByteTypeId, goUintTypeId, goUintPtrTypeId, goUint8TypeId, goUint16TypeId, goUint32TypeId, goUint64TypeId -> Unsigned.values() + .map { + when (type) { + goByteTypeId, goUint8TypeId -> Seed.Known(it.invoke(8)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toUByte(), + primitiveType + ) + } + + goUint16TypeId -> Seed.Known(it.invoke(16)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toUShort(), + primitiveType + ) + } + + goUint32TypeId -> Seed.Known(it.invoke(32)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toUInt(), + primitiveType + ) + } + + goUintTypeId, goUintPtrTypeId -> Seed.Known(it.invoke(intSize)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + if (intSize == 32) obj.toUInt() else obj.toULong(), + primitiveType + ) + } + + goUint64TypeId -> Seed.Known(it.invoke(64)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toULong(), + primitiveType + ) + } + + else -> return@sequence + } + } + + goFloat32TypeId -> generateFloat32Seeds(primitiveType) + + goFloat64TypeId -> generateFloat64Seeds(primitiveType) + + goComplex64TypeId -> generateComplexSeeds(primitiveType, goFloat32TypeId) + + goComplex128TypeId -> generateComplexSeeds(primitiveType, goFloat64TypeId) + + goStringTypeId -> listOf( + Seed.Known(StringValue("")) { obj: StringValue -> + GoUtPrimitiveModel( + obj.value, + primitiveType + ) + }, + Seed.Known(StringValue("hello")) { obj: StringValue -> + GoUtPrimitiveModel( + obj.value, + primitiveType + ) + }) + + else -> emptyList() + } + + primitives.forEach { seed -> + yield(seed) + } + } + } + + private fun generateFloat32Seeds(typeId: GoPrimitiveTypeId): List> { + return listOf( + Seed.Known(IEEE754Value.fromFloat(random.nextFloat())) { obj: IEEE754Value -> + val d = obj.toFloat() + if (d.isInfinite()) { + GoUtFloatInfModel(d.sign.toInt(), typeId) + } else if (d.isNaN()) { + GoUtFloatNaNModel(typeId) + } else { + GoUtPrimitiveModel(d, typeId) + } + } + ) + } + + private fun generateFloat64Seeds(typeId: GoPrimitiveTypeId): List> { + return listOf( + Seed.Known(IEEE754Value.fromDouble(random.nextDouble())) { obj: IEEE754Value -> + val d = obj.toDouble() + if (d.isInfinite()) { + GoUtFloatInfModel(d.sign.toInt(), typeId) + } else if (d.isNaN()) { + GoUtFloatNaNModel(typeId) + } else { + GoUtPrimitiveModel(d, typeId) + } + } + ) + } + + private fun generateComplexSeeds( + typeId: GoPrimitiveTypeId, + floatTypeId: GoPrimitiveTypeId + ): List> { + return listOf( + Seed.Recursive( + construct = Routine.Create(listOf(floatTypeId, floatTypeId)) { values -> + GoUtComplexModel( + realValue = values[0] as GoUtPrimitiveModel, + imagValue = values[1] as GoUtPrimitiveModel, + typeId = typeId + ) + }, + modify = sequence { + yield(Routine.Call(listOf(floatTypeId)) { self, values -> + val model = self as GoUtComplexModel + val value = values.first() as GoUtPrimitiveModel + model.realValue = value + }) + }, + empty = Routine.Empty { + GoUtComplexModel( + realValue = GoUtPrimitiveModel(0.0, floatTypeId), + imagValue = GoUtPrimitiveModel(0.0, floatTypeId), + typeId = typeId + ) + } + ) + ) + } +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoSliceValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoSliceValueProvider.kt new file mode 100644 index 0000000000..0aa5313fa1 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoSliceValueProvider.kt @@ -0,0 +1,35 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoSliceTypeId +import org.utbot.go.api.GoUtSliceModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoSliceValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoSliceTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequence { + type.let { it as GoSliceTypeId }.also { sliceType -> + yield( + Seed.Collection( + construct = Routine.Collection { + GoUtSliceModel( + value = arrayOfNulls(it), + typeId = sliceType, + length = it, + ) + }, + modify = Routine.ForEach(listOf(sliceType.elementTypeId!!)) { self, i, values -> + val model = self as GoUtSliceModel + model.value[i] = values.first() + } + ) + ) + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStructValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStructValueProvider.kt new file mode 100644 index 0000000000..b9d9e1369b --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStructValueProvider.kt @@ -0,0 +1,42 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoStructTypeId +import org.utbot.go.api.GoUtStructModel +import org.utbot.go.api.util.goDefaultValueModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoStructValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoStructTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequence { + type.let { it as GoStructTypeId }.also { structType -> + val fields = structType.fields + yield(Seed.Recursive( + construct = Routine.Create(fields.map { it.declaringType }) { values -> + GoUtStructModel( + value = linkedMapOf(*fields.zip(values).toTypedArray()), + typeId = structType, + ) + }, + modify = sequence { + fields.forEachIndexed { index, field -> + yield(Routine.Call(listOf(field.declaringType)) { self, values -> + val model = self as GoUtStructModel + val value = values.first() + model.value[fields[index]] = value + }) + } + }, + empty = Routine.Empty { + structType.goDefaultValueModel() + } + )) + } + } +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt new file mode 100644 index 0000000000..e5262669bd --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt @@ -0,0 +1,272 @@ +package org.utbot.go.gocodeanalyzer + +import com.beust.klaxon.TypeAdapter +import com.beust.klaxon.TypeFor +import org.utbot.go.api.* +import org.utbot.go.api.util.goPrimitives +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoTypeId +import kotlin.reflect.KClass + +data class AnalyzedPrimitiveType( + override val name: String +) : AnalyzedType(name) { + override fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId { + var result = analyzedTypes[index] + if (result == null) { + result = GoPrimitiveTypeId(name = name) + analyzedTypes[index] = result + } + return result + } +} + +data class AnalyzedStructType( + override val name: String, + val fields: List +) : AnalyzedType(name) { + data class AnalyzedField( + val name: String, + val type: String, + val isExported: Boolean + ) + + override fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId { + var result = analyzedTypes[index] + if (result == null) { + result = GoStructTypeId( + name = name, + fields = emptyList() + ) + analyzedTypes[index] = result + result.fields = fields.map { (name, type, isExported) -> + val fieldType = typesToAnalyze[type]!!.toGoTypeId(type, analyzedTypes, typesToAnalyze) + GoFieldId(fieldType, name, isExported) + } + } + return result + } +} + +data class AnalyzedArrayType( + override val name: String, + val elementType: String, + val length: Int +) : AnalyzedType(name) { + override fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId { + var result = analyzedTypes[index] + if (result == null) { + result = GoArrayTypeId( + name = name, + elementTypeId = typesToAnalyze[elementType]!!.toGoTypeId(elementType, analyzedTypes, typesToAnalyze), + length = length + ) + analyzedTypes[index] = result + } + return result + } +} + +data class AnalyzedSliceType( + override val name: String, + val elementType: String, +) : AnalyzedType(name) { + override fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId { + var result = analyzedTypes[index] + if (result == null) { + result = GoSliceTypeId( + name = name, + elementTypeId = typesToAnalyze[elementType]!!.toGoTypeId(elementType, analyzedTypes, typesToAnalyze), + ) + analyzedTypes[index] = result + } + return result + } +} + +data class AnalyzedMapType( + override val name: String, + val keyType: String, + val elementType: String, +) : AnalyzedType(name) { + override fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId { + var result = analyzedTypes[index] + if (result == null) { + result = GoMapTypeId( + name = name, + keyTypeId = typesToAnalyze[keyType]!!.toGoTypeId(keyType, analyzedTypes, typesToAnalyze), + elementTypeId = typesToAnalyze[elementType]!!.toGoTypeId(elementType, analyzedTypes, typesToAnalyze), + ) + analyzedTypes[index] = result + } + return result + } +} + +data class AnalyzedChanType( + override val name: String, + val elementType: String, + val direction: GoChanTypeId.Direction, +) : AnalyzedType(name) { + override fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId { + var result = analyzedTypes[index] + if (result == null) { + result = GoChanTypeId( + name = name, + elementTypeId = typesToAnalyze[elementType]!!.toGoTypeId(elementType, analyzedTypes, typesToAnalyze), + direction = direction + ) + analyzedTypes[index] = result + } + return result + } +} + +data class AnalyzedInterfaceType( + override val name: String, + val implementations: List, +) : AnalyzedType(name) { + override fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId { + var result = analyzedTypes[index] + if (result == null) { + result = GoInterfaceTypeId( + name = name, + implementations = implementations.map { type -> + typesToAnalyze[type]!!.toGoTypeId( + type, + analyzedTypes, + typesToAnalyze + ) + }) + analyzedTypes[index] = result + } + return result + } +} + +data class AnalyzedNamedType( + override val name: String, + val sourcePackage: GoPackage, + val implementsError: Boolean, + val underlyingType: String +) : AnalyzedType(name) { + override fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId { + var result = analyzedTypes[index] + if (result == null) { + result = GoNamedTypeId( + name = name, + sourcePackage = sourcePackage, + implementsError = implementsError, + underlyingTypeId = typesToAnalyze[underlyingType]!!.toGoTypeId( + underlyingType, + analyzedTypes, + typesToAnalyze + ), + ) + analyzedTypes[index] = result + } + return result + } +} + +data class AnalyzedPointerType( + override val name: String, + val elementType: String +) : AnalyzedType(name) { + override fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId { + var result = analyzedTypes[index] + if (result == null) { + result = GoPointerTypeId( + name = name, + elementTypeId = typesToAnalyze[elementType]!!.toGoTypeId(elementType, analyzedTypes, typesToAnalyze), + ) + analyzedTypes[index] = result + } + return result + } +} + +@TypeFor(field = "name", adapter = AnalyzedTypeAdapter::class) +abstract class AnalyzedType(open val name: String) { + abstract fun toGoTypeId( + index: String, + analyzedTypes: MutableMap, + typesToAnalyze: Map + ): GoTypeId +} + +class AnalyzedTypeAdapter : TypeAdapter { + override fun classFor(type: Any): KClass { + val typeName = type as String + return when { + typeName == "interface{}" -> AnalyzedInterfaceType::class + typeName == "struct{}" -> AnalyzedStructType::class + typeName == "map" -> AnalyzedMapType::class + typeName == "[]" -> AnalyzedSliceType::class + typeName == "[_]" -> AnalyzedArrayType::class + typeName == "chan" -> AnalyzedChanType::class + typeName == "*" -> AnalyzedPointerType::class + goPrimitives.map { it.name }.contains(typeName) -> AnalyzedPrimitiveType::class + else -> AnalyzedNamedType::class + } + } +} + +internal data class AnalyzedVariable(val name: String, val type: String) + +internal data class AnalyzedFunction( + val name: String, + val types: Map, + val receiver: AnalyzedVariable?, + val parameters: List, + val resultTypes: List, + val constants: Map>, +) + +internal data class AnalysisResult( + val absoluteFilePath: String, + val sourcePackage: GoPackage, + val analyzedFunctions: List, + val notSupportedFunctionNames: List, + val notFoundFunctionNames: List +) + +internal data class AnalysisResults(val results: List, val intSize: Int) + +class GoParsingSourceCodeAnalysisResultException(s: String, t: Throwable) : Exception(s, t) \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt new file mode 100644 index 0000000000..cb82545292 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt @@ -0,0 +1,9 @@ +package org.utbot.go.gocodeanalyzer + +internal data class AnalysisTarget( + val absoluteFilePath: String, + val targetFunctionNames: List, + val targetMethodNames: List +) + +internal data class AnalysisTargets(val targets: List) \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt new file mode 100644 index 0000000000..e2272e2a6d --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt @@ -0,0 +1,165 @@ +package org.utbot.go.gocodeanalyzer + +import com.beust.klaxon.KlaxonException +import org.utbot.common.FileUtil.extractDirectoryFromArchive +import org.utbot.common.scanForResourcesContaining +import org.utbot.go.api.GoPrimitiveTypeId +import org.utbot.go.api.GoUtDeclaredVariable +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.util.goSupportedConstantTypes +import org.utbot.go.api.util.intSize +import org.utbot.go.api.util.rawValueOfGoPrimitiveTypeToValue +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.util.executeCommandByNewProcessOrFail +import org.utbot.go.util.modifyEnvironment +import org.utbot.go.util.parseFromJsonOrFail +import org.utbot.go.util.writeJsonToFileOrFail +import java.io.File +import java.nio.file.Path + +object GoSourceCodeAnalyzer { + + data class GoSourceFileAnalysisResult( + val functions: List, + val notSupportedFunctionAndMethodNames: List, + val notFoundFunctionAndMethodNames: List + ) + + /** + * Takes maps from paths of Go source files to names of their selected functions and methods. + * + * Returns GoSourceCodeAnalyzerResult. + */ + fun analyzeGoSourceFilesForFunctions( + targetFunctionNamesBySourceFiles: Map>, + targetMethodNamesBySourceFiles: Map>, + goExecutableAbsolutePath: Path, + gopathAbsolutePath: Path + ): Map { + val analysisTargets = AnalysisTargets( + (targetFunctionNamesBySourceFiles.keys + targetMethodNamesBySourceFiles.keys).distinct().map { filePath -> + val targetFunctionNames = targetFunctionNamesBySourceFiles[filePath] ?: emptyList() + val targetMethodNames = targetMethodNamesBySourceFiles[filePath] ?: emptyList() + AnalysisTarget(filePath.toAbsolutePath().toString(), targetFunctionNames, targetMethodNames) + } + ) + val analysisTargetsFileName = createAnalysisTargetsFileName() + val analysisResultsFileName = createAnalysisResultsFileName() + + val goCodeAnalyzerSourceDir = extractGoCodeAnalyzerSourceDirectory() + val analysisTargetsFile = goCodeAnalyzerSourceDir.resolve(analysisTargetsFileName) + val analysisResultsFile = goCodeAnalyzerSourceDir.resolve(analysisResultsFileName) + + val goCodeAnalyzerRunCommand = listOf( + goExecutableAbsolutePath.toString(), "run" + ) + getGoCodeAnalyzerSourceFilesNames() + listOf( + "-targets", + analysisTargetsFileName, + "-results", + analysisResultsFileName, + ) + + try { + writeJsonToFileOrFail(analysisTargets, analysisTargetsFile) + val environment = modifyEnvironment(goExecutableAbsolutePath, gopathAbsolutePath) + executeCommandByNewProcessOrFail( + goCodeAnalyzerRunCommand, + goCodeAnalyzerSourceDir, + "GoSourceCodeAnalyzer for $analysisTargets", + environment + ) + val analysisResults = parseFromJsonOrFail(analysisResultsFile) + intSize = analysisResults.intSize + return analysisResults.results.map { analysisResult -> + GoUtFile(analysisResult.absoluteFilePath, analysisResult.sourcePackage) to analysisResult + }.associateBy({ (sourceFile, _) -> sourceFile }) { (sourceFile, analysisResult) -> + val functions = analysisResult.analyzedFunctions.map { analyzedFunction -> + val analyzedTypes = mutableMapOf() + analyzedFunction.types.keys.forEach { index -> + analyzedFunction.types[index]!!.toGoTypeId(index, analyzedTypes, analyzedFunction.types) + } + val receiver = analyzedFunction.receiver?.let { receiver -> + GoUtDeclaredVariable( + receiver.name, analyzedTypes[receiver.type]!! + ) + } + val parameters = analyzedFunction.parameters.map { parameter -> + GoUtDeclaredVariable( + parameter.name, analyzedTypes[parameter.type]!! + ) + } + val resultTypes = analyzedFunction.resultTypes.map { result -> + GoUtDeclaredVariable( + result.name, analyzedTypes[result.type]!!, + ) + } + val constants = mutableMapOf>() + analyzedFunction.constants.map { (type, rawValues) -> + val typeId = GoPrimitiveTypeId(type) + if (typeId !in goSupportedConstantTypes) { + error("Constants extraction: $type is a unsupported constant type") + } + val values = rawValues.map { rawValue -> + rawValueOfGoPrimitiveTypeToValue(typeId, rawValue) + } + constants.compute(typeId) { _, v -> if (v == null) values else v + values } + } + GoUtFunction( + analyzedFunction.name, + receiver, + parameters, + resultTypes, + constants, + sourceFile + ) + } + GoSourceFileAnalysisResult( + functions, + analysisResult.notSupportedFunctionNames, + analysisResult.notFoundFunctionNames + ) + } + } catch (exception: KlaxonException) { + throw GoParsingSourceCodeAnalysisResultException( + "An error occurred while parsing the result of the source code analysis.", exception + ) + } finally { + goCodeAnalyzerSourceDir.deleteRecursively() + } + } + + private fun extractGoCodeAnalyzerSourceDirectory(): File { + val sourceDirectoryName = "go_source_code_analyzer" + val classLoader = GoSourceCodeAnalyzer::class.java.classLoader + + val containingResourceFile = classLoader.scanForResourcesContaining(sourceDirectoryName).firstOrNull() ?: error( + "Can't find resource containing $sourceDirectoryName directory." + ) + if (containingResourceFile.extension != "jar") { + error("Resource for $sourceDirectoryName directory is expected to be JAR: others are not supported yet.") + } + + val archiveFilePath = containingResourceFile.toPath() + return extractDirectoryFromArchive(archiveFilePath, sourceDirectoryName)?.toFile() + ?: error("Can't find $sourceDirectoryName directory at the top level of JAR ${archiveFilePath.toAbsolutePath()}.") + } + + private fun getGoCodeAnalyzerSourceFilesNames(): List { + return listOf( + "main.go", + "analyzer_core.go", + "analysis_targets.go", + "analysis_results.go", + "constant_extractor.go" + ) + } + + private fun createAnalysisTargetsFileName(): String { + return "ut_go_analysis_targets.json" + } + + private fun createAnalysisResultsFileName(): String { + return "ut_go_analysis_results.json" + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeinstrumentation/GoPackageInstrumentation.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeinstrumentation/GoPackageInstrumentation.kt new file mode 100644 index 0000000000..8ca0011a6d --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeinstrumentation/GoPackageInstrumentation.kt @@ -0,0 +1,86 @@ +package org.utbot.go.gocodeinstrumentation + +import org.utbot.common.FileUtil +import org.utbot.common.scanForResourcesContaining +import org.utbot.go.util.executeCommandByNewProcessOrFail +import org.utbot.go.util.modifyEnvironment +import org.utbot.go.util.parseFromJsonOrFail +import org.utbot.go.util.writeJsonToFileOrFail +import java.io.File +import java.nio.file.Path + +object GoPackageInstrumentation { + + fun instrumentGoPackage( + testedFunctions: List, + absoluteDirectoryPath: String, + goExecutableAbsolutePath: Path, + gopathAbsolutePath: Path + ): InstrumentationResult { + val instrumentationTarget = InstrumentationTarget(absoluteDirectoryPath, testedFunctions) + val instrumentationTargetFileName = createInstrumentationTargetFileName() + val instrumentationResultFileName = createInstrumentationResultFileName() + + val goPackageInstrumentationSourceDir = extractGoPackageInstrumentationDirectory() + val instrumentationTargetFile = goPackageInstrumentationSourceDir.resolve(instrumentationTargetFileName) + val instrumentationResultFile = goPackageInstrumentationSourceDir.resolve(instrumentationResultFileName) + + val goPackageInstrumentationRunCommand = listOf( + goExecutableAbsolutePath.toString(), "run" + ) + getGoPackageInstrumentationFilesNames() + listOf( + "-target", + instrumentationTargetFile.absolutePath, + "-result", + instrumentationResultFile.absolutePath, + ) + + try { + writeJsonToFileOrFail(instrumentationTarget, instrumentationTargetFile) + val environment = modifyEnvironment(goExecutableAbsolutePath, gopathAbsolutePath) + executeCommandByNewProcessOrFail( + goPackageInstrumentationRunCommand, + goPackageInstrumentationSourceDir, + "GoPackageInstrumentation for $instrumentationTarget", + environment + ) + return parseFromJsonOrFail(instrumentationResultFile) + } finally { + instrumentationTargetFile.delete() + instrumentationResultFile.delete() + goPackageInstrumentationSourceDir.deleteRecursively() + } + } + + private fun extractGoPackageInstrumentationDirectory(): File { + val sourceDirectoryName = "go_package_instrumentation" + val classLoader = GoPackageInstrumentation::class.java.classLoader + + val containingResourceFile = classLoader.scanForResourcesContaining(sourceDirectoryName).firstOrNull() ?: error( + "Can't find resource containing $sourceDirectoryName directory." + ) + if (containingResourceFile.extension != "jar") { + error("Resource for $sourceDirectoryName directory is expected to be JAR: others are not supported yet.") + } + + val archiveFilePath = containingResourceFile.toPath() + return FileUtil.extractDirectoryFromArchive(archiveFilePath, sourceDirectoryName)?.toFile() + ?: error("Can't find $sourceDirectoryName directory at the top level of JAR ${archiveFilePath.toAbsolutePath()}.") + } + + private fun getGoPackageInstrumentationFilesNames(): List { + return listOf( + "main.go", + "instrumentator.go", + "instrumentation_target.go", + "instrumentation_result.go", + ) + } + + private fun createInstrumentationTargetFileName(): String { + return "ut_go_instrumentation_target.json" + } + + private fun createInstrumentationResultFileName(): String { + return "ut_go_instrumentation_result.json" + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeinstrumentation/InstrumentationResult.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeinstrumentation/InstrumentationResult.kt new file mode 100644 index 0000000000..0e36539af1 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeinstrumentation/InstrumentationResult.kt @@ -0,0 +1,7 @@ +package org.utbot.go.gocodeinstrumentation + +data class InstrumentationResult( + val absolutePathToInstrumentedPackage: String, + val absolutePathToInstrumentedModule: String, + val testedFunctionsToCounters: Map> +) \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeinstrumentation/InstrumentationTarget.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeinstrumentation/InstrumentationTarget.kt new file mode 100644 index 0000000000..2319b8333b --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeinstrumentation/InstrumentationTarget.kt @@ -0,0 +1,3 @@ +package org.utbot.go.gocodeinstrumentation + +internal data class InstrumentationTarget(val absolutePackagePath: String, val testedFunctions: List) \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/imports/GoImportsResolver.kt b/utbot-go/src/main/kotlin/org/utbot/go/imports/GoImportsResolver.kt new file mode 100644 index 0000000000..1a20c3ef8c --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/imports/GoImportsResolver.kt @@ -0,0 +1,41 @@ +package org.utbot.go.imports + +import org.utbot.go.api.util.getAllVisibleNamedTypes +import org.utbot.go.framework.api.go.GoImport +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoTypeId + +object GoImportsResolver { + + fun resolveImportsBasedOnTypes( + types: List, + sourcePackage: GoPackage, + busyImports: Set = emptySet() + ): Set = resolveImportsBasedOnRequiredPackages( + types.getAllVisibleNamedTypes(sourcePackage).map { it.sourcePackage }.toSet(), sourcePackage, busyImports + ) + + fun resolveImportsBasedOnRequiredPackages( + requiredPackages: Set, + sourcePackage: GoPackage, + busyImports: Set = emptySet() + ): Set { + val result = busyImports.associateBy { it.goPackage }.toMutableMap() + val busyAliases = busyImports.map { it.alias ?: it.goPackage.name }.toMutableSet() + requiredPackages.distinct().filter { it != sourcePackage && !it.isBuiltin && it !in result } + .forEach { goPackage -> + val alias = if (goPackage.name in busyAliases) { + var n = 1 + while (goPackage.name + n in busyAliases) { + n++ + } + goPackage.name + n + } else { + null + } + busyAliases += alias ?: goPackage.name + result[goPackage] = GoImport(goPackage, alias) + } + return result.values.toSet() + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt b/utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt new file mode 100644 index 0000000000..01adeea072 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt @@ -0,0 +1,133 @@ +package org.utbot.go.logic + +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.api.util.intSize +import org.utbot.go.gocodeanalyzer.GoSourceCodeAnalyzer +import org.utbot.go.gocodeinstrumentation.GoPackageInstrumentation +import org.utbot.go.gocodeinstrumentation.InstrumentationResult +import org.utbot.go.simplecodegeneration.GoTestCasesCodeGenerator +import java.nio.file.Path + +abstract class AbstractGoUtTestsGenerationController { + + fun generateTests( + selectedFunctionNamesBySourceFiles: Map>, + selectedMethodNamesBySourceFiles: Map>, + testsGenerationConfig: GoUtTestsGenerationConfig, + isCanceled: () -> Boolean = { false } + ) { + if (!onSourceCodeAnalysisStart(selectedFunctionNamesBySourceFiles, selectedMethodNamesBySourceFiles)) return + val analysisResults = GoSourceCodeAnalyzer.analyzeGoSourceFilesForFunctions( + selectedFunctionNamesBySourceFiles, + selectedMethodNamesBySourceFiles, + testsGenerationConfig.goExecutableAbsolutePath, + testsGenerationConfig.gopathAbsolutePath + ) + + if (!onSourceCodeAnalysisFinished(analysisResults)) return + + if (!onPackageInstrumentationStart()) return + val instrumentationResults = mutableMapOf() + analysisResults.forEach { (file, analysisResult) -> + val absoluteDirectoryPath = file.absoluteDirectoryPath + if (instrumentationResults[absoluteDirectoryPath] == null) { + instrumentationResults[absoluteDirectoryPath] = GoPackageInstrumentation.instrumentGoPackage( + testedFunctions = analysisResult.functions.map { it.name }, + absoluteDirectoryPath = absoluteDirectoryPath, + goExecutableAbsolutePath = testsGenerationConfig.goExecutableAbsolutePath, + gopathAbsolutePath = testsGenerationConfig.gopathAbsolutePath + ) + } + } + if (!onPackageInstrumentationFinished()) return + + val numOfFunctions = analysisResults.values + .map { it.functions.size } + .reduce { acc, numOfFunctions -> acc + numOfFunctions } + val functionTimeoutStepMillis = testsGenerationConfig.allFunctionExecutionTimeoutMillis / numOfFunctions + var startTimeMillis = System.currentTimeMillis() + val testCasesBySourceFiles = analysisResults.mapValues { (sourceFile, analysisResult) -> + val functions = analysisResult.functions + if (!onTestCasesGenerationForGoSourceFileFunctionsStart(sourceFile, functions)) return + val (absolutePathToInstrumentedPackage, absolutePathToInstrumentedModule, needToCoverLines) = + instrumentationResults[sourceFile.absoluteDirectoryPath]!! + GoTestCasesGenerator.generateTestCasesForGoSourceFileFunctions( + sourceFile, + functions, + absolutePathToInstrumentedPackage, + absolutePathToInstrumentedModule, + needToCoverLines, + testsGenerationConfig + ) { index -> isCanceled() || System.currentTimeMillis() - (startTimeMillis + (index + 1) * functionTimeoutStepMillis) > 0 } + .also { + startTimeMillis += functionTimeoutStepMillis * functions.size + if (!onTestCasesGenerationForGoSourceFileFunctionsFinished(sourceFile, it)) return + } + } + + testCasesBySourceFiles.forEach { (sourceFile, testCases) -> + if (!onTestCasesFileCodeGenerationStart(sourceFile, testCases)) return + val generatedTestsFileCode = GoTestCasesCodeGenerator.generateTestCasesFileCode(sourceFile, testCases) + if (!onTestCasesFileCodeGenerationFinished(sourceFile, generatedTestsFileCode)) return + } + } + + protected abstract fun onSourceCodeAnalysisStart( + targetFunctionNamesBySourceFiles: Map>, + targetMethodNamesBySourceFiles: Map>, + ): Boolean + + protected abstract fun onSourceCodeAnalysisFinished( + analysisResults: Map + ): Boolean + + protected abstract fun onPackageInstrumentationStart(): Boolean + + protected abstract fun onPackageInstrumentationFinished(): Boolean + + protected abstract fun onTestCasesGenerationForGoSourceFileFunctionsStart( + sourceFile: GoUtFile, + functions: List + ): Boolean + + protected abstract fun onTestCasesGenerationForGoSourceFileFunctionsFinished( + sourceFile: GoUtFile, + testCases: List + ): Boolean + + protected abstract fun onTestCasesFileCodeGenerationStart( + sourceFile: GoUtFile, + testCases: List + ): Boolean + + protected abstract fun onTestCasesFileCodeGenerationFinished( + sourceFile: GoUtFile, + generatedTestsFileCode: String + ): Boolean + + protected fun generateMissingSelectedFunctionsListMessage( + analysisResults: Map, + ): String? { + val missingSelectedFunctions = analysisResults.filter { (_, analysisResult) -> + analysisResult.notSupportedFunctionAndMethodNames.isNotEmpty() || analysisResult.notFoundFunctionAndMethodNames.isNotEmpty() + } + if (missingSelectedFunctions.isEmpty()) { + return null + } + return missingSelectedFunctions.map { (sourceFile, analysisResult) -> + val notSupportedFunctions = analysisResult.notSupportedFunctionAndMethodNames.joinToString(separator = ", ") + val notFoundFunctions = analysisResult.notFoundFunctionAndMethodNames.joinToString(separator = ", ") + val messageSb = StringBuilder() + messageSb.append("File ${sourceFile.absolutePath}") + if (notSupportedFunctions.isNotEmpty()) { + messageSb.append("\n-- contains currently unsupported functions: $notSupportedFunctions") + } + if (notFoundFunctions.isNotEmpty()) { + messageSb.append("\n-- does not contain functions: $notFoundFunctions") + } + messageSb.toString() + }.joinToString(separator = "\n\n", prefix = "\n\n", postfix = "\n\n") + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt new file mode 100644 index 0000000000..5c26dc71ad --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt @@ -0,0 +1,159 @@ +package org.utbot.go.logic + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.catch +import mu.KotlinLogging +import org.utbot.common.isWindows +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.go.CoveredLines +import org.utbot.go.GoEngine +import org.utbot.go.api.ExecutionResults +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.imports.GoImportsResolver +import org.utbot.go.util.executeCommandByNewProcessOrFail +import org.utbot.go.util.modifyEnvironment +import org.utbot.go.worker.GoWorker +import org.utbot.go.worker.GoWorkerCodeGenerationHelper +import java.io.File +import java.net.ServerSocket +import java.nio.file.Path +import kotlin.system.measureTimeMillis + +val logger = KotlinLogging.logger {} + +object GoTestCasesGenerator { + + fun generateTestCasesForGoSourceFileFunctions( + sourceFile: GoUtFile, + functions: List, + absolutePathToInstrumentedPackage: String, + absolutePathToInstrumentedModule: String, + needToCoverLines: Map>, + testsGenerationConfig: GoUtTestsGenerationConfig, + timeoutExceededOrIsCanceled: (index: Int) -> Boolean = { false }, + ): List = ServerSocket(0).use { serverSocket -> + val allTestCases = mutableListOf() + var testFile: File? = null + try { + // creating files for workers + val types = functions.flatMap { it.parameters }.map { it.type } + val imports = GoImportsResolver.resolveImportsBasedOnTypes( + types, sourceFile.sourcePackage, GoWorkerCodeGenerationHelper.alwaysRequiredImports + ) + val aliases = imports.filter { it.alias != null }.associate { it.goPackage to it.alias } + GoWorkerCodeGenerationHelper.createFileToExecute( + sourceFile, + functions, + absolutePathToInstrumentedPackage, + testsGenerationConfig.eachFunctionExecutionTimeoutMillis, + serverSocket.localPort, + imports + ) + GoWorkerCodeGenerationHelper.createFileWithCoverTab( + sourceFile, absolutePathToInstrumentedPackage + ) + + // compiling the test binary + testFile = run { + if (isWindows) { + Path.of(sourceFile.absoluteDirectoryPath, "utbot_go_test.exe") + } else { + Path.of(sourceFile.absoluteDirectoryPath, "utbot_go_test") + } + }.toFile() + val buildCommand = listOf( + testsGenerationConfig.goExecutableAbsolutePath.toString(), "test", "-c", "-o", testFile.absolutePath + ) + val environment = modifyEnvironment( + testsGenerationConfig.goExecutableAbsolutePath, + testsGenerationConfig.gopathAbsolutePath + ) + logger.debug { "Compiling the test binary - started" } + val compilingTestBinaryTime = measureTimeMillis { + executeCommandByNewProcessOrFail( + buildCommand, + File(absolutePathToInstrumentedPackage), + "compiling the test binary to the $testFile file", + environment + ) + } + logger.debug { "Compiling the test binary - completed in [$compilingTestBinaryTime] (ms)" } + + // starting worker processes + logger.debug { "Creation of workers - started" } + val creationOfWorkersStartTime = System.currentTimeMillis() + val workers = runBlocking { + val testFunctionName = GoWorkerCodeGenerationHelper.workerTestFunctionName + val goPackage = sourceFile.sourcePackage + val sourceFileDir = File(sourceFile.absoluteDirectoryPath) + return@runBlocking (1..testsGenerationConfig.numberOfFuzzingProcess).map { + async(Dispatchers.IO) { + GoWorker.createWorker( + testFunctionName, + testFile.absolutePath, + goPackage, + testsGenerationConfig.goExecutableAbsolutePath, + testsGenerationConfig.gopathAbsolutePath, + sourceFileDir, + serverSocket + ) + } + }.awaitAll() + } + logger.debug { "Creation of workers - completed in [${System.currentTimeMillis() - creationOfWorkersStartTime}] (ms)" } + + // fuzzing + functions.forEachIndexed { index, function -> + if (timeoutExceededOrIsCanceled(index)) return@forEachIndexed + val coveredLinesToExecutionResults = mutableMapOf() + val engine = GoEngine( + workers = workers, + functionUnderTest = function, + needToCoverLines = needToCoverLines[function.name]!!.toSet(), + aliases = aliases, + functionExecutionTimeoutMillis = testsGenerationConfig.eachFunctionExecutionTimeoutMillis, + mode = testsGenerationConfig.mode, + ) { timeoutExceededOrIsCanceled(index) } + logger.info { "Fuzzing for function [${function.name}] - started" } + val totalFuzzingTime = runBlocking { + measureTimeMillis { + engine.fuzzing().catch { + logger.error { "Error in flow: ${it.message}" } + }.collect { + it.entries.forEach { (coveredLines, executionResults) -> + if (coveredLinesToExecutionResults[coveredLines] == null) { + coveredLinesToExecutionResults[coveredLines] = executionResults + } else { + coveredLinesToExecutionResults[coveredLines]!!.update(executionResults) + } + } + } + } + } + val numberOfExecutionsPerSecond = if (totalFuzzingTime / 1000 != 0L) { + (engine.numberOfFunctionExecutions.get() / (totalFuzzingTime / 1000)).toString() + } else { + ">${engine.numberOfFunctionExecutions}" + } + logger.debug { "Number of function executions - [${engine.numberOfFunctionExecutions}] ($numberOfExecutionsPerSecond/sec)" } + val testCases = coveredLinesToExecutionResults.values.flatMap { it.getTestCases() } + logger.info { "Fuzzing for function [${function.name}] - completed in [$totalFuzzingTime] (ms). Generated [${testCases.size}] test cases" } + allTestCases += testCases + } + runBlocking { + workers.map { launch(Dispatchers.IO) { it.close() } }.joinAll() + } + } catch (e: TimeoutException) { + logger.error { e.message } + } catch (e: RuntimeException) { + logger.error { e.message } + } finally { + // delete test file and directory with instrumented packages + testFile?.delete() + File(absolutePathToInstrumentedModule).delete() + } + return allTestCases + } +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/logic/GoUtTestsGenerationConfig.kt b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoUtTestsGenerationConfig.kt new file mode 100644 index 0000000000..40527f1584 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoUtTestsGenerationConfig.kt @@ -0,0 +1,23 @@ +package org.utbot.go.logic + +import java.nio.file.Path + +enum class TestsGenerationMode { + DEFAULT, FUZZING_MODE +} + +class GoUtTestsGenerationConfig( + val goExecutableAbsolutePath: Path, + val gopathAbsolutePath: Path, + val numberOfFuzzingProcess: Int, + val mode: TestsGenerationMode, + val eachFunctionExecutionTimeoutMillis: Long, + val allFunctionExecutionTimeoutMillis: Long +) { + + companion object Constants { + const val DEFAULT_NUMBER_OF_FUZZING_PROCESSES: Int = 8 + const val DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS: Long = 60000 + const val DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS: Long = 1000 + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt new file mode 100644 index 0000000000..a48fe78110 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt @@ -0,0 +1,40 @@ +package org.utbot.go.simplecodegeneration + +import org.utbot.go.framework.api.go.GoImport +import org.utbot.go.framework.api.go.GoPackage + +class GoFileCodeBuilder( + sourcePackage: GoPackage, + imports: Set, +) { + private val packageLine: String = "package ${sourcePackage.name}" + private val importLines: String = importLines(imports) + private val topLevelElements: MutableList = mutableListOf() + + private fun importLines(imports: Set): String { + if (imports.isEmpty()) return "" + if (imports.size == 1) { + return "import ${imports.first()}" + } + + return imports.sortedWith(compareBy { it.goPackage.path }.thenBy { it.alias }) + .joinToString(separator = "", prefix = "import (\n", postfix = ")") { + "\t$it\n" + } + } + + fun buildCodeString(): String { + if (importLines.isEmpty()) { + return "$packageLine\n\n${topLevelElements.joinToString(separator = "\n\n")}" + } + return "$packageLine\n\n$importLines\n\n${topLevelElements.joinToString(separator = "\n\n")}" + } + + fun addTopLevelElements(vararg elements: String) { + topLevelElements.addAll(elements) + } + + fun addTopLevelElements(elements: Iterable) { + topLevelElements.addAll(elements) + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt new file mode 100644 index 0000000000..cd7cbfdc90 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt @@ -0,0 +1,455 @@ +package org.utbot.go.simplecodegeneration + +import org.utbot.go.api.* +import org.utbot.go.api.util.containsNaNOrInf +import org.utbot.go.api.util.goBoolTypeId +import org.utbot.go.api.util.goFloat64TypeId +import org.utbot.go.api.util.goStringTypeId +import org.utbot.go.framework.api.go.GoImport +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import org.utbot.go.imports.GoImportsResolver + +object GoTestCasesCodeGenerator { + + private const val prefixOfVariableNames = "arg" + + private val alwaysRequiredImports = setOf( + GoImport(GoPackage("assert", "github.com/stretchr/testify/assert")), + GoImport(GoPackage("testing", "testing")) + ) + + private data class Variable(val name: String, val type: GoTypeId, val value: GoUtModel) + + fun generateTestCasesFileCode(sourceFile: GoUtFile, testCases: List): String { + val destinationPackage = sourceFile.sourcePackage + val imports = if (testCases.isEmpty() || testCases.all { it.executionResult is GoUtTimeoutExceeded }) { + emptySet() + } else { + val requiredPackages = mutableSetOf() + testCases.forEach { testCase -> + testCase.parametersValues.forEach { + requiredPackages += it.getRequiredPackages(destinationPackage) + } + when (val executionResult = testCase.executionResult) { + is GoUtExecutionCompleted -> executionResult.models.forEach { + requiredPackages += it.getRequiredPackages(destinationPackage) + } + + is GoUtPanicFailure -> requiredPackages += executionResult.panicValue.getRequiredPackages( + destinationPackage + ) + } + } + + GoImportsResolver.resolveImportsBasedOnRequiredPackages( + requiredPackages, destinationPackage, alwaysRequiredImports + ) + } + val fileBuilder = GoFileCodeBuilder(destinationPackage, imports) + val aliases = imports.associate { (goPackage, alias) -> goPackage to alias } + val goUtModelToCodeConverter = GoUtModelToCodeConverter(destinationPackage, aliases) + + fun List.generateTestFunctions( + generateTestFunctionForTestCase: (GoUtFuzzedFunctionTestCase, Int?, GoUtModelToCodeConverter) -> String, + ) { + this.forEachIndexed { testIndex, testCase -> + val testIndexToShow = if (this.size == 1) null else testIndex + 1 + val testFunctionCode = + generateTestFunctionForTestCase(testCase, testIndexToShow, goUtModelToCodeConverter) + fileBuilder.addTopLevelElements(testFunctionCode) + } + } + + testCases.groupBy { it.function }.forEach { (_, functionTestCases) -> + functionTestCases.filter { it.executionResult is GoUtExecutionCompleted } + .generateTestFunctions(::generateTestFunctionForCompletedExecutionTestCase) + functionTestCases.filter { it.executionResult is GoUtPanicFailure } + .generateTestFunctions(::generateTestFunctionForPanicFailureTestCase) + functionTestCases.filter { it.executionResult is GoUtTimeoutExceeded } + .generateTestFunctions(::generateTestFunctionForTimeoutExceededTestCase) + } + + return fileBuilder.buildCodeString() + } + + private fun generateTestFunctionForCompletedExecutionTestCase( + testCase: GoUtFuzzedFunctionTestCase, testIndexToShow: Int?, goUtModelToCodeConverter: GoUtModelToCodeConverter + ): String { + val (fuzzedFunction, executionResult) = testCase + val function = fuzzedFunction.function + + val testFunctionNamePostfix = if (executionResult is GoUtExecutionWithNonNilError) { + "WithNonNilError" + } else { + "" + } + val testIndexToShowString = testIndexToShow ?: "" + val testFunctionSignatureDeclaration = + "func Test${function.name.replaceFirstChar(Char::titlecaseChar)}${testFunctionNamePostfix}ByUtGoFuzzer$testIndexToShowString(t *testing.T)" + + val variables: List = generateVariables(fuzzedFunction) + val variablesDeclarationAndInitialization = + generateVariablesDeclarationAndInitialization(variables, goUtModelToCodeConverter) + + if (function.results.isEmpty()) { + val actualFunctionCall = generateFuzzedFunctionCall(function, variables) + val testFunctionBody = buildString { + if (variablesDeclarationAndInitialization != "\n") { + append(variablesDeclarationAndInitialization) + } + appendLine("\tassert.NotPanics(t, func() {") + appendLine("\t\t$actualFunctionCall") + appendLine("\t})") + + } + return "$testFunctionSignatureDeclaration {\n$testFunctionBody}" + } + + val resultTypes = function.results.map { it.type } + val doResultTypesImplementError = resultTypes.map { it.implementsError } + val errorVariablesNumber = doResultTypesImplementError.count { it } + val commonVariablesNumber = resultTypes.size - errorVariablesNumber + val actualResultVariablesNames = run { + var errorVariablesIndex = 0 + var commonVariablesIndex = 0 + doResultTypesImplementError.map { implementsError -> + if (implementsError) { + "actualErr${if (errorVariablesNumber > 1) errorVariablesIndex++ else ""}" + } else { + "actualVal${if (commonVariablesNumber > 1) commonVariablesIndex++ else ""}" + } + } + } + val actualFunctionCall = generateFuzzedFunctionCallSavedToVariables( + actualResultVariablesNames, fuzzedFunction, variables + ) + + val expectedResultValues = (executionResult as GoUtExecutionCompleted).models + val expectedResultVariables = run { + var errorVariablesIndex = 0 + var commonVariablesIndex = 0 + resultTypes.zip(expectedResultValues).map { (type, value) -> + if (modelIsNilOrBoolOrFloatNanOrFloatInf(value)) { + return@map null + } + val name = if (type.implementsError) { + "expectedErrorMessage${if (errorVariablesNumber > 1) errorVariablesIndex++ else ""}" + } else { + "expectedVal${if (commonVariablesNumber > 1) commonVariablesIndex++ else ""}" + } + Variable(name, type, value) + } + } + + val expectedVariablesDeclarationAndInitialization = + generateVariablesDeclarationAndInitialization(expectedResultVariables, goUtModelToCodeConverter) + + val (assertionName, assertionTParameter) = if (expectedResultValues.size > 1 || expectedResultValues.any { it.isComplexModelAndNeedsSeparateAssertions() }) { + "assertMultiple" to "" + } else { + "assert" to "t, " + } + val allAssertionCalls = + actualResultVariablesNames.zip(expectedResultVariables.map { it?.name ?: "" }).zip(expectedResultValues) + .flatMap { (actualAndExpectedResultVariableName, expectedResultValue) -> + val (actualResultVariableName, expectedResultVariableName) = actualAndExpectedResultVariableName + + val assertionCalls = if (expectedResultValue.isComplexModelAndNeedsSeparateAssertions()) { + listOf( + generateCompletedExecutionAssertionCall( + expectedModel = expectedResultValue, + expectedResultCode = "real($expectedResultVariableName)", + actualResultCode = "real($actualResultVariableName)", + doesReturnTypeImplementError = expectedResultValue.typeId.implementsError, + assertionTParameter + ), generateCompletedExecutionAssertionCall( + expectedModel = expectedResultValue, + expectedResultCode = "imag($expectedResultVariableName)", + actualResultCode = "imag($actualResultVariableName)", + doesReturnTypeImplementError = expectedResultValue.typeId.implementsError, + assertionTParameter + ) + ) + } else { + listOf( + generateCompletedExecutionAssertionCall( + expectedModel = expectedResultValue, + expectedResultCode = expectedResultVariableName, + actualResultCode = actualResultVariableName, + doesReturnTypeImplementError = expectedResultValue.typeId.implementsError, + assertionTParameter + ) + ) + } + assertionCalls.map { "$assertionName.$it" } + } + + val testFunctionBody = buildString { + if (variablesDeclarationAndInitialization != "\n") { + append(variablesDeclarationAndInitialization) + } + append("\t$actualFunctionCall\n\n") + if (expectedVariablesDeclarationAndInitialization != "\n") { + append(expectedVariablesDeclarationAndInitialization) + } + if (expectedResultValues.size > 1 || expectedResultValues.any { it.isComplexModelAndNeedsSeparateAssertions() }) { + append("\tassertMultiple := assert.New(t)\n") + } + allAssertionCalls.forEach { + append("\t$it\n") + } + } + + return "$testFunctionSignatureDeclaration {\n$testFunctionBody}" + } + + private fun GoUtModel.isComplexModelAndNeedsSeparateAssertions(): Boolean = + this is GoUtComplexModel && this.containsNaNOrInf() + + private fun generateCompletedExecutionAssertionCall( + expectedModel: GoUtModel, + expectedResultCode: String, + actualResultCode: String, + doesReturnTypeImplementError: Boolean, + assertionTParameter: String, + ): String { + if (expectedModel is GoUtNilModel || (expectedModel is GoUtNamedModel && expectedModel.value is GoUtNilModel)) { + return "Nil($assertionTParameter$actualResultCode)" + } + if (doesReturnTypeImplementError && (expectedModel is GoUtNamedModel && expectedModel.value.typeId == goStringTypeId)) { + return "ErrorContains($assertionTParameter$actualResultCode, ${expectedResultCode})" + } + if (expectedModel is GoUtPrimitiveModel && expectedModel.typeId == goBoolTypeId) { + return if (expectedModel.value == true) { + "True($assertionTParameter$actualResultCode)" + } else { + "False($assertionTParameter$actualResultCode)" + } + } + if (expectedModel is GoUtFloatNaNModel) { + val castedActualResultCode = generateCastIfNeed(goFloat64TypeId, expectedModel.typeId, actualResultCode) + return "True(${assertionTParameter}math.IsNaN($castedActualResultCode))" + } + if (expectedModel is GoUtFloatInfModel) { + val castedActualResultCode = generateCastIfNeed(goFloat64TypeId, expectedModel.typeId, actualResultCode) + return "True(${assertionTParameter}math.IsInf($castedActualResultCode, ${expectedModel.sign}))" + } + val prefix = if (!expectedModel.isComparable()) "Not" else "" + return "${prefix}Equal($assertionTParameter$expectedResultCode, $actualResultCode)" + } + + private fun generateTestFunctionForPanicFailureTestCase( + testCase: GoUtFuzzedFunctionTestCase, testIndexToShow: Int?, goUtModelToCodeConverter: GoUtModelToCodeConverter + ): String { + val (fuzzedFunction, executionResult) = testCase + val function = fuzzedFunction.function + + val testIndexToShowString = testIndexToShow ?: "" + val testFunctionSignatureDeclaration = + "func Test${function.name.replaceFirstChar(Char::titlecaseChar)}PanicsByUtGoFuzzer$testIndexToShowString(t *testing.T)" + + val variables: List = generateVariables(fuzzedFunction) + val variablesDeclaration = generateVariablesDeclarationAndInitialization(variables, goUtModelToCodeConverter) + + val actualFunctionCall = generateFuzzedFunctionCall(function, variables) + val actualFunctionCallLambda = buildString { + appendLine("func() {") + if (function.results.isNotEmpty()) { + appendLine("\t\t${function.results.joinToString { "_" }} = $actualFunctionCall") + } else { + appendLine("\t\t$actualFunctionCall") + } + append("\t}") + } + val testFunctionBodySb = StringBuilder(variablesDeclaration) + val (expectedPanicValue, isErrorMessage) = (executionResult as GoUtPanicFailure) + val panicValueIsComparable = expectedPanicValue.isComparable() + if (isErrorMessage) { + val errorMessageVariable = Variable("expectedErrorMessage", expectedPanicValue.typeId, expectedPanicValue) + + val errorMessageToGoCode = goUtModelToCodeConverter.toGoCode(errorMessageVariable.value) + testFunctionBodySb.append("\t${errorMessageVariable.name} := ${errorMessageToGoCode}\n\n") + testFunctionBodySb.append("\tassert.PanicsWithError(t, ${errorMessageVariable.name}, $actualFunctionCallLambda)\n") + } else if (panicValueIsComparable || expectedPanicValue is GoUtNilModel) { + val panicValueVariable = Variable("expectedVal", expectedPanicValue.typeId, expectedPanicValue) + + val panicValueToGoCode = goUtModelToCodeConverter.toGoCode(panicValueVariable.value) + testFunctionBodySb.append("\t${panicValueVariable.name} := $panicValueToGoCode\n\n") + testFunctionBodySb.append("\tassert.PanicsWithValue(t, ${panicValueVariable.name}, $actualFunctionCallLambda)\n") + } else { + testFunctionBodySb.append("\tassert.Panics(t, $actualFunctionCallLambda)\n") + } + + val testFunctionBody = testFunctionBodySb.toString() + return "$testFunctionSignatureDeclaration {\n$testFunctionBody}" + } + + private fun generateTestFunctionForTimeoutExceededTestCase( + testCase: GoUtFuzzedFunctionTestCase, + @Suppress("UNUSED_PARAMETER") testIndexToShow: Int?, + goUtModelToCodeConverter: GoUtModelToCodeConverter + ): String { + val (fuzzedFunction, executionResult) = testCase + val functionName = fuzzedFunction.function.name + val fuzzedParametersToString = fuzzedFunction.parametersValues.joinToString { + goUtModelToCodeConverter.toGoCode(it) + } + val actualFunctionCall = "$functionName($fuzzedParametersToString)" + val exceededTimeoutMillis = (executionResult as GoUtTimeoutExceeded).timeoutMillis + return "// $actualFunctionCall exceeded $exceededTimeoutMillis ms timeout" + } + + private fun generateVariables(fuzzedFunction: GoUtFuzzedFunction): List { + val parameters = fuzzedFunction.function.let { function -> + if (function.isMethod) { + listOf(function.receiver!!) + function.parameters + } else { + function.parameters + } + } + val parameterNames = parameters.map { it.name } + val parameterValues = fuzzedFunction.parametersValues + val parametersTypes = parameters.map { it.type } + + val busyVariableNames = parameterNames.filter { it != "" && it != "_" }.toMutableSet() + val variablesNumber = parameterNames.filter { it == "" || it == "_" }.size + var variablesIndex = 1 + + return parameterNames.zip(parametersTypes).zip(parameterValues) + .map { (nameAndType, value) -> + val (name, type) = nameAndType + val variableName = if (name == "" || name == "_") { + if (variablesNumber == 1 && prefixOfVariableNames !in busyVariableNames) { + prefixOfVariableNames + } else { + while ("$prefixOfVariableNames$variablesIndex" in busyVariableNames) { + variablesIndex++ + } + "$prefixOfVariableNames$variablesIndex" + } + } else { + name + } + busyVariableNames.add(variableName) + Variable(variableName, type, value) + } + } + + private fun generateChannelInitialization( + nameOfVariable: String, + model: GoUtChanModel, + goUtModelToCodeConverter: GoUtModelToCodeConverter + ): String = model.getElements().joinToString(separator = "") { + "\t$nameOfVariable <- ${goUtModelToCodeConverter.toGoCode(it)}\n" + } + "\tclose($nameOfVariable)\n" + + private fun generatePointerToPrimitiveInitialization( + nameOfVariable: String, + model: GoUtPrimitiveModel, + goUtModelToCodeConverter: GoUtModelToCodeConverter + ): String = "\t*$nameOfVariable = ${goUtModelToCodeConverter.toGoCodeWithoutTypeName(model)}\n" + + private fun generateVariableDeclaration( + variable: Variable, + goUtModelToCodeConverter: GoUtModelToCodeConverter + ): String { + val (name, type, value) = variable + return if (type.implementsError && (value is GoUtNamedModel && value.value.typeId == goStringTypeId)) { + "\t$name := ${goUtModelToCodeConverter.toGoCode(value.value)}\n" + } else { + "\t$name := ${goUtModelToCodeConverter.toGoCode(value)}\n" + } + } + + private fun generateVariableInitialization( + variable: Variable, + goUtModelToCodeConverter: GoUtModelToCodeConverter + ): String { + val (name, _, value) = variable + return when (value) { + is GoUtChanModel -> generateChannelInitialization(name, value, goUtModelToCodeConverter) + + is GoUtNamedModel -> if (value.value is GoUtChanModel) { + generateChannelInitialization(name, value.value as GoUtChanModel, goUtModelToCodeConverter) + } else { + "" + } + + is GoUtPointerModel -> if (value.value is GoUtPrimitiveModel) { + generatePointerToPrimitiveInitialization( + name, + value.value as GoUtPrimitiveModel, + goUtModelToCodeConverter + ) + } else if (value.value is GoUtNamedModel && (value.value as GoUtNamedModel).value is GoUtPrimitiveModel) { + generatePointerToPrimitiveInitialization( + name, + (value.value as GoUtNamedModel).value as GoUtPrimitiveModel, + goUtModelToCodeConverter + ) + } else { + "" + } + + else -> "" + } + } + + private fun generateVariablesDeclarationAndInitialization( + variables: List, goUtModelToCodeConverter: GoUtModelToCodeConverter + ): String { + val vars = variables.filterNotNull() + return if (vars.isNotEmpty()) { + vars.joinToString(separator = "", postfix = "\n") { variable -> + val declaration = generateVariableDeclaration(variable, goUtModelToCodeConverter) + val initialization = generateVariableInitialization(variable, goUtModelToCodeConverter) + declaration + initialization + } + } else { + "" + } + } + + private fun generateFuzzedFunctionCall(function: GoUtFunction, variables: List): String { + return if (function.isMethod) { + val fuzzedParametersToString = variables.drop(1).joinToString(prefix = "(", postfix = ")") { + it.name + } + "${variables[0].name}.${function.name}$fuzzedParametersToString" + } else { + val fuzzedParametersToString = variables.joinToString(prefix = "(", postfix = ")") { + it.name + } + "${function.name}$fuzzedParametersToString" + } + } + + private fun generateVariablesDeclarationTo(variablesNames: List, expression: String): String { + val variables = variablesNames.joinToString() + return "$variables := $expression" + } + + private fun generateFuzzedFunctionCallSavedToVariables( + variablesNames: List, fuzzedFunction: GoUtFuzzedFunction, variables: List + ): String = generateVariablesDeclarationTo( + variablesNames, expression = generateFuzzedFunctionCall(fuzzedFunction.function, variables) + ) + + private fun modelIsNilOrBoolOrFloatNanOrFloatInf(value: GoUtModel): Boolean = value is GoUtNilModel + || (value is GoUtNamedModel && value.value is GoUtNilModel) + || value.typeId == goBoolTypeId + || value is GoUtFloatNaNModel + || value is GoUtFloatInfModel + + private fun generateCastIfNeed( + toTypeId: GoPrimitiveTypeId, expressionType: GoPrimitiveTypeId, expression: String + ): String { + return if (expressionType != toTypeId) { + "${toTypeId.name}($expression)" + } else { + expression + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoUtModelToCodeConverter.kt b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoUtModelToCodeConverter.kt new file mode 100644 index 0000000000..233cb67458 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoUtModelToCodeConverter.kt @@ -0,0 +1,151 @@ +package org.utbot.go.simplecodegeneration + +import org.utbot.go.api.* +import org.utbot.go.api.util.goDefaultValueModel +import org.utbot.go.api.util.goStringTypeId +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +class GoUtModelToCodeConverter( + private val destinationPackage: GoPackage, + private val aliases: Map +) { + + fun toGoCode(model: GoUtModel): String = when (model) { + is GoUtNilModel -> nilModelToGoCode(model) + + is GoUtPrimitiveModel -> when (model.explicitCastMode) { + ExplicitCastMode.REQUIRED, ExplicitCastMode.DEPENDS -> primitiveModelToCastedValueGoCode(model) + ExplicitCastMode.NEVER -> primitiveModelToValueGoCode(model) + } + + is GoUtArrayModel -> arrayModelToGoCode(model) + + is GoUtSliceModel -> sliceModelToGoCode(model) + + is GoUtMapModel -> mapModelToGoCode(model) + + is GoUtChanModel -> chanModelToGoCode(model) + + is GoUtNamedModel -> namedModelToGoCode(model) + + is GoUtPointerModel -> pointerModelToGoCode(model) + + else -> error("Converting a ${model.javaClass} to Go code isn't supported") + } + + fun toGoCodeWithoutTypeName(model: GoUtModel): String = when (model) { + is GoUtNilModel -> "nil" + + is GoUtPrimitiveModel -> when (model.explicitCastMode) { + ExplicitCastMode.REQUIRED -> primitiveModelToCastedValueGoCode(model) + ExplicitCastMode.DEPENDS, ExplicitCastMode.NEVER -> primitiveModelToValueGoCode(model) + } + + is GoUtStructModel -> structModelToGoCodeWithoutStructName(model) + + is GoUtArrayModel -> arrayModelToGoCodeWithoutTypeName(model) + + is GoUtSliceModel -> sliceModelToGoCodeWithoutTypeName(model) + + is GoUtMapModel -> mapModelToGoCodeWithoutTypeName(model) + + is GoUtNamedModel -> toGoCodeWithoutTypeName(model.value) + + is GoUtPointerModel -> pointerModelToGoCode(model) + + else -> error("Converting a ${model.javaClass} to Go code isn't supported") + } + + private fun nilModelToGoCode(model: GoUtNilModel): String { + val typeName = model.typeId.getRelativeName(destinationPackage, aliases) + return "($typeName)(nil)" + } + + private fun primitiveModelToValueGoCode(model: GoUtPrimitiveModel): String = when (model) { + is GoUtComplexModel -> complexModeToValueGoCode(model) + else -> if (model.typeId == goStringTypeId) "\"${model.value}\"" else "${model.value}" + } + + private fun primitiveModelToCastedValueGoCode(model: GoUtPrimitiveModel): String { + val typeName = model.typeId.getRelativeName(destinationPackage, aliases) + return "$typeName(${primitiveModelToValueGoCode(model)})" + } + + private fun complexModeToValueGoCode(model: GoUtComplexModel) = + "complex(${toGoCode(model.realValue)}, ${toGoCode(model.imagValue)})" + + private fun structModelToGoCodeWithoutStructName(model: GoUtStructModel): String = + model.value.entries.filter { (fieldId, model) -> model != fieldId.declaringType.goDefaultValueModel() } + .joinToString(prefix = "{", postfix = "}") { (fieldId, model) -> + "${fieldId.name}: ${toGoCode(model)}" + } + + private fun arrayModelToGoCode(model: GoUtArrayModel): String { + val typeName = model.typeId.getRelativeName(destinationPackage, aliases) + return typeName + arrayModelToGoCodeWithoutTypeName(model) + } + + private fun arrayModelToGoCodeWithoutTypeName(model: GoUtArrayModel): String = + model.getElements().joinToString(prefix = "{", postfix = "}") { + toGoCodeWithoutTypeName(it) + } + + private fun sliceModelToGoCode(model: GoUtSliceModel): String { + val typeName = model.typeId.getRelativeName(destinationPackage, aliases) + return typeName + sliceModelToGoCodeWithoutTypeName(model) + } + + private fun sliceModelToGoCodeWithoutTypeName(model: GoUtSliceModel): String = + model.getElements().joinToString(prefix = "{", postfix = "}") { + toGoCodeWithoutTypeName(it) + } + + private fun mapModelToGoCode(model: GoUtMapModel): String { + val typeName = model.typeId.getRelativeName(destinationPackage, aliases) + return typeName + mapModelToGoCodeWithoutTypeName(model) + } + + private fun mapModelToGoCodeWithoutTypeName(model: GoUtMapModel): String = + model.value.entries.joinToString(prefix = "{", postfix = "}") { + "${toGoCode(it.key)}: ${toGoCodeWithoutTypeName(it.value)}" + } + + private fun chanModelToGoCode(model: GoUtChanModel): String { + val elemTypeName = model.typeId.elementTypeId!!.getRelativeName(destinationPackage, aliases) + return "make(chan $elemTypeName, ${model.value.size})" + } + + private fun namedModelToGoCode(model: GoUtNamedModel): String { + if (model.value is GoUtNamedModel) { + return toGoCode(model.value) + } + val typeName = model.typeId.getRelativeName(destinationPackage, aliases) + return if (model.value is GoUtPrimitiveModel || model.value is GoUtNilModel) { + "$typeName(${toGoCodeWithoutTypeName(model.value)})" + } else { + "$typeName${toGoCodeWithoutTypeName(model.value)}" + } + } + + private fun pointerToZeroValueOfType(typeId: GoTypeId): String { + val typeName = typeId.getRelativeName(destinationPackage, aliases) + return "new($typeName)" + } + + private fun pointerModelToGoCode(model: GoUtPointerModel): String = when (val value = model.value) { + is GoUtNilModel -> pointerToZeroValueOfType(value.typeId) + is GoUtPrimitiveModel -> pointerToZeroValueOfType(value.typeId) + + is GoUtNamedModel -> { + if (value.value is GoUtPrimitiveModel) { + pointerToZeroValueOfType(value.typeId) + } else { + "&${toGoCode(value)}" + } + } + + else -> "&${toGoCode(value)}" + } +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt new file mode 100644 index 0000000000..47cf0927da --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt @@ -0,0 +1,26 @@ +package org.utbot.go.util + +import com.beust.klaxon.Klaxon +import java.io.File + +fun convertObjectToJsonString(targetObject: T): String = Klaxon().toJsonString(targetObject) + +fun writeJsonToFileOrFail(targetObject: T, jsonFile: File) { + val targetObjectAsJson = convertObjectToJsonString(targetObject) + jsonFile.writeText(targetObjectAsJson) +} + +inline fun parseFromJsonOrFail(jsonFile: File): T { + val result = Klaxon().parse(jsonFile) + if (result == null) { + val rawResults = try { + jsonFile.readText() + } catch (exception: Exception) { + null + } + throw RuntimeException( + "Failed to deserialize results: $rawResults" + ) + } + return result +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt new file mode 100644 index 0000000000..4f07143c40 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt @@ -0,0 +1,61 @@ +package org.utbot.go.util + +import java.io.File +import java.io.InputStreamReader +import java.nio.file.Path + +fun modifyEnvironment(goExecutableAbsolutePath: Path, gopathAbsolutePath: Path): MutableMap { + val environment = System.getenv().toMutableMap().apply { + this["Path"] = + listOfNotNull(goExecutableAbsolutePath.parent, this["Path"]).joinToString(separator = File.pathSeparator) + this["GOROOT"] = goExecutableAbsolutePath.parent.parent.toString() + this["GOPATH"] = gopathAbsolutePath.toString() + } + return environment +} + +fun executeCommandByNewProcessOrFail( + command: List, + workingDirectory: File, + executionTargetName: String, + environment: Map = System.getenv(), + helpMessage: String? = null +) { + val helpMessageLine = if (helpMessage == null) "" else "\n\nHELP: $helpMessage" + val executedProcess = runCatching { + val process = executeCommandByNewProcessOrFailWithoutWaiting(command, workingDirectory, environment) + process.waitFor() + process + }.getOrElse { + throw RuntimeException( + StringBuilder() + .append("Execution of $executionTargetName in child process failed with throwable: ") + .append("$it").append(helpMessageLine) + .toString() + ) + } + val exitCode = executedProcess.exitValue() + if (exitCode != 0) { + val processOutput = InputStreamReader(executedProcess.inputStream).readText() + throw RuntimeException( + StringBuilder() + .append("Execution of $executionTargetName in child process failed with non-zero exit code = $exitCode: ") + .append("\n$processOutput").append(helpMessageLine) + .toString() + ) + } +} + +fun executeCommandByNewProcessOrFailWithoutWaiting( + command: List, + workingDirectory: File, + environment: Map = System.getenv() +): Process { + val processBuilder = ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectErrorStream(true) + .directory(workingDirectory) + processBuilder.environment().clear() + processBuilder.environment().putAll(environment) + return processBuilder.start() +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/worker/GoCodeTemplates.kt b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoCodeTemplates.kt new file mode 100644 index 0000000000..ce227be1bc --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoCodeTemplates.kt @@ -0,0 +1,1431 @@ +package org.utbot.go.worker + +import org.utbot.go.api.GoInterfaceTypeId +import org.utbot.go.api.GoNamedTypeId +import org.utbot.go.api.util.goDefaultValueModel +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.simplecodegeneration.GoUtModelToCodeConverter + +object GoCodeTemplates { + + private val errorMessages = """ + const ( + ErrParsingValue = "failed to parse %s value: %s" + ErrInvalidTypeName = "invalid type name: %s" + ErrStringToReflectTypeFailure = "failed to convert '%s' to reflect.Type: %s" + ErrRawValueToReflectValueFailure = "failed to convert RawValue to reflect.Value: %s" + ErrReflectValueToRawValueFailure = "failed to convert reflect.Value to RawValue: %s" + ) + """.trimIndent() + + private val typeOfExecutionResult = """ + type __TypeOfExecutionResult__ int + + const ( + Completed = iota + PanicFailure + TimeoutExceeded + ) + """.trimIndent() + + private val executionResultStruct = """ + type __ExecutionResult__ struct { + Type __TypeOfExecutionResult__ + ResultValues []reflect.Value + } + """.trimIndent() + + private val testInputStruct = """ + type __TestInput__ struct { + FunctionName string `json:"functionName"` + Arguments []map[string]interface{} `json:"arguments"` + } + """.trimIndent() + + private val rawValueInterface = """ + type __RawValue__ interface { + __toReflectValue__() (reflect.Value, error) + } + """.trimIndent() + + private val primitiveValueStruct = """ + type __PrimitiveValue__ struct { + Type string `json:"type"` + Value string `json:"value"` + } + """.trimIndent() + + + private val parseFloatFunction = """ + func __parseFloat__(s string, bitSize int) (float64, error) { + if s == "NaN" { + return math.NaN(), nil + } + + if s == "+Inf" { + return math.Inf(1), nil + } + + if s == "-Inf" { + return math.Inf(-1), nil + } + + value, err := strconv.ParseFloat(s, bitSize) + if err != nil { + return value, fmt.Errorf("failed to parse float: %s", s) + } + + return value, nil + } + """.trimIndent() + + private val primitiveValueToReflectValueMethod = """ + func (v __PrimitiveValue__) __toReflectValue__() (reflect.Value, error) { + + const complexPartsDelimiter = "@" + + switch v.Type { + case "bool": + value, err := strconv.ParseBool(v.Value) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(value), nil + case "int": + value, err := strconv.Atoi(v.Value) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(value), nil + case "int8": + value, err := strconv.ParseInt(v.Value, 10, 8) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(int8(value)), nil + case "int16": + value, err := strconv.ParseInt(v.Value, 10, 16) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(int16(value)), nil + case "int32": + value, err := strconv.ParseInt(v.Value, 10, 32) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(int32(value)), nil + case "rune": + value, err := strconv.ParseInt(v.Value, 10, 32) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(rune(value)), nil + case "int64": + value, err := strconv.ParseInt(v.Value, 10, 64) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(value), nil + case "byte": + value, err := strconv.ParseUint(v.Value, 10, 8) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(byte(value)), nil + case "uint": + value, err := strconv.ParseUint(v.Value, 10, strconv.IntSize) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(uint(value)), nil + case "uint8": + value, err := strconv.ParseUint(v.Value, 10, 8) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(uint8(value)), nil + case "uint16": + value, err := strconv.ParseUint(v.Value, 10, 16) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(uint16(value)), nil + case "uint32": + value, err := strconv.ParseUint(v.Value, 10, 32) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(uint32(value)), nil + case "uint64": + value, err := strconv.ParseUint(v.Value, 10, 64) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(value), nil + case "float32": + value, err := __parseFloat__(v.Value, 32) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(float32(value)), nil + case "float64": + value, err := __parseFloat__(v.Value, 64) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(value), nil + case "complex64": + splitValue := strings.Split(v.Value, complexPartsDelimiter) + if len(splitValue) != 2 { + return reflect.Value{}, fmt.Errorf("not correct complex64 value: %s", v.Value) + } + realPart, err := __parseFloat__(splitValue[0], 32) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + imaginaryPart, err := __parseFloat__(splitValue[1], 32) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(complex(float32(realPart), float32(imaginaryPart))), nil + case "complex128": + splitValue := strings.Split(v.Value, complexPartsDelimiter) + if len(splitValue) != 2 { + return reflect.Value{}, fmt.Errorf("not correct complex128 value: %s", v.Value) + } + + realPart, err := strconv.ParseFloat(splitValue[0], 64) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + imaginaryPart, err := strconv.ParseFloat(splitValue[1], 64) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(complex(realPart, imaginaryPart)), nil + case "string": + return reflect.ValueOf(v.Value), nil + case "uintptr": + value, err := strconv.ParseUint(v.Value, 10, strconv.IntSize) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrParsingValue, v.Type, err) + } + + return reflect.ValueOf(uintptr(value)), nil + } + return reflect.Value{}, fmt.Errorf("unsupported primitive type: '%s'", v.Type) + } + """.trimIndent() + + private val fieldValueStruct = """ + type __FieldValue__ struct { + Name string `json:"name"` + Value __RawValue__ `json:"value"` + IsExported bool `json:"isExported"` + } + """.trimIndent() + + private val structValueStruct = """ + type __StructValue__ struct { + Type string `json:"type"` + Value []__FieldValue__ `json:"value"` + } + """.trimIndent() + + private val structValueToReflectValueMethod = """ + func (v __StructValue__) __toReflectValue__() (reflect.Value, error) { + structType, err := __convertStringToReflectType__(v.Type) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrStringToReflectTypeFailure, v.Type, err) + } + + structPtr := reflect.New(structType) + + for _, f := range v.Value { + field := structPtr.Elem().FieldByName(f.Name) + + reflectValue, err := f.Value.__toReflectValue__() + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrRawValueToReflectValueFailure, err) + } + + if field.Type().Kind() == reflect.Uintptr { + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().SetUint(reflectValue.Uint()) + } else { + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflectValue) + } + } + + return structPtr.Elem(), nil + } + """.trimIndent() + + private val arrayValueStruct = """ + type __ArrayValue__ struct { + Type string `json:"type"` + ElementType string `json:"elementType"` + Length int `json:"length"` + Value []__RawValue__ `json:"value"` + } + """.trimIndent() + + private val arrayValueToReflectValueMethod = """ + func (v __ArrayValue__) __toReflectValue__() (reflect.Value, error) { + elementType, err := __convertStringToReflectType__(v.ElementType) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrStringToReflectTypeFailure, v.ElementType, err) + } + + arrayType := reflect.ArrayOf(v.Length, elementType) + arrayPtr := reflect.New(arrayType) + + for i := 0; i < v.Length; i++ { + element := arrayPtr.Elem().Index(i) + + reflectValue, err := v.Value[i].__toReflectValue__() + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrRawValueToReflectValueFailure, err) + } + + element.Set(reflectValue) + } + + return arrayPtr.Elem(), nil + } + """.trimIndent() + + private val sliceValueStruct = """ + type __SliceValue__ struct { + Type string `json:"type"` + ElementType string `json:"elementType"` + Length int `json:"length"` + Value []__RawValue__ `json:"value"` + } + """.trimIndent() + + private val sliceValueToReflectValueMethod = """ + func (v __SliceValue__) __toReflectValue__() (reflect.Value, error) { + elementType, err := __convertStringToReflectType__(v.ElementType) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrStringToReflectTypeFailure, v.ElementType, err) + } + + sliceType := reflect.SliceOf(elementType) + slice := reflect.MakeSlice(sliceType, v.Length, v.Length) + slicePtr := reflect.New(slice.Type()) + slicePtr.Elem().Set(slice) + + for i := 0; i < len(v.Value); i++ { + element := slicePtr.Elem().Index(i) + + reflectValue, err := v.Value[i].__toReflectValue__() + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrRawValueToReflectValueFailure, err) + } + + element.Set(reflectValue) + } + + return slicePtr.Elem(), nil + } + """.trimIndent() + + private val keyValueStruct = """ + type __KeyValue__ struct { + Key __RawValue__ `json:"key"` + Value __RawValue__ `json:"value"` + } + """.trimIndent() + + private val mapValueStruct = """ + type __MapValue__ struct { + Type string `json:"type"` + KeyType string `json:"keyType"` + ElementType string `json:"elementType"` + Value []__KeyValue__ `json:"value"` + } + """.trimIndent() + + private val mapValueToReflectValueMethod = """ + func (v __MapValue__) __toReflectValue__() (reflect.Value, error) { + keyType, err := __convertStringToReflectType__(v.KeyType) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrStringToReflectTypeFailure, v.KeyType, err) + } + + elementType, err := __convertStringToReflectType__(v.ElementType) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrStringToReflectTypeFailure, v.ElementType, err) + } + + mapType := reflect.MapOf(keyType, elementType) + m := reflect.MakeMap(mapType) + for _, keyValue := range v.Value { + keyReflectValue, err := keyValue.Key.__toReflectValue__() + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrRawValueToReflectValueFailure, err) + } + + valueReflectValue, err := keyValue.Value.__toReflectValue__() + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrRawValueToReflectValueFailure, err) + } + + m.SetMapIndex(keyReflectValue, valueReflectValue) + } + return m, nil + } + """.trimIndent() + + private val chanValueStruct = """ + type __ChanValue__ struct { + Type string `json:"type"` + ElementType string `json:"elementType"` + Direction string `json:"direction"` + Length int `json:"length"` + Value []__RawValue__ `json:"value"` + } + """.trimIndent() + + private val chanValueToReflectValueMethod = """ + func (v __ChanValue__) __toReflectValue__() (reflect.Value, error) { + elementType, err := __convertStringToReflectType__(v.ElementType) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrStringToReflectTypeFailure, v.ElementType, err) + } + + dir := reflect.BothDir + + chanType := reflect.ChanOf(dir, elementType) + channel := reflect.MakeChan(chanType, v.Length) + + for i := range v.Value { + reflectValue, err := v.Value[i].__toReflectValue__() + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrRawValueToReflectValueFailure, err) + } + + channel.Send(reflectValue) + } + if v.Direction != "SENDONLY" { + channel.Close() + } + + return channel, nil + } + """.trimIndent() + + private val nilValueStruct = """ + type __NilValue__ struct { + Type string `json:"type"` + } + """.trimIndent() + + private val nilValueToReflectValueMethod = """ + func (v __NilValue__) __toReflectValue__() (reflect.Value, error) { + typ, err := __convertStringToReflectType__(v.Type) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrStringToReflectTypeFailure, v.Type, err) + } + + return reflect.Zero(typ), nil + } + """.trimIndent() + + private val namedValueStruct = """ + type __NamedValue__ struct { + Type string `json:"type"` + Value __RawValue__ `json:"value"` + } + """.trimIndent() + + private val namedValueToReflectValueMethod = """ + func (v __NamedValue__) __toReflectValue__() (reflect.Value, error) { + typ, err := __convertStringToReflectType__(v.Type) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrStringToReflectTypeFailure, v.Type, err) + } + + if n, ok := v.Value.(__NilValue__); ok && n.Type == "interface{}" { + return reflect.Zero(typ), nil + } + + value, err := v.Value.__toReflectValue__() + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrRawValueToReflectValueFailure, err) + } + + return value.Convert(typ), nil + } + """.trimIndent() + + private val pointerValueStruct = """ + type __PointerValue__ struct { + Type string `json:"type"` + ElementType string `json:"elementType"` + Value __RawValue__ `json:"value"` + } + """.trimIndent() + + private val pointerValueToReflectValueMethod = """ + func (v __PointerValue__) __toReflectValue__() (reflect.Value, error) { + elementType, err := __convertStringToReflectType__(v.ElementType) + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrStringToReflectTypeFailure, v.Type, err) + } + + value, err := v.Value.__toReflectValue__() + if err != nil { + return reflect.Value{}, fmt.Errorf(ErrRawValueToReflectValueFailure, err) + } + + pointer := reflect.New(elementType) + pointer.Elem().Set(value) + + return pointer, nil + } + """.trimIndent() + + private fun convertStringToReflectTypeFunction( + namedTypes: Set, destinationPackage: GoPackage, aliases: Map + ): String { + val converter = GoUtModelToCodeConverter(destinationPackage, aliases) + return """ + func __convertStringToReflectType__(typeName string) (reflect.Type, error) { + var result reflect.Type + + switch { + case strings.HasPrefix(typeName, "map["): + index := strings.IndexRune(typeName, ']') + if index == -1 { + return nil, fmt.Errorf(ErrInvalidTypeName, typeName) + } + + keyTypeStr := typeName[4:index] + keyType, err := __convertStringToReflectType__(keyTypeStr) + if err != nil { + return nil, fmt.Errorf(ErrStringToReflectTypeFailure, keyTypeStr, err) + } + + elementTypeStr := typeName[index+1:] + elementType, err := __convertStringToReflectType__(elementTypeStr) + if err != nil { + return nil, fmt.Errorf(ErrStringToReflectTypeFailure, elementTypeStr, err) + } + + result = reflect.MapOf(keyType, elementType) + case strings.HasPrefix(typeName, "[]"): + index := strings.IndexRune(typeName, ']') + if index == -1 { + return nil, fmt.Errorf(ErrInvalidTypeName, typeName) + } + + res, err := __convertStringToReflectType__(typeName[index+1:]) + if err != nil { + return nil, fmt.Errorf(ErrInvalidTypeName, typeName) + } + + result = reflect.SliceOf(res) + case strings.HasPrefix(typeName, "["): + index := strings.IndexRune(typeName, ']') + if index == -1 { + return nil, fmt.Errorf(ErrInvalidTypeName, typeName) + } + + lengthStr := typeName[1:index] + length, err := strconv.Atoi(lengthStr) + if err != nil { + return nil, err + } + + res, err := __convertStringToReflectType__(typeName[index+1:]) + if err != nil { + return nil, fmt.Errorf(ErrStringToReflectTypeFailure, typeName[index+1:], err) + } + + result = reflect.ArrayOf(length, res) + case strings.HasPrefix(typeName, "<-chan") || strings.HasPrefix(typeName, "chan"): + dir := reflect.BothDir + index := 5 + if strings.HasPrefix(typeName, "<-chan") { + dir = reflect.RecvDir + index = 7 + } else if strings.HasPrefix(typeName, "chan<-") { + dir = reflect.SendDir + index = 7 + } + + elemType, err := __convertStringToReflectType__(typeName[index:]) + if err != nil { + return nil, fmt.Errorf(ErrStringToReflectTypeFailure, typeName[index:], err) + } + + result = reflect.ChanOf(dir, elemType) + case strings.HasPrefix(typeName, "*"): + elemType, err := __convertStringToReflectType__(typeName[1:]) + if err != nil { + return nil, fmt.Errorf(ErrStringToReflectTypeFailure, typeName[1:], err) + } + + result = reflect.PointerTo(elemType) + default: + switch typeName { + case "bool": + result = reflect.TypeOf(true) + case "int": + result = reflect.TypeOf(0) + case "int8": + result = reflect.TypeOf(int8(0)) + case "int16": + result = reflect.TypeOf(int16(0)) + case "int32": + result = reflect.TypeOf(int32(0)) + case "rune": + result = reflect.TypeOf(rune(0)) + case "int64": + result = reflect.TypeOf(int64(0)) + case "byte": + result = reflect.TypeOf(byte(0)) + case "uint": + result = reflect.TypeOf(uint(0)) + case "uint8": + result = reflect.TypeOf(uint8(0)) + case "uint16": + result = reflect.TypeOf(uint16(0)) + case "uint32": + result = reflect.TypeOf(uint32(0)) + case "uint64": + result = reflect.TypeOf(uint64(0)) + case "float32": + result = reflect.TypeOf(float32(0)) + case "float64": + result = reflect.TypeOf(float64(0)) + case "complex64": + result = reflect.TypeOf(complex(float32(0), float32(0))) + case "complex128": + result = reflect.TypeOf(complex(float64(0), float64(0))) + case "string": + result = reflect.TypeOf("") + case "uintptr": + result = reflect.TypeOf(uintptr(0)) + case "interface{}": + result = reflect.TypeOf((*interface{})(nil)).Elem() + ${ + namedTypes.joinToString(separator = "\n") { + val relativeName = it.getRelativeName(destinationPackage, aliases) + if (it.underlyingTypeId is GoInterfaceTypeId) { + "case \"${relativeName}\": result = reflect.TypeOf((*$relativeName)(nil)).Elem()" + } else { + "case \"${relativeName}\": result = reflect.TypeOf(${converter.toGoCode(it.goDefaultValueModel())})" + } + } + } + default: + return nil, fmt.Errorf("unsupported type: %s", typeName) + } + } + return result, nil + } + """.trimIndent() + } + + private val panicMessageStruct = """ + type __RawPanicMessage__ struct { + RawResultValue __RawValue__ `json:"rawResultValue"` + ImplementsError bool `json:"implementsError"` + } + """.trimIndent() + + private val rawExecutionResultStruct = """ + type __RawExecutionResult__ struct { + TimeoutExceeded bool `json:"timeoutExceeded"` + RawResultValues []__RawValue__ `json:"rawResultValues"` + PanicMessage *__RawPanicMessage__ `json:"panicMessage"` + CoverTab map[int]int `json:"coverTab"` + } + """.trimIndent() + + private val convertReflectValueOfDefinedTypeToRawValueFunction = """ + func __convertReflectValueOfDefinedTypeToRawValue__(v reflect.Value) (__RawValue__, error) { + value, err := __convertReflectValueOfPredeclaredOrNotDefinedTypeToRawValue__(v) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + + return __NamedValue__{ + Type: v.Type().Name(), + Value: value, + }, nil + } + """.trimIndent() + + private val convertFloat64ValueToStringFunction = """ + func __convertFloat64ValueToString__(value float64) string { + const outputNaN = "NaN" + const outputPosInf = "+Inf" + const outputNegInf = "-Inf" + switch { + case math.IsNaN(value): + return fmt.Sprint(outputNaN) + case math.IsInf(value, 1): + return fmt.Sprint(outputPosInf) + case math.IsInf(value, -1): + return fmt.Sprint(outputNegInf) + default: + return fmt.Sprintf("%v", value) + } + } + """.trimIndent() + + private val convertReflectValueOfPredeclaredOrNotDefinedTypeToRawValueFunction = """ + func __convertReflectValueOfPredeclaredOrNotDefinedTypeToRawValue__(v reflect.Value) (__RawValue__, error) { + const outputComplexPartsDelimiter = "@" + + switch v.Kind() { + case reflect.Bool: + return __PrimitiveValue__{ + Type: v.Kind().String(), + Value: fmt.Sprintf("%#v", v.Bool()), + }, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return __PrimitiveValue__{ + Type: v.Kind().String(), + Value: fmt.Sprintf("%#v", v.Int()), + }, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return __PrimitiveValue__{ + Type: v.Kind().String(), + Value: fmt.Sprintf("%v", v.Uint()), + }, nil + case reflect.Float32, reflect.Float64: + return __PrimitiveValue__{ + Type: v.Kind().String(), + Value: __convertFloat64ValueToString__(v.Float()), + }, nil + case reflect.Complex64, reflect.Complex128: + value := v.Complex() + realPartString := __convertFloat64ValueToString__(real(value)) + imagPartString := __convertFloat64ValueToString__(imag(value)) + return __PrimitiveValue__{ + Type: v.Kind().String(), + Value: fmt.Sprintf("%v%v%v", realPartString, outputComplexPartsDelimiter, imagPartString), + }, nil + case reflect.String: + return __PrimitiveValue__{ + Type: reflect.String.String(), + Value: fmt.Sprintf("%v", v.String()), + }, nil + case reflect.Struct: + fields := reflect.VisibleFields(v.Type()) + resultValues := make([]__FieldValue__, 0, v.NumField()) + for _, field := range fields { + if len(field.Index) != 1 { + continue + } + + res, err := __convertReflectValueToRawValue__(v.FieldByName(field.Name)) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + + resultValues = append(resultValues, __FieldValue__{ + Name: field.Name, + Value: res, + IsExported: field.IsExported(), + }) + } + return __StructValue__{ + Type: "struct{}", + Value: resultValues, + }, nil + case reflect.Array: + elem := v.Type().Elem() + elementType := elem.String() + arrayElementValues := make([]__RawValue__, 0, v.Len()) + for i := 0; i < v.Len(); i++ { + arrayElementValue, err := __convertReflectValueToRawValue__(v.Index(i)) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + + arrayElementValues = append(arrayElementValues, arrayElementValue) + } + length := len(arrayElementValues) + return __ArrayValue__{ + Type: fmt.Sprintf("[%d]%s", length, elementType), + ElementType: elementType, + Length: length, + Value: arrayElementValues, + }, nil + case reflect.Slice: + if v.IsNil() { + return __NilValue__{Type: "nil"}, nil + } + elem := v.Type().Elem() + elementType := elem.String() + typeName := fmt.Sprintf("[]%s", elementType) + sliceElementValues := make([]__RawValue__, 0, v.Len()) + for i := 0; i < v.Len(); i++ { + sliceElementValue, err := __convertReflectValueToRawValue__(v.Index(i)) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + + sliceElementValues = append(sliceElementValues, sliceElementValue) + } + length := len(sliceElementValues) + return __SliceValue__{ + Type: typeName, + ElementType: elementType, + Length: length, + Value: sliceElementValues, + }, nil + case reflect.Map: + if v.IsNil() { + return __NilValue__{Type: "nil"}, nil + } + key := v.Type().Key() + keyType := key.String() + elem := v.Type().Elem() + elementType := elem.String() + typeName := fmt.Sprintf("map[%s]%s", keyType, elementType) + mapValues := make([]__KeyValue__, 0, v.Len()) + for iter := v.MapRange(); iter.Next(); { + key, err := __convertReflectValueToRawValue__(iter.Key()) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + + value, err := __convertReflectValueToRawValue__(iter.Value()) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + + mapValues = append(mapValues, __KeyValue__{ + Key: key, + Value: value, + }) + } + return __MapValue__{ + Type: typeName, + KeyType: keyType, + ElementType: elementType, + Value: mapValues, + }, nil + case reflect.Chan: + if v.IsNil() { + return __NilValue__{Type: "nil"}, nil + } + typeName := v.Type().String() + elementType := v.Type().Elem().String() + dir := "SENDRECV" + if v.Type().ChanDir() == reflect.SendDir { + dir = "SENDONLY" + } else if v.Type().ChanDir() == reflect.RecvDir { + dir = "RECVONLY" + } + length := v.Len() + + chanElementValues := make([]__RawValue__, 0, v.Len()) + if dir != "SENDONLY" { + for v.Len() > 0 { + val, _ := v.Recv() + rawValue, err := __convertReflectValueToRawValue__(val) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + + chanElementValues = append(chanElementValues, rawValue) + } + } + + return __ChanValue__{ + Type: typeName, + ElementType: elementType, + Direction: dir, + Length: length, + Value: chanElementValues, + }, nil + case reflect.Interface: + if v.Interface() == nil { + return __NilValue__{Type: "nil"}, nil + } + if e, ok := v.Interface().(error); ok { + value, err := __convertReflectValueOfPredeclaredOrNotDefinedTypeToRawValue__(reflect.ValueOf(e.Error())) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + return __NamedValue__{ + Type: "error", + Value: value, + }, nil + } + return nil, fmt.Errorf("unsupported result type: %s", v.Type().String()) + case reflect.Pointer: + if v.IsNil() { + return __NilValue__{Type: "nil"}, nil + } + typeName := v.Type().String() + elementType := v.Type().Elem().String() + + value, err := __convertReflectValueToRawValue__(v.Elem()) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + + return __PointerValue__{ + Type: typeName, + ElementType: elementType, + Value: value, + }, nil + default: + return nil, fmt.Errorf("unsupported result type: %s", v.Type().String()) + } + } + """.trimIndent() + + private val convertReflectValueToRawValueFunction = """ + func __convertReflectValueToRawValue__(v reflect.Value) (__RawValue__, error) { + if v.Type().PkgPath() != "" { + return __convertReflectValueOfDefinedTypeToRawValue__(v) + } + return __convertReflectValueOfPredeclaredOrNotDefinedTypeToRawValue__(v) + } + """.trimIndent() + + private val executeFunctionFunction = """ + func __executeFunction__( + function reflect.Value, arguments []reflect.Value, timeout time.Duration, + ) __RawExecutionResult__ { + ctxWithTimeout, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + __CoverTab__ = make([]int, __CoverSize__) + + executionResult := make(chan __ExecutionResult__, 1) + go func() { + panicked := true + defer func() { + panicMessage := recover() + if panicked { + executionResult <- __ExecutionResult__{ + Type: PanicFailure, + ResultValues: []reflect.Value{reflect.ValueOf(panicMessage)}, + } + } + }() + + executionResult <- __ExecutionResult__{ + Type: Completed, + ResultValues: function.Call(arguments), + } + panicked = false + }() + + var result __ExecutionResult__ + select { + case result = <-executionResult: + case <-ctxWithTimeout.Done(): + result = __ExecutionResult__{Type: TimeoutExceeded} + } + + return __wrapExecutionResult__(result) + } + """.trimIndent() + + private val wrapExecutionResultFunction = """ + func __wrapExecutionResult__(executionResult __ExecutionResult__) __RawExecutionResult__ { + var result __RawExecutionResult__ + switch executionResult.Type { + case Completed: + resultValues, err := __wrapResultValues__(executionResult.ResultValues) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to wrap result values: %s", err) + os.Exit(1) + } + + result = __RawExecutionResult__{RawResultValues: resultValues} + case PanicFailure: + panicMessage := executionResult.ResultValues[0].Interface() + panicAsError, implementsError := panicMessage.(error) + var ( + resultValue __RawValue__ + err error + ) + if implementsError { + resultValue, err = __convertReflectValueToRawValue__(reflect.ValueOf(panicAsError.Error())) + } else { + resultValue, err = __convertReflectValueToRawValue__(reflect.ValueOf(panicMessage)) + } + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, ErrReflectValueToRawValueFailure, err) + os.Exit(1) + } + + result = __RawExecutionResult__{ + RawResultValues: []__RawValue__{}, + PanicMessage: &__RawPanicMessage__{ + RawResultValue: resultValue, + ImplementsError: implementsError, + }, + } + case TimeoutExceeded: + result = __RawExecutionResult__{ + TimeoutExceeded: true, + RawResultValues: []__RawValue__{}, + } + } + + coverTab := make(map[int]int, 256) + for i, v := range __CoverTab__ { + if v != 0 { + coverTab[i] = v + } + } + result.CoverTab = coverTab + + return result + } + """.trimIndent() + + private val wrapResultValuesForWorkerFunction = """ + func __wrapResultValues__(values []reflect.Value) ([]__RawValue__, error) { + rawValues := make([]__RawValue__, 0, len(values)) + for _, value := range values { + resultValue, err := __convertReflectValueToRawValue__(value) + if err != nil { + return nil, fmt.Errorf(ErrReflectValueToRawValueFailure, err) + } + + rawValues = append(rawValues, resultValue) + } + return rawValues, nil + } + """.trimIndent() + + private val convertRawValuesToReflectValuesFunction = """ + func __convertRawValuesToReflectValues__(values []__RawValue__) ([]reflect.Value, error) { + parameters := make([]reflect.Value, 0, len(values)) + + for _, value := range values { + reflectValue, err := value.__toReflectValue__() + if err != nil { + return nil, fmt.Errorf("failed to convert RawValue %s to reflect.Value: %s", value, err) + } + + parameters = append(parameters, reflectValue) + } + + return parameters, nil + } + """.trimIndent() + + private val parseTestInputFunction = """ + func __parseTestInput__(decoder *json.Decoder) (funcName string, rawValues []__RawValue__, err error) { + var testInput __TestInput__ + err = decoder.Decode(&testInput) + if err != nil { + return + } + + funcName = testInput.FunctionName + rawValues = make([]__RawValue__, 0, 10) + for _, arg := range testInput.Arguments { + var rawValue __RawValue__ + + rawValue, err = __parseRawValue__(arg, "") + if err != nil { + return "", nil, fmt.Errorf("failed to parse argument %s of function %s: %s", arg, funcName, err) + } + + rawValues = append(rawValues, rawValue) + } + + return + } + """.trimIndent() + + private val parseRawValueFunction = """ + func __parseRawValue__(rawValue map[string]interface{}, name string) (__RawValue__, error) { + typeName, ok := rawValue["type"] + if !ok { + return nil, fmt.Errorf("every RawValue must contain field 'type'") + } + typeNameStr, ok := typeName.(string) + if !ok { + return nil, fmt.Errorf("field 'type' must be string") + } + + v, ok := rawValue["value"] + if !ok { + return __NilValue__{Type: typeNameStr}, nil + } + + switch { + case typeNameStr == "struct{}": + if name == "" { + return nil, fmt.Errorf("anonymous structs is not supported") + } + + value, ok := v.([]interface{}) + if !ok { + return nil, fmt.Errorf("StructValue field 'value' must be array") + } + + values := make([]__FieldValue__, 0, len(value)) + for _, v := range value { + nextValue, err := __parseFieldValue__(v.(map[string]interface{})) + if err != nil { + return nil, fmt.Errorf("failed to parse field %s of struct: %s", v, err) + } + + values = append(values, nextValue) + } + + return __StructValue__{ + Type: name, + Value: values, + }, nil + case strings.HasPrefix(typeNameStr, "map["): + keyType, ok := rawValue["keyType"] + if !ok { + return nil, fmt.Errorf("MapValue must contain field 'keyType'") + } + keyTypeStr, ok := keyType.(string) + if !ok { + return nil, fmt.Errorf("MapValue field 'keyType' must be string") + } + + elementType, ok := rawValue["elementType"] + if !ok { + return nil, fmt.Errorf("MapValue must contain field 'elementType'") + } + elementTypeStr, ok := elementType.(string) + if !ok { + return nil, fmt.Errorf("MapValue field 'elementType' must be string") + } + + value, ok := v.([]interface{}) + if !ok { + return nil, fmt.Errorf("MapValue field 'value' must be array") + } + + values := make([]__KeyValue__, 0, len(value)) + for _, v := range value { + nextValue, err := __parseKeyValue__(v.(map[string]interface{})) + if err != nil { + return nil, fmt.Errorf("failed to parse KeyValue %s of map: %s", v, err) + } + + values = append(values, nextValue) + } + + return __MapValue__{ + Type: typeNameStr, + KeyType: keyTypeStr, + ElementType: elementTypeStr, + Value: values, + }, nil + case strings.HasPrefix(typeNameStr, "[]"): + elementType, ok := rawValue["elementType"] + if !ok { + return nil, fmt.Errorf("SliceValue must contain field 'elementType'") + } + elementTypeStr, ok := elementType.(string) + if !ok { + return nil, fmt.Errorf("SliceValue field 'elementType' must be string") + } + + if _, ok := rawValue["length"]; !ok { + return nil, fmt.Errorf("SliceValue must contain field 'length'") + } + length, ok := rawValue["length"].(float64) + if !ok { + return nil, fmt.Errorf("SliceValue field 'length' must be float64") + } + + value, ok := v.([]interface{}) + if !ok || len(value) != int(length) { + return nil, fmt.Errorf("SliceValue field 'value' must be array of length %d", int(length)) + } + + values := make([]__RawValue__, 0, len(value)) + for i, v := range value { + nextValue, err := __parseRawValue__(v.(map[string]interface{}), "") + if err != nil { + return nil, fmt.Errorf("failed to parse %d slice element: %s", i, err) + } + + values = append(values, nextValue) + } + + return __SliceValue__{ + Type: typeNameStr, + ElementType: elementTypeStr, + Length: int(length), + Value: values, + }, nil + case strings.HasPrefix(typeNameStr, "["): + elementType, ok := rawValue["elementType"] + if !ok { + return nil, fmt.Errorf("ArrayValue must contain field 'elementType'") + } + elementTypeStr, ok := elementType.(string) + if !ok { + return nil, fmt.Errorf("ArrayValue field 'elementType' must be string") + } + + if _, ok := rawValue["length"]; !ok { + return nil, fmt.Errorf("ArrayValue must contain field 'length'") + } + length, ok := rawValue["length"].(float64) + if !ok { + return nil, fmt.Errorf("ArrayValue field 'length' must be float64") + } + + value, ok := v.([]interface{}) + if !ok || len(value) != int(length) { + return nil, fmt.Errorf("ArrayValue field 'value' must be array of length %d", int(length)) + } + + values := make([]__RawValue__, 0, len(value)) + for i, v := range value { + nextValue, err := __parseRawValue__(v.(map[string]interface{}), "") + if err != nil { + return nil, fmt.Errorf("failed to parse %d array element: %s", i, err) + } + + values = append(values, nextValue) + } + + return __ArrayValue__{ + Type: typeNameStr, + ElementType: elementTypeStr, + Length: int(length), + Value: values, + }, nil + case strings.HasPrefix(typeNameStr, "<-chan") || strings.HasPrefix(typeNameStr, "chan"): + elementType, ok := rawValue["elementType"] + if !ok { + return nil, fmt.Errorf("ChanValue must contain field 'elementType'") + } + elementTypeStr, ok := elementType.(string) + if !ok { + return nil, fmt.Errorf("ChanValue field 'elementType' must be string") + } + + dir, ok := rawValue["direction"] + if !ok { + return nil, fmt.Errorf("ChanValue must contain field 'direction'") + } + direction, ok := dir.(string) + if !ok { + return nil, fmt.Errorf("ChanValue field 'direction' must be string") + } + + if _, ok := rawValue["length"]; !ok { + return nil, fmt.Errorf("ChanValue must contain field 'length'") + } + length, ok := rawValue["length"].(float64) + if !ok { + return nil, fmt.Errorf("ChanValue field 'length' must be float64") + } + + value, ok := v.([]interface{}) + if !ok || len(value) != int(length) { + return nil, fmt.Errorf("ChanValue field 'value' must be array of length %d", int(length)) + } + + values := make([]__RawValue__, 0, len(value)) + for i, v := range value { + nextValue, err := __parseRawValue__(v.(map[string]interface{}), "") + if err != nil { + return nil, fmt.Errorf("failed to parse %d chan element: %s", i, err) + } + + values = append(values, nextValue) + } + + return __ChanValue__{ + Type: typeNameStr, + ElementType: elementTypeStr, + Direction: direction, + Length: int(length), + Value: values, + }, nil + case strings.HasPrefix(typeNameStr, "*"): + elementType, ok := rawValue["elementType"] + if !ok { + return nil, fmt.Errorf("PointerValue must contain field 'elementType'") + } + elementTypeStr, ok := elementType.(string) + if !ok { + return nil, fmt.Errorf("PointerValue field 'elementType' must be string") + } + + value, err := __parseRawValue__(v.(map[string]interface{}), "") + if err != nil { + return nil, fmt.Errorf("failed to parse of PointerValue with type %s: %s", typeNameStr, err) + } + + return __PointerValue__{ + Type: typeNameStr, + ElementType: elementTypeStr, + Value: value, + }, nil + default: + switch typeNameStr { + case "bool", "rune", "int", "int8", "int16", "int32", "int64", "byte", "uint", "uint8", "uint16", "uint32", "uint64", "float32", "float64", "complex64", "complex128", "string", "uintptr": + value, ok := v.(string) + if !ok { + return nil, fmt.Errorf("PrimitiveValue field 'value' must be string") + } + + return __PrimitiveValue__{ + Type: typeNameStr, + Value: value, + }, nil + default: // named type + value, err := __parseRawValue__(v.(map[string]interface{}), typeNameStr) + if err != nil { + return nil, fmt.Errorf("failed to parse of NamedValue with type %s: %s", typeNameStr, err) + } + + return __NamedValue__{ + Type: typeNameStr, + Value: value, + }, nil + } + } + } + """.trimIndent() + + private val parseFieldValueFunction = """ + func __parseFieldValue__(p map[string]interface{}) (__FieldValue__, error) { + name, ok := p["name"] + if !ok { + return __FieldValue__{}, fmt.Errorf("FieldValue must contain field 'name'") + } + nameStr, ok := name.(string) + if !ok { + return __FieldValue__{}, fmt.Errorf("FieldValue 'name' must be string") + } + + if _, ok := p["value"]; !ok { + return __FieldValue__{}, fmt.Errorf("FieldValue must contain field 'value'") + } + value, err := __parseRawValue__(p["value"].(map[string]interface{}), "") + if err != nil { + return __FieldValue__{}, err + } + + isExported, ok := p["isExported"] + if !ok { + return __FieldValue__{}, fmt.Errorf("FieldValue must contain field 'isExported'") + } + isExportedBool, ok := isExported.(bool) + if !ok { + return __FieldValue__{}, fmt.Errorf("FieldValue 'isExported' must be bool") + } + + return __FieldValue__{ + Name: nameStr, + Value: value, + IsExported: isExportedBool, + }, nil + } + """.trimIndent() + + private val parseKeyValueFunction = """ + func __parseKeyValue__(p map[string]interface{}) (__KeyValue__, error) { + if _, ok := p["key"]; !ok { + return __KeyValue__{}, fmt.Errorf("KeyValue must contain field 'key'") + } + key, err := __parseRawValue__(p["key"].(map[string]interface{}), "") + if err != nil { + return __KeyValue__{}, err + } + + if _, ok := p["value"]; !ok { + return __KeyValue__{}, fmt.Errorf("KeyValue must contain field 'value'") + } + value, err := __parseRawValue__(p["value"].(map[string]interface{}), "") + if err != nil { + return __KeyValue__{}, err + } + + return __KeyValue__{ + Key: key, + Value: value, + }, nil + } + """.trimIndent() + + fun getTopLevelHelperStructsAndFunctionsForWorker( + namedTypes: Set, + destinationPackage: GoPackage, + aliases: Map, + ) = listOf( + errorMessages, + typeOfExecutionResult, + executionResultStruct, + testInputStruct, + rawValueInterface, + primitiveValueStruct, + parseFloatFunction, + primitiveValueToReflectValueMethod, + fieldValueStruct, + structValueStruct, + structValueToReflectValueMethod, + keyValueStruct, + mapValueStruct, + mapValueToReflectValueMethod, + arrayValueStruct, + arrayValueToReflectValueMethod, + sliceValueStruct, + sliceValueToReflectValueMethod, + chanValueStruct, + chanValueToReflectValueMethod, + nilValueStruct, + nilValueToReflectValueMethod, + namedValueStruct, + namedValueToReflectValueMethod, + pointerValueStruct, + pointerValueToReflectValueMethod, + convertStringToReflectTypeFunction(namedTypes, destinationPackage, aliases), + panicMessageStruct, + rawExecutionResultStruct, + convertReflectValueOfDefinedTypeToRawValueFunction, + convertFloat64ValueToStringFunction, + convertReflectValueOfPredeclaredOrNotDefinedTypeToRawValueFunction, + convertReflectValueToRawValueFunction, + executeFunctionFunction, + wrapExecutionResultFunction, + wrapResultValuesForWorkerFunction, + convertRawValuesToReflectValuesFunction, + parseTestInputFunction, + parseRawValueFunction, + parseFieldValueFunction, + parseKeyValueFunction + ) +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorker.kt b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorker.kt new file mode 100644 index 0000000000..e29b250f2c --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorker.kt @@ -0,0 +1,169 @@ +package org.utbot.go.worker + +import com.beust.klaxon.Klaxon +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.util.convertToRawValue +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.framework.api.go.GoUtModel +import org.utbot.go.util.convertObjectToJsonString +import org.utbot.go.util.executeCommandByNewProcessOrFailWithoutWaiting +import org.utbot.go.util.modifyEnvironment +import java.io.* +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketTimeoutException +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import java.util.concurrent.TimeUnit + +class GoWorker private constructor( + private var process: Process, + private var socket: Socket, + private val testFunctionName: String, + private val testFilePath: String, + private val goPackage: GoPackage, + private val goExecutableAbsolutePath: Path, + private val gopathAbsolutePath: Path, + private val workingDirectory: File, + private val serverSocket: ServerSocket, + private val readTimeoutMillis: Long, + private val connectionTimeoutMillis: Long, + private val endOfWorkerExecutionTimeoutMillis: Long +) : Closeable { + private var input: DataInputStream = DataInputStream(socket.getInputStream()) + private var output: DataOutputStream = DataOutputStream(socket.getOutputStream()) + + data class TestInput( + val functionName: String, val arguments: List + ) + + fun sendFuzzedParametersValues( + function: GoUtFunction, arguments: List, aliases: Map + ): Int { + val rawValues = arguments.map { it.convertToRawValue(goPackage, aliases) } + val testCase = TestInput(function.name, rawValues) + val json = convertObjectToJsonString(testCase) + output.write(json.encodeToByteArray()) + output.flush() + return json.length + } + + fun receiveRawExecutionResult(): RawExecutionResult { + socket.soTimeout = readTimeoutMillis.toInt() + val length = input.readInt() + val buffer = ByteArray(length) + input.read(buffer) + return Klaxon().parse(buffer.toString(StandardCharsets.UTF_8)) + ?: error("Error with parsing json as raw execution result") + } + + fun restartWorker() { + socket.close() + input.close() + output.close() + + process.destroy() + process = startWorkerProcess( + testFunctionName, testFilePath, goExecutableAbsolutePath, gopathAbsolutePath, workingDirectory + ) + + socket = connectingToWorker( + serverSocket, process, connectionTimeoutMillis, endOfWorkerExecutionTimeoutMillis + ) + input = DataInputStream(socket.getInputStream()) + output = DataOutputStream(socket.getOutputStream()) + } + + override fun close() { + socket.close() + input.close() + output.close() + + val processHasExited = process.waitFor(endOfWorkerExecutionTimeoutMillis, TimeUnit.MILLISECONDS) + if (!processHasExited) { + process.destroy() + val processOutput = InputStreamReader(process.inputStream).readText() + throw TimeoutException(buildString { + appendLine("Timeout exceeded: Worker didn't finish. Process output: ") + appendLine(processOutput) + }) + } + val exitCode = process.exitValue() + if (exitCode != 0) { + val processOutput = InputStreamReader(process.inputStream).readText() + throw RuntimeException(buildString { + appendLine("Execution of functions in child process failed with non-zero exit code = $exitCode: ") + appendLine(processOutput) + }) + } + } + + companion object { + private fun startWorkerProcess( + testFunctionName: String, + testFileName: String, + goExecutableAbsolutePath: Path, + gopathAbsolutePath: Path, + workingDirectory: File, + ): Process { + val environment = modifyEnvironment(goExecutableAbsolutePath, gopathAbsolutePath) + val command = listOf(testFileName, "--test.run", testFunctionName) + return executeCommandByNewProcessOrFailWithoutWaiting(command, workingDirectory, environment) + } + + private fun connectingToWorker( + serverSocket: ServerSocket, + process: Process, + connectionTimeoutMillis: Long, + endOfWorkerExecutionTimeout: Long, + ): Socket { + val workerSocket = try { + serverSocket.soTimeout = connectionTimeoutMillis.toInt() + serverSocket.accept() + } catch (e: SocketTimeoutException) { + val processHasExited = process.waitFor(endOfWorkerExecutionTimeout, TimeUnit.MILLISECONDS) + if (processHasExited) { + throw GoWorkerFailedException("An error occurred while starting the worker.") + } else { + process.destroy() + } + throw TimeoutException("Timeout exceeded: Worker not connected") + } + return workerSocket + } + + fun createWorker( + testFunctionName: String, + testFilePath: String, + goPackage: GoPackage, + goExecutableAbsolutePath: Path, + gopathAbsolutePath: Path, + workingDirectory: File, + serverSocket: ServerSocket, + connectionTimeoutMillis: Long = 10000, + endOfWorkerExecutionTimeout: Long = 5000, + ): GoWorker { + val workerProcess = startWorkerProcess( + testFunctionName, testFilePath, goExecutableAbsolutePath, gopathAbsolutePath, workingDirectory + ) + val workerSocket = connectingToWorker( + serverSocket, workerProcess, connectionTimeoutMillis, endOfWorkerExecutionTimeout + ) + return GoWorker( + process = workerProcess, + socket = workerSocket, + testFunctionName = testFunctionName, + testFilePath = testFilePath, + goPackage = goPackage, + goExecutableAbsolutePath = goExecutableAbsolutePath, + gopathAbsolutePath = gopathAbsolutePath, + workingDirectory = workingDirectory, + serverSocket = serverSocket, + readTimeoutMillis = 2 * endOfWorkerExecutionTimeout, + connectionTimeoutMillis = connectionTimeoutMillis, + endOfWorkerExecutionTimeoutMillis = endOfWorkerExecutionTimeout + ) + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorkerCodeGenerationHelper.kt b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorkerCodeGenerationHelper.kt new file mode 100644 index 0000000000..ae87fd1344 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorkerCodeGenerationHelper.kt @@ -0,0 +1,205 @@ +package org.utbot.go.worker + +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.util.getAllVisibleNamedTypes +import org.utbot.go.framework.api.go.GoImport +import org.utbot.go.framework.api.go.GoPackage +import org.utbot.go.simplecodegeneration.GoFileCodeBuilder +import java.io.File + +internal object GoWorkerCodeGenerationHelper { + + const val workerTestFunctionName = "TestGoFileFuzzedFunctionsByUtGoWorker" + + val alwaysRequiredImports = setOf( + GoPackage("io", "io"), + GoPackage("os", "os"), + GoPackage("context", "context"), + GoPackage("binary", "encoding/binary"), + GoPackage("json", "encoding/json"), + GoPackage("fmt", "fmt"), + GoPackage("math", "math"), + GoPackage("net", "net"), + GoPackage("reflect", "reflect"), + GoPackage("strconv", "strconv"), + GoPackage("strings", "strings"), + GoPackage("testing", "testing"), + GoPackage("time", "time"), + GoPackage("unsafe", "unsafe") + ).map { GoImport(it) }.toSet() + + fun createFileToExecute( + sourceFile: GoUtFile, + functions: List, + absoluteInstrumentedPackagePath: String, + eachExecutionTimeoutMillis: Long, + port: Int, + imports: Set + ): File { + val fileToExecuteName = createFileToExecuteName() + val sourceFileDir = File(absoluteInstrumentedPackagePath) + val fileToExecute = sourceFileDir.resolve(fileToExecuteName) + + val fileToExecuteGoCode = + generateWorkerTestFileGoCode( + sourceFile, + functions, + eachExecutionTimeoutMillis, + port, + imports + ) + fileToExecute.writeText(fileToExecuteGoCode) + return fileToExecute + } + + fun createFileWithCoverTab( + sourceFile: GoUtFile, + absoluteInstrumentedPackagePath: String, + ): File { + val fileWithCoverTabName = createFileWithCoverTabName() + val sourceFileDir = File(absoluteInstrumentedPackagePath) + val fileWithCoverTab = sourceFileDir.resolve(fileWithCoverTabName) + + val fileWithCoverTabGoCode = generateFileWithCoverTabGoCode(sourceFile.sourcePackage) + fileWithCoverTab.writeText(fileWithCoverTabGoCode) + return fileWithCoverTab + } + + private fun createFileToExecuteName(): String { + return "utbot_go_worker_test.go" + } + + private fun createFileWithCoverTabName(): String { + return "utbot_go_cover.go" + } + + private fun generateWorkerTestFileGoCode( + sourceFile: GoUtFile, + functions: List, + eachExecutionTimeoutMillis: Long, + port: Int, + imports: Set + ): String { + val destinationPackage = sourceFile.sourcePackage + val fileCodeBuilder = GoFileCodeBuilder(destinationPackage, imports) + + val types = functions.flatMap { + it.parameters + if (it.isMethod) listOf(it.receiver!!) else emptyList() + }.map { it.type } + val aliases = imports.associate { it.goPackage to it.alias } + val namedTypes = types.getAllVisibleNamedTypes(destinationPackage) + + val workerTestFunctionCode = generateWorkerTestFunctionCode( + functions, destinationPackage, aliases, eachExecutionTimeoutMillis, port + ) + fileCodeBuilder.addTopLevelElements( + GoCodeTemplates.getTopLevelHelperStructsAndFunctionsForWorker( + namedTypes, + destinationPackage, + aliases, + ) + workerTestFunctionCode + ) + + return fileCodeBuilder.buildCodeString() + } + + private fun generateFileWithCoverTabGoCode(goPackage: GoPackage): String = """ + package ${goPackage.name} + + const __CoverSize__ = 64 << 10 + + var __CoverTab__ []int + """.trimIndent() + + private fun generateWorkerTestFunctionCode( + functions: List, + destinationPackage: GoPackage, + aliases: Map, + eachExecutionTimeoutMillis: Long, + port: Int + ): String { + val functionNameToFunctionCall = functions.map { function -> + function.name to if (function.isMethod) { + "(${function.receiver!!.type.getRelativeName(destinationPackage, aliases)}).${function.name}" + } else { + function.name + } + } + return """ + func $workerTestFunctionName(t *testing.T) { + con, err := net.Dial("tcp", ":$port") + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Connection to server failed: %s", err) + os.Exit(1) + } + + defer func() { + err = con.Close() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Closing connection failed: %s", err) + os.Exit(1) + } + }() + + jsonDecoder := json.NewDecoder(con) + for { + var ( + funcName string + rawValues []__RawValue__ + ) + funcName, rawValues, err = __parseTestInput__(jsonDecoder) + if err == io.EOF { + break + } + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to parse test input: %s", err) + os.Exit(1) + } + + var arguments []reflect.Value + arguments, err = __convertRawValuesToReflectValues__(rawValues) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to convert slice of RawValue to slice of reflect.Value: %s", err) + os.Exit(1) + } + + var function reflect.Value + switch funcName { + ${ + functionNameToFunctionCall.joinToString(separator = "\n") { (functionName, functionCall) -> + "case \"${functionName}\": function = reflect.ValueOf($functionCall)" + } + } + default: + _, _ = fmt.Fprintf(os.Stderr, "Function %s not found", funcName) + os.Exit(1) + } + + executionResult := __executeFunction__(function, arguments, $eachExecutionTimeoutMillis*time.Millisecond) + + var jsonBytes []byte + jsonBytes, err = json.Marshal(executionResult) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to serialize execution result to json: %s", err) + os.Exit(1) + } + + bs := make([]byte, 4) + binary.BigEndian.PutUint32(bs, uint32(len(jsonBytes))) + _, err = con.Write(bs) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to send length of execution result: %s", err) + os.Exit(1) + } + + _, err = con.Write(jsonBytes) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to send execution result: %s", err) + os.Exit(1) + } + } + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/worker/RawExecutionResults.kt b/utbot-go/src/main/kotlin/org/utbot/go/worker/RawExecutionResults.kt new file mode 100644 index 0000000000..3fb23a76bd --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/worker/RawExecutionResults.kt @@ -0,0 +1,260 @@ +package org.utbot.go.worker + +import com.beust.klaxon.TypeAdapter +import com.beust.klaxon.TypeFor +import org.utbot.go.api.* +import org.utbot.go.api.util.* +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import kotlin.reflect.KClass + +data class PrimitiveValue( + override val type: String, + val value: String, +) : RawValue(type) + +data class NamedValue( + override val type: String, + val value: RawValue, +) : RawValue(type) + +data class StructValue( + override val type: String, + val value: List +) : RawValue(type) { + data class FieldValue( + val name: String, + val value: RawValue, + val isExported: Boolean + ) +} + +data class ArrayValue( + override val type: String, + val elementType: String, + val length: Int, + val value: List +) : RawValue(type) + +data class SliceValue( + override val type: String, + val elementType: String, + val length: Int, + val value: List +) : RawValue(type) + +data class MapValue( + override val type: String, + val keyType: String, + val elementType: String, + val value: List +) : RawValue(type) { + data class KeyValue( + val key: RawValue, + val value: RawValue + ) +} + +data class ChanValue( + override val type: String, + val elementType: String, + val direction: String, + val length: Int, + val value: List +) : RawValue(type) + +data class NilValue(override val type: String) : RawValue(type) + +data class InterfaceValue(override val type: String) : RawValue(type) + +data class PointerValue(override val type: String, val elementType: String, val value: RawValue) : RawValue(type) + +@TypeFor(field = "type", adapter = RawValueAdapter::class) +abstract class RawValue(open val type: String) + +class RawValueAdapter : TypeAdapter { + override fun classFor(type: Any): KClass { + val typeName = type as String + return when { + typeName == "nil" -> NilValue::class + typeName == "interface{}" -> InterfaceValue::class + typeName == "struct{}" -> StructValue::class + typeName.startsWith("map[") -> MapValue::class + typeName.startsWith("[]") -> SliceValue::class + typeName.startsWith("[") -> ArrayValue::class + typeName.startsWith("<-chan") || typeName.startsWith("chan") -> ChanValue::class + typeName.startsWith("*") -> PointerValue::class + goPrimitives.map { it.name }.contains(typeName) -> PrimitiveValue::class + else -> NamedValue::class + } + } +} + +data class RawPanicMessage( + val rawResultValue: RawValue, val implementsError: Boolean +) + +data class RawExecutionResult( + val timeoutExceeded: Boolean, + val rawResultValues: List, + val panicMessage: RawPanicMessage?, + val coverTab: Map +) + +private object RawValuesCodes { + const val NAN_VALUE = "NaN" + const val POS_INF_VALUE = "+Inf" + const val NEG_INF_VALUE = "-Inf" + const val COMPLEX_PARTS_DELIMITER = "@" +} + +class GoWorkerFailedException(s: String) : Exception(s) + +fun convertRawExecutionResultToExecutionResult( + rawExecutionResult: RawExecutionResult, functionResultTypes: List, timeoutMillis: Long +): GoUtExecutionResult { + if (rawExecutionResult.timeoutExceeded) { + return GoUtTimeoutExceeded(timeoutMillis) + } + if (rawExecutionResult.panicMessage != null) { + val (rawResultValue, implementsError) = rawExecutionResult.panicMessage + val panicValue = if (goPrimitives.map { it.name }.contains(rawResultValue.type)) { + createGoUtPrimitiveModelFromRawValue( + rawResultValue as PrimitiveValue, GoPrimitiveTypeId(rawResultValue.type) + ) + } else { + error("Only primitive panic value is currently supported") + } + return GoUtPanicFailure(panicValue, implementsError) + } + if (rawExecutionResult.rawResultValues.size != functionResultTypes.size) { + error("Function completed execution must have as many result raw values as result types.") + } + var executedWithNonNilErrorString = false + val resultValues = rawExecutionResult.rawResultValues.zip(functionResultTypes).map { (rawResultValue, resultType) -> + val model = createGoUtModelFromRawValue(rawResultValue, resultType) + if (resultType.implementsError && (model is GoUtNamedModel && model.value.typeId == goStringTypeId)) { + executedWithNonNilErrorString = true + } + return@map model + } + return if (executedWithNonNilErrorString) { + GoUtExecutionWithNonNilError(resultValues) + } else { + GoUtExecutionSuccess(resultValues) + } +} + +private fun createGoUtModelFromRawValue( + rawValue: RawValue, typeId: GoTypeId, +): GoUtModel = if (rawValue is NilValue) { + GoUtNilModel(typeId) +} else { + when (typeId) { + is GoNamedTypeId -> createGoUtNamedModelFromRawValue(rawValue as NamedValue, typeId) + // Only for error interface + is GoInterfaceTypeId -> GoUtPrimitiveModel((rawValue as PrimitiveValue).value, goStringTypeId) + + is GoStructTypeId -> createGoUtStructModelFromRawValue(rawValue as StructValue, typeId) + + is GoArrayTypeId -> createGoUtArrayModelFromRawValue(rawValue as ArrayValue, typeId) + + is GoSliceTypeId -> createGoUtSliceModelFromRawValue(rawValue as SliceValue, typeId) + + is GoMapTypeId -> createGoUtMapModelFromRawValue(rawValue as MapValue, typeId) + + is GoPrimitiveTypeId -> createGoUtPrimitiveModelFromRawValue(rawValue as PrimitiveValue, typeId) + + is GoPointerTypeId -> createGoUtPointerModelFromRawValue(rawValue as PointerValue, typeId) + + is GoChanTypeId -> createGoUtChanModelFromRawValue(rawValue as ChanValue, typeId) + + else -> error("Creating a model from raw value of [${typeId.javaClass}] type is not supported") + } +} + +private fun createGoUtPrimitiveModelFromRawValue( + resultValue: PrimitiveValue, typeId: GoPrimitiveTypeId, +): GoUtPrimitiveModel { + val rawValue = resultValue.value + if (typeId == goFloat64TypeId || typeId == goFloat32TypeId) { + return convertRawFloatValueToGoUtPrimitiveModel(rawValue, typeId) + } + if (typeId == goComplex128TypeId || typeId == goComplex64TypeId) { + val correspondingFloatType = if (typeId == goComplex128TypeId) goFloat64TypeId else goFloat32TypeId + val (realPartModel, imagPartModel) = rawValue.split(RawValuesCodes.COMPLEX_PARTS_DELIMITER).map { + convertRawFloatValueToGoUtPrimitiveModel(it, correspondingFloatType, typeId == goComplex64TypeId) + } + return GoUtComplexModel(realPartModel, imagPartModel, typeId) + } + val value = rawValueOfGoPrimitiveTypeToValue(typeId, rawValue) + return GoUtPrimitiveModel(value, typeId) +} + +private fun convertRawFloatValueToGoUtPrimitiveModel( + rawValue: String, typeId: GoPrimitiveTypeId, explicitCastRequired: Boolean = false +): GoUtPrimitiveModel { + return when (rawValue) { + RawValuesCodes.NAN_VALUE -> GoUtFloatNaNModel(typeId) + RawValuesCodes.POS_INF_VALUE -> GoUtFloatInfModel(1, typeId) + RawValuesCodes.NEG_INF_VALUE -> GoUtFloatInfModel(-1, typeId) + else -> { + val typedValue = if (typeId == goFloat64TypeId) rawValue.toDouble() else rawValue.toFloat() + if (explicitCastRequired) { + GoUtPrimitiveModel(typedValue, typeId, explicitCastMode = ExplicitCastMode.REQUIRED) + } else { + GoUtPrimitiveModel(typedValue, typeId) + } + } + } +} + +private fun createGoUtStructModelFromRawValue(resultValue: StructValue, resultTypeId: GoStructTypeId): GoUtStructModel { + val value = linkedMapOf(*resultValue.value.zip(resultTypeId.fields).map { (value, fieldId) -> + fieldId to createGoUtModelFromRawValue(value.value, fieldId.declaringType) + }.toTypedArray()) + return GoUtStructModel(value, resultTypeId) +} + +private fun createGoUtArrayModelFromRawValue(resultValue: ArrayValue, resultTypeId: GoArrayTypeId): GoUtArrayModel { + val value = resultValue.value.map { + createGoUtModelFromRawValue(it, resultTypeId.elementTypeId!!) + }.toTypedArray() + return GoUtArrayModel(value, resultTypeId) +} + +private fun createGoUtSliceModelFromRawValue(resultValue: SliceValue, resultTypeId: GoSliceTypeId): GoUtSliceModel { + val value = resultValue.value.map { + createGoUtModelFromRawValue(it, resultTypeId.elementTypeId!!) + }.toTypedArray() + return GoUtSliceModel(value, resultTypeId, resultValue.length) +} + +private fun createGoUtMapModelFromRawValue(resultValue: MapValue, resultTypeId: GoMapTypeId): GoUtMapModel { + val value = resultValue.value.associate { + val key = createGoUtModelFromRawValue(it.key, resultTypeId.keyTypeId) + val value = createGoUtModelFromRawValue(it.value, resultTypeId.elementTypeId!!) + key to value + }.toMutableMap() + return GoUtMapModel(value, resultTypeId) +} + +private fun createGoUtNamedModelFromRawValue(resultValue: NamedValue, resultTypeId: GoNamedTypeId): GoUtNamedModel { + val value = createGoUtModelFromRawValue(resultValue.value, resultTypeId.underlyingTypeId) + return GoUtNamedModel(value, resultTypeId) +} + +private fun createGoUtPointerModelFromRawValue( + resultValue: PointerValue, + resultTypeId: GoPointerTypeId +): GoUtPointerModel { + val value = createGoUtModelFromRawValue(resultValue.value, resultTypeId.elementTypeId!!) + return GoUtPointerModel(value, resultTypeId) +} + +private fun createGoUtChanModelFromRawValue(resultValue: ChanValue, resultTypeId: GoChanTypeId): GoUtChanModel { + val value = resultValue.value.map { + createGoUtModelFromRawValue(it, resultTypeId.elementTypeId!!) + }.toTypedArray() + return GoUtChanModel(value, resultTypeId) +} \ No newline at end of file diff --git a/utbot-go/src/main/resources/go_package_instrumentation/go.mod b/utbot-go/src/main/resources/go_package_instrumentation/go.mod new file mode 100644 index 0000000000..0c811c6551 --- /dev/null +++ b/utbot-go/src/main/resources/go_package_instrumentation/go.mod @@ -0,0 +1,10 @@ +module go_package_modifier + +go 1.18 + +require golang.org/x/tools v0.8.0 + +require ( + golang.org/x/mod v0.10.0 // indirect + golang.org/x/sys v0.7.0 // indirect +) diff --git a/utbot-go/src/main/resources/go_package_instrumentation/go.sum b/utbot-go/src/main/resources/go_package_instrumentation/go.sum new file mode 100644 index 0000000000..d84f5f1091 --- /dev/null +++ b/utbot-go/src/main/resources/go_package_instrumentation/go.sum @@ -0,0 +1,7 @@ +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= diff --git a/utbot-go/src/main/resources/go_package_instrumentation/instrumentation_result.go b/utbot-go/src/main/resources/go_package_instrumentation/instrumentation_result.go new file mode 100644 index 0000000000..955303b49a --- /dev/null +++ b/utbot-go/src/main/resources/go_package_instrumentation/instrumentation_result.go @@ -0,0 +1,7 @@ +package main + +type InstrumentationResult struct { + AbsolutePathToInstrumentedPackage string `json:"absolutePathToInstrumentedPackage"` + AbsolutePathToInstrumentedModule string `json:"absolutePathToInstrumentedModule"` + TestedFunctionsToCounters map[string][]string `json:"testedFunctionsToCounters"` +} diff --git a/utbot-go/src/main/resources/go_package_instrumentation/instrumentation_target.go b/utbot-go/src/main/resources/go_package_instrumentation/instrumentation_target.go new file mode 100644 index 0000000000..bc98cece3a --- /dev/null +++ b/utbot-go/src/main/resources/go_package_instrumentation/instrumentation_target.go @@ -0,0 +1,6 @@ +package main + +type InstrumentationTarget struct { + AbsolutePackagePath string `json:"absolutePackagePath"` + TestedFunctions []string `json:"testedFunctions"` +} diff --git a/utbot-go/src/main/resources/go_package_instrumentation/instrumentator.go b/utbot-go/src/main/resources/go_package_instrumentation/instrumentator.go new file mode 100644 index 0000000000..1ec4dbbc09 --- /dev/null +++ b/utbot-go/src/main/resources/go_package_instrumentation/instrumentator.go @@ -0,0 +1,164 @@ +package main + +import ( + "crypto/sha1" + "go/ast" + "go/token" + "strconv" +) + +type Instrumentator struct { + lineCounter int + currFunction string + testedFunctions map[string]bool + functionToCounters map[string][]string +} + +func NewInstrumentator(testedFunctions map[string]bool) Instrumentator { + return Instrumentator{ + testedFunctions: testedFunctions, + functionToCounters: make(map[string][]string), + } +} + +func (f *Instrumentator) Visit(node ast.Node) ast.Visitor { + switch n := node.(type) { + case *ast.FuncDecl: + n.Doc = nil + f.currFunction = n.Name.Name + case *ast.BlockStmt: + if n == nil { + n = &ast.BlockStmt{} + } + n.List = f.addCounter(n.List) + return nil + case *ast.IfStmt: + if n.Body == nil { + return nil + } + ast.Walk(f, n.Body) + if n.Else == nil { + n.Else = &ast.BlockStmt{} + } + switch stmt := n.Else.(type) { + case *ast.IfStmt: + n.Else = &ast.BlockStmt{List: []ast.Stmt{stmt}} + } + ast.Walk(f, n.Else) + return nil + case *ast.ForStmt: + if n.Body == nil { + return nil + } + ast.Walk(f, n.Body) + return nil + case *ast.RangeStmt: + if n.Body == nil { + return nil + } + ast.Walk(f, n.Body) + return nil + case *ast.SwitchStmt: + hasDefault := false + if n.Body == nil { + n.Body = &ast.BlockStmt{} + } + for _, stmt := range n.Body.List { + if cas, ok := stmt.(*ast.CaseClause); ok && cas.List == nil { + hasDefault = true + break + } + } + if !hasDefault { + n.Body.List = append(n.Body.List, &ast.CaseClause{}) + } + for _, stmt := range n.Body.List { + ast.Walk(f, stmt) + } + return nil + case *ast.TypeSwitchStmt: + hasDefault := false + if n.Body == nil { + n.Body = &ast.BlockStmt{} + } + for _, stmt := range n.Body.List { + if cas, ok := stmt.(*ast.CaseClause); ok && cas.List == nil { + hasDefault = true + break + } + } + if !hasDefault { + n.Body.List = append(n.Body.List, &ast.CaseClause{}) + } + for _, stmt := range n.Body.List { + ast.Walk(f, stmt) + } + return nil + case *ast.SelectStmt: + if n.Body == nil { + return nil + } + for _, stmt := range n.Body.List { + ast.Walk(f, stmt) + } + return nil + case *ast.CaseClause: + for _, expr := range n.List { + ast.Walk(f, expr) + } + n.Body = f.addCounter(n.Body) + return nil + case *ast.CommClause: + ast.Walk(f, n.Comm) + n.Body = f.addCounter(n.Body) + return nil + } + return f +} + +func (f *Instrumentator) addCounter(stmts []ast.Stmt) []ast.Stmt { + if len(stmts) == 0 { + return []ast.Stmt{f.newCounter()} + } + + var newList []ast.Stmt + for _, stmt := range stmts { + newList = append(newList, f.newCounter()) + ast.Walk(f, stmt) + newList = append(newList, stmt) + if _, ok := stmt.(*ast.ReturnStmt); ok { + break + } + } + return newList +} + +func (f *Instrumentator) getNextCounter() int { + f.lineCounter++ + id := f.lineCounter + buf := []byte{byte(id), byte(id >> 8), byte(id >> 16), byte(id >> 24)} + hash := sha1.Sum(buf) + return int(uint16(hash[0]) | uint16(hash[1])<<8) +} + +func (f *Instrumentator) newCounter() ast.Stmt { + cnt := strconv.Itoa(f.getNextCounter()) + + funcName := f.currFunction + if ok := f.testedFunctions[funcName]; ok { + f.functionToCounters[funcName] = append(f.functionToCounters[funcName], cnt) + } + + idx := &ast.BasicLit{ + Kind: token.INT, + Value: cnt, + } + counter := &ast.IndexExpr{ + X: ast.NewIdent("__CoverTab__"), + Index: idx, + } + return &ast.IncDecStmt{ + X: counter, + Tok: token.INC, + } +} diff --git a/utbot-go/src/main/resources/go_package_instrumentation/main.go b/utbot-go/src/main/resources/go_package_instrumentation/main.go new file mode 100644 index 0000000000..e59fea31e4 --- /dev/null +++ b/utbot-go/src/main/resources/go_package_instrumentation/main.go @@ -0,0 +1,178 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "go/ast" + "go/printer" + "golang.org/x/tools/go/packages" + "io" + "io/fs" + "os" + "path/filepath" + "strings" +) + +func failf(str string, args ...any) { + _, _ = fmt.Fprintf(os.Stderr, str+"\n", args...) + os.Exit(1) +} + +func instrument(astFile *ast.File, modifier *Instrumentator) { + ast.Walk(modifier, astFile) +} + +func copyFile(src, dst string) { + r, err := os.Open(src) + if err != nil { + failf("copyFile: could not read %v", src, err) + } + w, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + failf("copyFile: could not write %v: %v", dst, err) + } + if _, err := io.Copy(w, r); err != nil { + failf("copyFile: copying failed: %v", err) + } + if err := r.Close(); err != nil { + failf("copyFile: closing %v failed: %v", src, err) + } + if err := w.Close(); err != nil { + failf("copyFile: closing %v failed: %v", dst, err) + } +} + +func main() { + var targetFilePath, resultFilePath string + flag.StringVar(&targetFilePath, "target", "", "path to JSON file to read instrumentation target from") + flag.StringVar(&resultFilePath, "result", "", "path to JSON file to write instrumentation result to") + flag.Parse() + + // read and deserialize targets + targetBytes, readErr := os.ReadFile(targetFilePath) + if readErr != nil { + failf("failed to read file %s: %s", targetFilePath, readErr) + } + var instrumentationTarget InstrumentationTarget + fromJsonErr := json.Unmarshal(targetBytes, &instrumentationTarget) + if fromJsonErr != nil { + failf("failed to parse instrumentation target: %s", fromJsonErr) + } + + // parse package + pkgPath := instrumentationTarget.AbsolutePackagePath + cfg := packages.Config{ + Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | + packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | + packages.NeedDeps | packages.NeedModule | packages.NeedEmbedFiles | packages.NeedEmbedPatterns | packages.NeedExportFile, + Dir: pkgPath, + } + cfg.Env = os.Environ() + pkgs, err := packages.Load(&cfg, pkgPath) + if err != nil { + failf("failed to parse package: %s", err) + } + if len(pkgs) != 1 { + failf("cannot build multiple packages: %s", err) + } + if packages.PrintErrors(pkgs) > 0 { + failf("typechecking of %s failed", pkgPath) + } + + targetPackage := pkgs[0] + module := targetPackage.Module + + workdir, err := os.MkdirTemp("", "utbot-go*") + if err != nil { + failf("failed to create temporary directory: %s", err) + } + + testedFunctions := make(map[string]bool, len(instrumentationTarget.TestedFunctions)) + for _, f := range instrumentationTarget.TestedFunctions { + testedFunctions[f] = true + } + modifier := NewInstrumentator(testedFunctions) + absolutePathToInstrumentedPackage := "" + visit := func(pkg *packages.Package) { + if pkg.Module == nil || pkg.Module.Path != module.Path { + return + } + for i, fullName := range pkg.CompiledGoFiles { + fname := strings.Replace(fullName, module.Dir, "", 1) + outpath := filepath.Join(workdir, fname) + if !strings.HasSuffix(fullName, ".go") { + continue + } + astFile := pkg.Syntax[i] + if pkg.PkgPath == targetPackage.PkgPath { + instrument(astFile, &modifier) + absolutePathToInstrumentedPackage = filepath.Dir(outpath) + } + buf := new(bytes.Buffer) + c := printer.Config{ + Mode: printer.TabIndent, + Tabwidth: 4, + Indent: 0, + } + err = c.Fprint(buf, pkg.Fset, astFile) + if err != nil { + failf("failed to fprint: %s", err) + } + err = os.MkdirAll(filepath.Dir(outpath), 0666) + if err != nil { + failf("failed to create directories: %s", err) + } + _, err = os.Create(outpath) + if err != nil { + failf("failed to create file: %s", err) + } + err = os.WriteFile(outpath, buf.Bytes(), 0666) + if err != nil { + failf("failed to write to file: %s", err) + } + } + } + packages.Visit(pkgs, nil, visit) + + err = filepath.Walk(module.Dir, func(path string, info fs.FileInfo, err error) error { + if info.IsDir() && info.Name() == ".git" { + return filepath.SkipDir + } + if info.IsDir() { + dname := strings.Replace(path, module.Dir, "", 1) + dirPath := filepath.Join(workdir, dname) + err = os.MkdirAll(dirPath, 0666) + return err + } + if !strings.HasSuffix(path, ".go") { + fname := strings.Replace(path, module.Dir, "", 1) + outpath := filepath.Join(workdir, fname) + err = os.Symlink(path, outpath) + if err != nil { + copyFile(path, outpath) + } + return nil + } + return nil + }) + if err != nil { + failf("failed to walk the module directory: %s", err) + } + + // serialize and write results + instrumentationResult := InstrumentationResult{ + AbsolutePathToInstrumentedPackage: absolutePathToInstrumentedPackage, + AbsolutePathToInstrumentedModule: workdir, + TestedFunctionsToCounters: modifier.functionToCounters, + } + jsonBytes, toJsonErr := json.MarshalIndent(instrumentationResult, "", " ") + if toJsonErr != nil { + failf("failed to serialize instrumentation result: %s", toJsonErr) + } + writeErr := os.WriteFile(resultFilePath, jsonBytes, os.ModePerm) + if writeErr != nil { + failf("failed to write instrumentation result: %s", writeErr) + } +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go new file mode 100644 index 0000000000..7bfdeb455a --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go @@ -0,0 +1,131 @@ +package main + +import "go/token" + +type Package struct { + Name string `json:"name"` + Path string `json:"path"` +} + +type AnalyzedType interface { + GetName() string +} + +type AnalyzedNamedType struct { + Name string `json:"name"` + SourcePackage Package `json:"sourcePackage"` + ImplementsError bool `json:"implementsError"` + UnderlyingType string `json:"underlyingType"` +} + +func (t AnalyzedNamedType) GetName() string { + return t.Name +} + +type AnalyzedInterfaceType struct { + Name string `json:"name"` + Implementations []string `json:"implementations"` +} + +func (t AnalyzedInterfaceType) GetName() string { + return t.Name +} + +type AnalyzedPrimitiveType struct { + Name string `json:"name"` +} + +func (t AnalyzedPrimitiveType) GetName() string { + return t.Name +} + +type AnalyzedField struct { + Name string `json:"name"` + Type string `json:"type"` + IsExported bool `json:"isExported"` +} + +type AnalyzedStructType struct { + Name string `json:"name"` + Fields []AnalyzedField `json:"fields"` +} + +func (t AnalyzedStructType) GetName() string { + return t.Name +} + +type AnalyzedArrayType struct { + Name string `json:"name"` + ElementType string `json:"elementType"` + Length int64 `json:"length"` +} + +func (t AnalyzedArrayType) GetName() string { + return t.Name +} + +type AnalyzedSliceType struct { + Name string `json:"name"` + ElementType string `json:"elementType"` +} + +func (t AnalyzedSliceType) GetName() string { + return t.Name +} + +type AnalyzedMapType struct { + Name string `json:"name"` + KeyType string `json:"keyType"` + ElementType string `json:"elementType"` +} + +func (t AnalyzedMapType) GetName() string { + return t.Name +} + +type AnalyzedChanType struct { + Name string `json:"name"` + ElementType string `json:"elementType"` + Direction string `json:"direction"` +} + +func (t AnalyzedChanType) GetName() string { + return t.Name +} + +type AnalyzedPointerType struct { + Name string `json:"name"` + ElementType string `json:"elementType"` +} + +func (t AnalyzedPointerType) GetName() string { + return t.Name +} + +type AnalyzedVariable struct { + Name string `json:"name"` + Type string `json:"type"` +} + +type AnalyzedFunction struct { + Name string `json:"name"` + Types map[string]AnalyzedType `json:"types"` + Receiver *AnalyzedVariable `json:"receiver"` + Parameters []AnalyzedVariable `json:"parameters"` + ResultTypes []AnalyzedVariable `json:"resultTypes"` + Constants map[string][]string `json:"constants"` + position token.Pos +} + +type AnalysisResult struct { + AbsoluteFilePath string `json:"absoluteFilePath"` + SourcePackage Package `json:"sourcePackage"` + AnalyzedFunctions []AnalyzedFunction `json:"analyzedFunctions"` + NotSupportedFunctionNames []string `json:"notSupportedFunctionNames"` + NotFoundFunctionNames []string `json:"notFoundFunctionNames"` +} + +type AnalysisResults struct { + Results []AnalysisResult `json:"results"` + IntSize int `json:"intSize"` +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go new file mode 100644 index 0000000000..e71600523d --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go @@ -0,0 +1,11 @@ +package main + +type AnalysisTarget struct { + AbsoluteFilePath string `json:"absoluteFilePath"` + TargetFunctionNames []string `json:"targetFunctionNames"` + TargetMethodNames []string `json:"targetMethodNames"` +} + +type AnalysisTargets struct { + Targets []AnalysisTarget `json:"targets"` +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go b/utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go new file mode 100644 index 0000000000..10475e3d3e --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go @@ -0,0 +1,460 @@ +package main + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "sort" + "strconv" + "sync" +) + +var errorInterface = func() *types.Interface { + variable := types.NewVar(token.NoPos, nil, "", types.Typ[types.String]) + results := types.NewTuple(variable) + signature := types.NewSignatureType(nil, nil, nil, nil, results, false) + method := types.NewFunc(token.NoPos, nil, "Error", signature) + return types.NewInterfaceType([]*types.Func{method}, nil) +}() + +func implementsError(typ types.Type) bool { + return types.Implements(typ, errorInterface) +} + +func ChanDirToString(dir types.ChanDir) (string, error) { + switch dir { + case types.SendOnly: + return "SENDONLY", nil + case types.RecvOnly: + return "RECVONLY", nil + case types.SendRecv: + return "SENDRECV", nil + } + return "", fmt.Errorf("unsupported channel direction: %d", dir) +} + +func toAnalyzedType( + typ types.Type, + analyzedTypes map[string]AnalyzedType, + typeToIndex map[string]string, + sourcePackage Package, + currentPackage Package, + info *types.Info, +) string { + if index, ok := typeToIndex[typ.String()]; ok { + return index + } + + var result AnalyzedType + indexOfResult := strconv.Itoa(len(typeToIndex)) + typeToIndex[typ.String()] = indexOfResult + + switch t := typ.(type) { + case *types.Pointer: + indexOfElemType := toAnalyzedType(t.Elem(), analyzedTypes, typeToIndex, sourcePackage, currentPackage, info) + + result = AnalyzedPointerType{ + Name: "*", + ElementType: indexOfElemType, + } + case *types.Named: + name := t.Obj().Name() + + var pkg Package + if p := t.Obj().Pkg(); p != nil { + pkg.Name = p.Name() + pkg.Path = p.Path() + } + + isError := implementsError(t) + + indexOfUnderlyingType := toAnalyzedType(t.Underlying(), analyzedTypes, typeToIndex, sourcePackage, pkg, info) + + result = AnalyzedNamedType{ + Name: name, + SourcePackage: pkg, + ImplementsError: isError, + UnderlyingType: indexOfUnderlyingType, + } + case *types.Basic: + name := t.Name() + result = AnalyzedPrimitiveType{Name: name} + case *types.Struct: + println(t.String()) + fields := make([]AnalyzedField, 0, t.NumFields()) + for i := 0; i < t.NumFields(); i++ { + field := t.Field(i) + if currentPackage != sourcePackage && !field.Exported() { + continue + } + + fieldType := toAnalyzedType(field.Type(), analyzedTypes, typeToIndex, sourcePackage, currentPackage, info) + + fields = append(fields, AnalyzedField{field.Name(), fieldType, field.Exported()}) + } + + result = AnalyzedStructType{ + Name: "struct{}", + Fields: fields, + } + case *types.Array: + println(t.String()) + indexOfArrayElemType := toAnalyzedType(t.Elem(), analyzedTypes, typeToIndex, sourcePackage, currentPackage, info) + + length := t.Len() + + result = AnalyzedArrayType{ + Name: "[_]", + ElementType: indexOfArrayElemType, + Length: length, + } + case *types.Slice: + println(t.String()) + indexOfSliceElemType := toAnalyzedType(t.Elem(), analyzedTypes, typeToIndex, sourcePackage, currentPackage, info) + + result = AnalyzedSliceType{ + Name: "[]", + ElementType: indexOfSliceElemType, + } + case *types.Map: + println(t.String()) + indexOfKeyType := toAnalyzedType(t.Key(), analyzedTypes, typeToIndex, sourcePackage, currentPackage, info) + indexOfElemType := toAnalyzedType(t.Elem(), analyzedTypes, typeToIndex, sourcePackage, currentPackage, info) + + result = AnalyzedMapType{ + Name: "map", + KeyType: indexOfKeyType, + ElementType: indexOfElemType, + } + case *types.Chan: + println(t.String()) + indexOfElemType := toAnalyzedType(t.Elem(), analyzedTypes, typeToIndex, sourcePackage, currentPackage, info) + + chanDir, err := ChanDirToString(t.Dir()) + checkError(err) + + result = AnalyzedChanType{ + Name: "chan", + ElementType: indexOfElemType, + Direction: chanDir, + } + case *types.Interface: + implementations := make([]string, 0) + used := make(map[string]bool) + for _, typeAndValue := range info.Types { + switch typeAndValue.Type.(type) { + case *types.Struct, *types.Signature, *types.Tuple, *types.TypeParam, *types.Union: + continue + case *types.Basic: + b := typeAndValue.Type.(*types.Basic) + switch b.Kind() { + case types.UntypedBool, types.UntypedInt, types.UntypedRune, types.UntypedFloat, types.UntypedComplex, types.UntypedString, types.UntypedNil: + continue + } + } + if !types.IsInterface(typeAndValue.Type) && types.Implements(typeAndValue.Type, t) { + analyzedType := toAnalyzedType(typeAndValue.Type, analyzedTypes, typeToIndex, sourcePackage, currentPackage, info) + if used[analyzedType] { + continue + } + used[analyzedType] = true + implementations = append(implementations, analyzedType) + } + } + result = AnalyzedInterfaceType{ + Name: "interface{}", + Implementations: implementations, + } + default: + err := fmt.Errorf("unsupported type: %s", typ.Underlying()) + checkError(err) + } + analyzedTypes[indexOfResult] = result + return indexOfResult +} + +type ResultOfChecking int + +const ( + SupportedType = iota + Unknown + UnsupportedType +) + +func checkTypeIsSupported( + typ types.Type, + visited map[string]ResultOfChecking, + isResultType bool, + sourcePackage Package, + currentPackage Package, + depth int, +) ResultOfChecking { + if res, ok := visited[typ.String()]; ok { + return res + } + var result ResultOfChecking = Unknown + visited[typ.String()] = result + switch t := typ.(type) { + case *types.Pointer: + // no support for pointer to pointer and pointer to channel, + // support for pointer to primitive only if depth is 0 + switch t.Elem().Underlying().(type) { + case *types.Basic: + if depth == 0 { + result = SupportedType + } else { + result = UnsupportedType + } + case *types.Chan: + if depth == 0 && !isResultType { + result = SupportedType + } else { + result = UnsupportedType + } + case *types.Pointer: + result = UnsupportedType + default: + result = checkTypeIsSupported(t.Elem(), visited, isResultType, sourcePackage, currentPackage, depth+1) + } + case *types.Named: + var pkg Package + if p := t.Obj().Pkg(); p != nil { + pkg.Name = p.Name() + pkg.Path = p.Path() + } + result = checkTypeIsSupported(t.Underlying(), visited, isResultType, sourcePackage, pkg, depth+1) + case *types.Basic: + result = SupportedType + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + field := t.Field(i) + if currentPackage != sourcePackage && !field.Exported() { + continue + } + + if checkTypeIsSupported(field.Type(), visited, isResultType, sourcePackage, currentPackage, depth+1) == UnsupportedType { + visited[t.String()] = UnsupportedType + return UnsupportedType + } + } + result = SupportedType + case *types.Array: + result = checkTypeIsSupported(t.Elem(), visited, isResultType, sourcePackage, currentPackage, depth+1) + case *types.Slice: + result = checkTypeIsSupported(t.Elem(), visited, isResultType, sourcePackage, currentPackage, depth+1) + case *types.Map: + if checkTypeIsSupported(t.Key(), visited, isResultType, sourcePackage, currentPackage, depth+1) == UnsupportedType || + checkTypeIsSupported(t.Elem(), visited, isResultType, sourcePackage, currentPackage, depth+1) == UnsupportedType { + result = UnsupportedType + } else { + result = SupportedType + } + case *types.Chan: + if !isResultType && depth == 0 { + result = checkTypeIsSupported(t.Elem(), visited, isResultType, sourcePackage, currentPackage, depth+1) + } + case *types.Interface: + if isResultType { + if implementsError(t) && depth == 0 { + result = SupportedType + } else { + result = UnsupportedType + } + } else { + result = SupportedType + } + } + + if result == Unknown { + result = UnsupportedType + } + visited[typ.String()] = result + return visited[typ.String()] +} + +func checkIsSupported(signature *types.Signature, sourcePackage Package) bool { + if signature.TypeParams() != nil { // has type params + return false + } + if signature.Variadic() { // is variadic + return false + } + visited := make(map[string]ResultOfChecking, signature.Results().Len()+signature.Params().Len()) + if signature.Recv() != nil { + receiverType := signature.Recv().Type() + checkTypeIsSupported(receiverType, visited, false, sourcePackage, sourcePackage, 0) + if visited[receiverType.String()] == UnsupportedType { + return false + } + } + if results := signature.Results(); results != nil { + for i := 0; i < results.Len(); i++ { + resultType := results.At(i).Type().Underlying() + if _, ok := visited[resultType.String()]; ok { + continue + } + checkTypeIsSupported(resultType, visited, true, sourcePackage, sourcePackage, 0) + if visited[resultType.String()] == UnsupportedType { + return false + } + } + } + if parameters := signature.Params(); parameters != nil { + for i := 0; i < parameters.Len(); i++ { + paramType := parameters.At(i).Type().Underlying() + if _, ok := visited[paramType.String()]; ok { + continue + } + checkTypeIsSupported(paramType, visited, false, sourcePackage, sourcePackage, 0) + if visited[paramType.String()] == UnsupportedType { + return false + } + } + } + return true +} + +func extractConstants(info *types.Info, funcDecl *ast.FuncDecl) map[string][]string { + constantExtractor := ConstantExtractor{info: info, constants: map[string][]string{}} + ast.Walk(&constantExtractor, funcDecl) + return constantExtractor.constants +} + +func collectTargetAnalyzedFunctions( + info *types.Info, + targetFunctionNames []string, + targetMethodNames []string, + sourcePackage Package, +) ( + analyzedFunctions []AnalyzedFunction, + notSupportedFunctionNames []string, + notFoundFunctionNames []string, +) { + analyzedFunctions = []AnalyzedFunction{} + notSupportedFunctionNames = []string{} + notFoundFunctionNames = []string{} + + foundTargetFunctionNamesMap := map[string]bool{} + for _, functionName := range targetFunctionNames { + foundTargetFunctionNamesMap[functionName] = false + } + + foundTargetMethodNamesMap := map[string]bool{} + for _, functionName := range targetMethodNames { + foundTargetMethodNamesMap[functionName] = false + } + + var wg sync.WaitGroup + var mutex sync.Mutex + + for ident, obj := range info.Defs { + switch typedObj := obj.(type) { + case *types.Func: + wg.Add(1) + go func(ident *ast.Ident, typeObj *types.Func) { + defer wg.Done() + + analyzedFunction := AnalyzedFunction{ + Name: typedObj.Name(), + Parameters: []AnalyzedVariable{}, + ResultTypes: []AnalyzedVariable{}, + Constants: map[string][]string{}, + position: typedObj.Pos(), + } + + signature := typedObj.Type().(*types.Signature) + if signature.Recv() != nil { + mutex.Lock() + if isFound, ok := foundTargetMethodNamesMap[analyzedFunction.Name]; !ok || isFound { + mutex.Unlock() + return + } else { + foundTargetMethodNamesMap[analyzedFunction.Name] = true + mutex.Unlock() + } + } else { + mutex.Lock() + if isFound, ok := foundTargetFunctionNamesMap[analyzedFunction.Name]; !ok || isFound { + mutex.Unlock() + return + } else { + foundTargetFunctionNamesMap[analyzedFunction.Name] = true + mutex.Unlock() + } + } + + if !checkIsSupported(signature, sourcePackage) { + mutex.Lock() + notSupportedFunctionNames = append(notSupportedFunctionNames, analyzedFunction.Name) + mutex.Unlock() + return + } + analyzedTypes := make(map[string]AnalyzedType, signature.Params().Len()+signature.Results().Len()) + typeToIndex := make(map[string]string, signature.Params().Len()+signature.Results().Len()) + if receiver := signature.Recv(); receiver != nil { + receiverType := toAnalyzedType(receiver.Type(), analyzedTypes, typeToIndex, sourcePackage, sourcePackage, info) + analyzedFunction.Receiver = &AnalyzedVariable{ + Name: receiver.Name(), + Type: receiverType, + } + } + if parameters := signature.Params(); parameters != nil { + for i := 0; i < parameters.Len(); i++ { + parameter := parameters.At(i) + parameterType := toAnalyzedType(parameter.Type(), analyzedTypes, typeToIndex, sourcePackage, sourcePackage, info) + analyzedFunction.Parameters = append( + analyzedFunction.Parameters, + AnalyzedVariable{ + Name: parameter.Name(), + Type: parameterType, + }, + ) + } + } + if results := signature.Results(); results != nil { + for i := 0; i < results.Len(); i++ { + result := results.At(i) + resultType := toAnalyzedType(result.Type(), analyzedTypes, typeToIndex, sourcePackage, sourcePackage, info) + analyzedFunction.ResultTypes = append( + analyzedFunction.ResultTypes, + AnalyzedVariable{ + Name: result.Name(), + Type: resultType, + }, + ) + } + } + + if ident.Obj != nil { + funcDecl := ident.Obj.Decl.(*ast.FuncDecl) + analyzedFunction.Constants = extractConstants(info, funcDecl) + } + analyzedFunction.Types = analyzedTypes + + mutex.Lock() + analyzedFunctions = append(analyzedFunctions, analyzedFunction) + mutex.Unlock() + }(ident, typedObj) + } + } + + wg.Wait() + + for functionName, isFound := range foundTargetFunctionNamesMap { + if !isFound { + notFoundFunctionNames = append(notFoundFunctionNames, functionName) + } + } + for methodName, isFound := range foundTargetMethodNamesMap { + if !isFound { + notFoundFunctionNames = append(notFoundFunctionNames, methodName) + } + } + sort.Slice(analyzedFunctions, func(i, j int) bool { + return analyzedFunctions[i].position < analyzedFunctions[j].position + }) + sort.Strings(notSupportedFunctionNames) + sort.Strings(notFoundFunctionNames) + return +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/constant_extractor.go b/utbot-go/src/main/resources/go_source_code_analyzer/constant_extractor.go new file mode 100644 index 0000000000..b8f12389d5 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/constant_extractor.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "go/ast" + "go/constant" + "go/types" +) + +type ConstantExtractor struct { + info *types.Info + constants map[string][]string +} + +func (e *ConstantExtractor) Visit(node ast.Node) ast.Visitor { + if _, ok := node.(*ast.BasicLit); !ok { + return e + } + + expr, ok := node.(ast.Expr) + if !ok { + return e + } + + typeAndValue, ok := e.info.Types[expr] + if !ok { + return e + } + + var t = typeAndValue.Type + basicType, ok := t.(*types.Basic) + if !ok { + return e + } + + switch basicType.Kind() { + case types.Int, types.Int8, types.Int16, types.Int32, types.Int64: + e.constants[basicType.Name()] = append(e.constants[basicType.Name()], typeAndValue.Value.String()) + case types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64, types.Uintptr: + e.constants[basicType.Name()] = append(e.constants[basicType.Name()], typeAndValue.Value.String()) + case types.Float32: + if f32, ok := constant.Float32Val(typeAndValue.Value); ok { + e.constants[basicType.Name()] = append(e.constants[basicType.Name()], fmt.Sprintf("%v", f32)) + } + case types.Float64: + if f64, ok := constant.Float64Val(typeAndValue.Value); ok { + e.constants[basicType.Name()] = append(e.constants[basicType.Name()], fmt.Sprintf("%v", f64)) + } + case types.String: + e.constants[basicType.Name()] = append(e.constants[basicType.Name()], constant.StringVal(typeAndValue.Value)) + } + + return e +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/go.mod b/utbot-go/src/main/resources/go_source_code_analyzer/go.mod new file mode 100644 index 0000000000..cd5711b49d --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/go.mod @@ -0,0 +1,10 @@ +module go_source_code_analyzer + +go 1.18 + +require golang.org/x/tools v0.4.0 + +require ( + golang.org/x/mod v0.7.0 // indirect + golang.org/x/sys v0.3.0 // indirect +) diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/go.sum b/utbot-go/src/main/resources/go_source_code_analyzer/go.sum new file mode 100644 index 0000000000..a20f737720 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/go.sum @@ -0,0 +1,7 @@ +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/main.go b/utbot-go/src/main/resources/go_source_code_analyzer/main.go new file mode 100644 index 0000000000..efc8e166ed --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strconv" + "sync" + + "golang.org/x/tools/go/packages" +) + +func checkError(err error) { + if err != nil { + log.Fatal(err.Error()) + } +} + +func analyzeTarget(target AnalysisTarget) (*AnalysisResult, error) { + if len(target.TargetFunctionNames) == 0 && len(target.TargetMethodNames) == 0 { + return nil, fmt.Errorf("target must contain target functions or methods") + } + + pkgPath := filepath.Dir(target.AbsoluteFilePath) + + dir, _ := filepath.Split(target.AbsoluteFilePath) + cfg := packages.Config{ + Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedDeps | + packages.NeedImports | packages.NeedSyntax | packages.NeedFiles | packages.NeedCompiledGoFiles, + Dir: dir, + } + cfg.Env = os.Environ() + pkgs, err := packages.Load(&cfg, pkgPath) + checkError(err) + if len(pkgs) != 1 { + return nil, fmt.Errorf("cannot build multiple packages: %s", err) + } + if packages.PrintErrors(pkgs) > 0 { + return nil, fmt.Errorf("typechecking of %s failed", pkgPath) + } + + targetPackage := pkgs[0] + if len(targetPackage.CompiledGoFiles) != len(targetPackage.Syntax) { + return nil, fmt.Errorf("parsing returned nil for some files") + } + index := 0 + for ; index < len(targetPackage.CompiledGoFiles); index++ { + p1, err := filepath.Abs(targetPackage.CompiledGoFiles[index]) + checkError(err) + p2, err := filepath.Abs(target.AbsoluteFilePath) + checkError(err) + + if p1 == p2 { + break + } + } + if index == len(targetPackage.CompiledGoFiles) { + return nil, fmt.Errorf("target file not found in compiled go files") + } + + // collect required info about selected functions + analyzedFunctions, notSupportedFunctionsNames, notFoundFunctionsNames := + collectTargetAnalyzedFunctions( + targetPackage.TypesInfo, + target.TargetFunctionNames, + target.TargetMethodNames, + Package{ + Name: targetPackage.Name, + Path: targetPackage.PkgPath, + }, + ) + + return &AnalysisResult{ + AbsoluteFilePath: target.AbsoluteFilePath, + SourcePackage: Package{ + Name: targetPackage.Name, + Path: targetPackage.PkgPath, + }, + AnalyzedFunctions: analyzedFunctions, + NotSupportedFunctionNames: notSupportedFunctionsNames, + NotFoundFunctionNames: notFoundFunctionsNames, + }, nil +} + +func main() { + var targetsFilePath, resultsFilePath string + flag.StringVar(&targetsFilePath, "targets", "", "path to JSON file to read analysis targets from") + flag.StringVar(&resultsFilePath, "results", "", "path to JSON file to write analysis results to") + flag.Parse() + + // read and deserialize targets + targetsBytes, readErr := os.ReadFile(targetsFilePath) + checkError(readErr) + + var analysisTargets AnalysisTargets + fromJsonErr := json.Unmarshal(targetsBytes, &analysisTargets) + checkError(fromJsonErr) + + // parse each requested Go source file + var wg sync.WaitGroup + + results := make([]AnalysisResult, len(analysisTargets.Targets)) + for i, target := range analysisTargets.Targets { + wg.Add(1) + go func(i int, target AnalysisTarget) { + defer wg.Done() + + result, err := analyzeTarget(target) + checkError(err) + + results[i] = *result + }(i, target) + } + + wg.Wait() + + analysisResults := AnalysisResults{ + Results: results, + IntSize: strconv.IntSize, + } + + // serialize and write results + jsonBytes, toJsonErr := json.MarshalIndent(analysisResults, "", " ") + checkError(toJsonErr) + + writeErr := os.WriteFile(resultsFilePath, jsonBytes, os.ModePerm) + checkError(writeErr) +} diff --git a/utbot-gradle/docs/utbot-gradle.md b/utbot-gradle/docs/utbot-gradle.md index 745cc48468..4891d32c72 100644 --- a/utbot-gradle/docs/utbot-gradle.md +++ b/utbot-gradle/docs/utbot-gradle.md @@ -41,6 +41,7 @@ __Groovy:__ sarifReportsRelativeRoot = 'build/generated/sarif' markGeneratedTestsDirectoryAsTestSourcesRoot = true testPrivateMethods = false + projectType = 'purejvm' testFramework = 'junit5' mockFramework = 'mockito' generationTimeout = 60000L @@ -60,6 +61,7 @@ __Kotlin DSL:__ sarifReportsRelativeRoot.set("build/generated/sarif") markGeneratedTestsDirectoryAsTestSourcesRoot.set(true) testPrivateMethods.set(false) + projectType.set("purejvm") testFramework.set("junit5") mockFramework.set("mockito") generationTimeout.set(60000L) @@ -81,6 +83,7 @@ generateTestsAndSarifReport -PgeneratedTestsRelativeRoot='build/generated/test' -PsarifReportsRelativeRoot='build/generated/sarif' -PtestPrivateMethods='false' + -PtestProjectType=purejvm -PtestFramework=junit5 -PmockFramework=mockito -PgenerationTimeout=60000 @@ -119,6 +122,14 @@ generateTestsAndSarifReport - Generate tests for private methods or not. - By default, `false` is used. +- `projectType` – + - The type of project being analyzed. + - Can be one of: + - `'purejvm'` _(by default)_ + - `'spring'` + - `'python'` + - `'javascript'` + - `testFramework` – - The name of the test framework to be used. - Can be one of: 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 index 2f7bac2262..1e5081a6ba 100644 --- a/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/GenerateTestsAndSarifReportTask.kt +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/GenerateTestsAndSarifReportTask.kt @@ -1,137 +1,137 @@ -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.TestCaseGenerator -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 org.utbot.framework.plugin.services.JdkInfoDefaultProvider -import java.io.File -import java.net.URLClassLoader -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() { - // the user specifies the parameters using "-Pname=value" - sarifProperties.taskParameters = project.gradle.startParameter.projectProperties - 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 - - private val dependencyPaths by lazy { - val thisClassLoader = this::class.java.classLoader as? URLClassLoader - ?: return@lazy System.getProperty("java.class.path") - thisClassLoader.urLs.joinToString(File.pathSeparator) { it.path } - } - - /** - * 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)) { - val testCaseGenerator = - TestCaseGenerator( - listOf(sourceSet.workingDirectory), - sourceSet.runtimeClasspath, - dependencyPaths, - JdkInfoDefaultProvider().info - ) - sourceSet.targetClasses.forEach { targetClass -> - generateForClass(sourceSet, targetClass, testCaseGenerator) - } - } - } - } - - /** - * Generates tests and a SARIF report for the class [targetClass]. - */ - private fun generateForClass( - sourceSet: SourceSetWrapper, - targetClass: TargetClassWrapper, - testCaseGenerator: TestCaseGenerator, - ) { - logger.debug().bracket("Generating tests for the $targetClass") { - val sourceFindingStrategy = SourceFindingStrategyGradle(sourceSet, targetClass.testsCodeFile.path) - GenerateTestsAndSarifReportFacade(sarifProperties, sourceFindingStrategy, testCaseGenerator) - .generateForClass(targetClass, sourceSet.workingDirectory) - } - } - - /** - * 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() - } -} +package org.utbot.gradle.plugin + +import mu.KLogger +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.utbot.common.measureTime +import org.utbot.common.debug +import org.utbot.framework.plugin.api.TestCaseGenerator +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 org.utbot.framework.plugin.services.JdkInfoDefaultProvider +import java.io.File +import java.net.URLClassLoader +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() { + // the user specifies the parameters using "-Pname=value" + sarifProperties.taskParameters = project.gradle.startParameter.projectProperties + 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 + + private val dependencyPaths by lazy { + val thisClassLoader = this::class.java.classLoader as? URLClassLoader + ?: return@lazy System.getProperty("java.class.path") + thisClassLoader.urLs.joinToString(File.pathSeparator) { it.path } + } + + /** + * 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().measureTime({ "Generating tests for the '${sourceSet.sourceSet.name}' source set" }) { + withUtContext(UtContext(sourceSet.classLoader)) { + val testCaseGenerator = + TestCaseGenerator( + listOf(sourceSet.workingDirectory), + sourceSet.runtimeClasspath, + dependencyPaths, + JdkInfoDefaultProvider().info + ) + sourceSet.targetClasses.forEach { targetClass -> + generateForClass(sourceSet, targetClass, testCaseGenerator) + } + } + } + } + + /** + * Generates tests and a SARIF report for the class [targetClass]. + */ + private fun generateForClass( + sourceSet: SourceSetWrapper, + targetClass: TargetClassWrapper, + testCaseGenerator: TestCaseGenerator, + ) { + logger.debug().measureTime({ "Generating tests for the $targetClass" }) { + val sourceFindingStrategy = SourceFindingStrategyGradle(sourceSet, targetClass.testsCodeFile.path) + GenerateTestsAndSarifReportFacade(sarifProperties, sourceFindingStrategy, testCaseGenerator) + .generateForClass(targetClass, sourceSet.workingDirectory) + } + } + + /** + * 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/gradle/plugin/extension/SarifGradleExtension.kt b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtension.kt index 7737836899..5fda696742 100644 --- a/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtension.kt +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtension.kt @@ -49,6 +49,12 @@ abstract class SarifGradleExtension { @get:Input abstract val testPrivateMethods: Property + /** + * Can be one of: 'purejvm', 'spring', 'python', 'javascript`. + */ + @get:Input + abstract val projectType: Property + /** * Can be one of: 'junit4', 'junit5', 'testng'. */ 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 index 0464a51b28..de56527fb6 100644 --- 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 @@ -2,9 +2,10 @@ 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.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MockFramework @@ -57,6 +58,11 @@ class SarifGradleExtensionProvider( ?: extension.testPrivateMethods.orNull ?: false + override val projectType: ProjectType + get() = (taskParameters["projectType"] ?: extension.projectType.orNull) + ?.let(::projectTypeParse) + ?: ProjectType.PureJvm + override val testFramework: TestFramework get() = (taskParameters["testFramework"] ?: extension.testFramework.orNull) ?.let(::testFrameworkParse) diff --git a/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProviderTest.kt b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProviderTest.kt index 8db032a787..9432b519fe 100644 --- a/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProviderTest.kt +++ b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProviderTest.kt @@ -8,7 +8,16 @@ 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.* +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.MockitoStaticMocking +import org.utbot.framework.codegen.domain.NoStaticMocking +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.ProjectType.* +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.TestNg import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MockFramework @@ -248,6 +257,69 @@ class SarifGradleExtensionProviderTest { } } + @Nested + @DisplayName("projectType") + inner class ProjectTypeTest { + @Test + fun `should be ProjectType defaultItem by default`() { + setProjectTypeInExtension(null) + assertEquals(PureJvm, extensionProvider.projectType) + } + + @Test + fun `should be equal to PureJvm`() { + setProjectTypeInExtension("purejvm") + assertEquals(PureJvm, extensionProvider.projectType) + } + + @Test + fun `should be equal to Spring`() { + setProjectTypeInExtension("spring") + assertEquals(Spring, extensionProvider.projectType) + } + + @Test + fun `should be equal to Python`() { + setProjectTypeInExtension("python") + assertEquals(Python, extensionProvider.projectType) + } + + @Test + fun `should be equal to JavaScript`() { + setProjectTypeInExtension("javascript") + assertEquals(JavaScript, extensionProvider.projectType) + } + + @Test + fun `should fail on unknown project type`() { + setProjectTypeInExtension("unknown") + assertThrows { + extensionProvider.projectType + } + } + + @Test + fun `should be provided from the task parameters`() { + setProjectTypeInTaskParameters("spring") + assertEquals(Spring, extensionProvider.projectType) + } + + @Test + fun `should be provided from the task parameters, not from the extension`() { + setProjectTypeInTaskParameters("python") + setProjectTypeInExtension("javascript") + assertEquals(Python, extensionProvider.projectType) + } + + private fun setProjectTypeInExtension(value: String?) { + Mockito.`when`(extensionMock.projectType).thenReturn(createStringProperty(value)) + } + + private fun setProjectTypeInTaskParameters(value: String) { + extensionProvider.taskParameters = mapOf("projectType" to value) + } + } + @Nested @DisplayName("testFramework") inner class TestFrameworkTest { diff --git a/utbot-instrumentation-tests/build.gradle b/utbot-instrumentation-tests/build.gradle index 83cc69e5a8..99cc4d567e 100644 --- a/utbot-instrumentation-tests/build.gradle +++ b/utbot-instrumentation-tests/build.gradle @@ -1,3 +1,20 @@ +compileKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + configurations { fetchInstrumentationJar } @@ -9,13 +26,13 @@ dependencies { testImplementation configurations.fetchInstrumentationJar testImplementation project(':utbot-sample') testImplementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion - implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: '2022.3.1' - implementation group: 'com.jetbrains.rd', name: 'rd-core', version: '2022.3.1' + implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: rdVersion + implementation group: 'com.jetbrains.rd', name: 'rd-core', version: rdVersion } processResources { - // We will extract this jar in `ChildProcessRunner` class. + // We will extract this jar in `InstrumentedProcess` class. from(configurations.fetchInstrumentationJar) { - into "instrumentation-lib" + into "lib" } } \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/java/org/utbot/examples/samples/transformation/StringMethodsCalls.java b/utbot-instrumentation-tests/src/test/java/org/utbot/examples/samples/transformation/StringMethodsCalls.java new file mode 100644 index 0000000000..d886d02cfa --- /dev/null +++ b/utbot-instrumentation-tests/src/test/java/org/utbot/examples/samples/transformation/StringMethodsCalls.java @@ -0,0 +1,45 @@ +package org.utbot.examples.samples.transformation; + +public class StringMethodsCalls { + public static boolean equalsWithEmptyString(String strToCompare) { + if (strToCompare.equals("")) { + return true; + } + return false; + } + + public static boolean equalsWithNotEmptyString(String strToCompare) { + if (strToCompare.equals("abc")) { + return true; + } + return false; + } + + public static boolean startsWithWithEmptyString(String strToCompare) { + if (strToCompare.startsWith("")) { + return true; + } + return false; + } + + public static boolean startsWithWithNotEmptyString(String strToCompare) { + if (strToCompare.startsWith("abc")) { + return true; + } + return false; + } + + public static boolean endsWithWithEmptyString(String strToCompare) { + if (strToCompare.endsWith("")) { + return true; + } + return false; + } + + public static boolean endsWithWithNotEmptyString(String strToCompare) { + if (strToCompare.endsWith("abc")) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestBytecodeTransformation.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestBytecodeTransformation.kt new file mode 100644 index 0000000000..997a55c58b --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestBytecodeTransformation.kt @@ -0,0 +1,130 @@ +package org.utbot.examples + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.utbot.examples.samples.transformation.StringMethodsCalls +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.transformation.BytecodeTransformation +import org.utbot.instrumentation.withInstrumentation + +class TestBytecodeTransformation { + lateinit var utContext: AutoCloseable + + @Test + fun testStringEqualsWithEmptyStringCall() { + withInstrumentation( + BytecodeTransformation.Factory, + StringMethodsCalls::class.java.protectionDomain.codeSource.location.path + ) { executor -> + val res1 = executor.execute(StringMethodsCalls::equalsWithEmptyString, arrayOf("")) + assertTrue(res1.getOrNull() as Boolean) + + val res2 = executor.execute(StringMethodsCalls::equalsWithEmptyString, arrayOf("abc")) + assertFalse(res2.getOrNull() as Boolean) + + val res3 = executor.execute(StringMethodsCalls::equalsWithEmptyString, arrayOf(null)) + assertTrue(res3.exceptionOrNull() is NullPointerException) + } + } + + @Test + fun testStringEqualsWithNotEmptyStringCall() { + withInstrumentation( + BytecodeTransformation.Factory, + StringMethodsCalls::class.java.protectionDomain.codeSource.location.path + ) { executor -> + val res1 = executor.execute(StringMethodsCalls::equalsWithNotEmptyString, arrayOf("")) + assertFalse(res1.getOrNull() as Boolean) + + val res2 = executor.execute(StringMethodsCalls::equalsWithNotEmptyString, arrayOf("abc")) + assertTrue(res2.getOrNull() as Boolean) + + val res3 = executor.execute(StringMethodsCalls::equalsWithNotEmptyString, arrayOf("abcd")) + assertFalse(res3.getOrNull() as Boolean) + + val res4 = executor.execute(StringMethodsCalls::equalsWithNotEmptyString, arrayOf(null)) + assertTrue(res4.exceptionOrNull() is NullPointerException) + } + } + + @Test + fun testStringStartsWithWithEmptyStringCall() { + withInstrumentation( + BytecodeTransformation.Factory, + StringMethodsCalls::class.java.protectionDomain.codeSource.location.path + ) { executor -> + val res1 = executor.execute(StringMethodsCalls::startsWithWithEmptyString, arrayOf("")) + assertTrue(res1.getOrNull() as Boolean) + + val res2 = executor.execute(StringMethodsCalls::startsWithWithEmptyString, arrayOf("abc")) + assertTrue(res2.getOrNull() as Boolean) + + val res3 = executor.execute(StringMethodsCalls::startsWithWithEmptyString, arrayOf(null)) + assertTrue(res3.exceptionOrNull() is NullPointerException) + } + } + + @Test + fun testStringStartsWithWithNotEmptyStringCall() { + withInstrumentation( + BytecodeTransformation.Factory, + StringMethodsCalls::class.java.protectionDomain.codeSource.location.path + ) { executor -> + val res1 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf("")) + assertFalse(res1.getOrNull() as Boolean) + + val res2 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf("abc")) + assertTrue(res2.getOrNull() as Boolean) + + val res3 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf("abcd")) + assertTrue(res3.getOrNull() as Boolean) + + val res4 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf("aabc")) + assertFalse(res4.getOrNull() as Boolean) + + val res5 = executor.execute(StringMethodsCalls::startsWithWithNotEmptyString, arrayOf(null)) + assertTrue(res5.exceptionOrNull() is NullPointerException) + } + } + + @Test + fun testStringEndsWithWithEmptyString() { + withInstrumentation( + BytecodeTransformation.Factory, + StringMethodsCalls::class.java.protectionDomain.codeSource.location.path + ) { executor -> + val res1 = executor.execute(StringMethodsCalls::endsWithWithEmptyString, arrayOf("")) + assertTrue(res1.getOrNull() as Boolean) + + val res2 = executor.execute(StringMethodsCalls::endsWithWithEmptyString, arrayOf("abc")) + assertTrue(res2.getOrNull() as Boolean) + + val res3 = executor.execute(StringMethodsCalls::endsWithWithEmptyString, arrayOf(null)) + assertTrue(res3.exceptionOrNull() is NullPointerException) + } + } + + @Test + fun testStringEndsWithWithNotEmptyString() { + withInstrumentation( + BytecodeTransformation.Factory, + StringMethodsCalls::class.java.protectionDomain.codeSource.location.path + ) { executor -> + val res1 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf("")) + assertFalse(res1.getOrNull() as Boolean) + + val res2 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf("abc")) + assertTrue(res2.getOrNull() as Boolean) + + val res3 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf("aabc")) + assertTrue(res3.getOrNull() as Boolean) + + val res4 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf("abcd")) + assertFalse(res4.getOrNull() as Boolean) + + val res5 = executor.execute(StringMethodsCalls::endsWithWithNotEmptyString, arrayOf(null)) + assertTrue(res5.exceptionOrNull() is NullPointerException) + } + } +} \ No newline at end of file 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 index 93801a9621..fea5acdb17 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestConstructors.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestConstructors.kt @@ -28,7 +28,7 @@ class TestConstructors { @Test fun testDefaultConstructor() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, CLASSPATH ).use { executor -> val constructors = ClassWithMultipleConstructors::class.constructors @@ -43,7 +43,7 @@ class TestConstructors { @Test fun testIntConstructors() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, CLASSPATH ).use { executor -> val constructors = ClassWithMultipleConstructors::class.constructors @@ -65,7 +65,7 @@ class TestConstructors { @Test fun testStringConstructors() { withInstrumentation( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, CLASSPATH ) { executor -> val constructors = ClassWithMultipleConstructors::class.constructors @@ -86,7 +86,7 @@ class TestConstructors { @Test fun testCoverageConstructor() { withInstrumentation( - CoverageInstrumentation, + CoverageInstrumentation.Factory, CLASSPATH ) { executor -> val constructors = ClassWithMultipleConstructors::class.constructors @@ -106,7 +106,7 @@ class TestConstructors { @Test fun testExecutionTraceConstructor() { withInstrumentation( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, CLASSPATH ) { executor -> val constructors = ClassWithMultipleConstructors::class.constructors 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 index 40599120de..565ca75f12 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestCoverageInstrumentation.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestCoverageInstrumentation.kt @@ -10,7 +10,7 @@ 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.InstrumentedProcessError import org.utbot.instrumentation.util.StaticEnvironment import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf @@ -24,7 +24,7 @@ class TestCoverageInstrumentation { @Test fun testCatchTargetException() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -41,7 +41,7 @@ class TestCoverageInstrumentation { @Test fun testIfBranches() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -63,11 +63,11 @@ class TestCoverageInstrumentation { @Test fun testWrongArgumentsException() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() - val exc = assertThrows { + val exc = assertThrows { it.execute( ExampleClass::bar, arrayOf(testObject, 1, 2, 3) @@ -86,11 +86,11 @@ class TestCoverageInstrumentation { @Test fun testMultipleRunsInsideCoverage() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() - val exc = assertThrows { + val exc = assertThrows { it.execute( ExampleClass::bar, arrayOf(testObject, 1, 2, 3) @@ -121,7 +121,7 @@ class TestCoverageInstrumentation { @Test fun testSameResult() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -143,7 +143,7 @@ class TestCoverageInstrumentation { @Test fun testResult() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -160,7 +160,7 @@ class TestCoverageInstrumentation { @Test fun testEmptyMethod() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -176,7 +176,7 @@ class TestCoverageInstrumentation { @Test fun testTernaryOperator() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, StaticSubstitutionExamples::class.java.protectionDomain.codeSource.location.path ).use { val testObject = StaticSubstitutionExamples() 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 index f1201f832a..866d243bc5 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestGetSourceFileName.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestGetSourceFileName.kt @@ -27,19 +27,19 @@ class TestGetSourceFileName { @Test fun testThis() { - assertEquals("TestGetSourceFileName.kt", Instrumenter.computeSourceFileName(TestGetSourceFileName::class.java)) + assertEquals("TestGetSourceFileName.kt", Instrumenter.adapter.computeSourceFileName(TestGetSourceFileName::class.java)) } @Test fun testJavaExample1() { - assertEquals("ExampleClass.java", Instrumenter.computeSourceFileName(ExampleClass::class.java)) + assertEquals("ExampleClass.java", Instrumenter.adapter.computeSourceFileName(ExampleClass::class.java)) } @Test fun testJavaExample2() { assertEquals( "ClassWithInnerClasses.java", - Instrumenter.computeSourceFileName(ClassWithInnerClasses::class.java) + Instrumenter.adapter.computeSourceFileName(ClassWithInnerClasses::class.java) ) } @@ -47,7 +47,7 @@ class TestGetSourceFileName { fun testInnerClass() { assertEquals( "ClassWithInnerClasses.java", - Instrumenter.computeSourceFileName(ClassWithInnerClasses.InnerStaticClass::class.java) + Instrumenter.adapter.computeSourceFileName(ClassWithInnerClasses.InnerStaticClass::class.java) ) } @@ -55,12 +55,12 @@ class TestGetSourceFileName { fun testSameNameButDifferentPackages() { assertEquals( true, - Instrumenter.computeSourceFileByClass(org.utbot.examples.samples.root.MyClass::class.java)?.toPath() + Instrumenter.adapter.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) + Instrumenter.adapter.computeSourceFileByClass(org.utbot.examples.samples.root.child.MyClass::class.java) ?.toPath()?.endsWith(Paths.get("root", "child", "MyClass.java")) ) } @@ -69,7 +69,7 @@ class TestGetSourceFileName { fun testEmptyPackage() { assertEquals( true, - Instrumenter.computeSourceFileByClass(ClassWithoutPackage::class.java)?.toPath() + Instrumenter.adapter.computeSourceFileByClass(ClassWithoutPackage::class.java)?.toPath() ?.endsWith("java/ClassWithoutPackage.java") ) } @@ -78,7 +78,7 @@ class TestGetSourceFileName { fun testPackageDoesNotMatchDir() { assertEquals( true, - Instrumenter.computeSourceFileByClass(ClassWithWrongPackage::class.java)?.toPath() + Instrumenter.adapter.computeSourceFileByClass(ClassWithWrongPackage::class.java)?.toPath() ?.endsWith("org/utbot/examples/samples/ClassWithWrongPackage.kt") ) } @@ -87,7 +87,7 @@ class TestGetSourceFileName { fun testSearchDir() { assertEquals( null, - Instrumenter.computeSourceFileByClass( + Instrumenter.adapter.computeSourceFileByClass( org.utbot.examples.samples.root.MyClass::class.java, Paths.get("src/test/kotlin") )?.name @@ -95,7 +95,7 @@ class TestGetSourceFileName { assertEquals( "MyClass.java", - Instrumenter.computeSourceFileByClass( + Instrumenter.adapter.computeSourceFileByClass( org.utbot.examples.samples.root.MyClass::class.java, Paths.get("src/test") )?.name 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 index 7e4bc49eb4..a585b2f45f 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeInstrumentation.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeInstrumentation.kt @@ -7,7 +7,7 @@ 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 org.utbot.instrumentation.util.InstrumentedProcessError import kotlin.reflect.full.declaredMembers import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf @@ -21,7 +21,7 @@ class TestInvokeInstrumentation { @Test fun testCatchTargetException() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { @@ -36,11 +36,11 @@ class TestInvokeInstrumentation { @Test fun testWrongArgumentsException() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() - val exc = assertThrows { + val exc = assertThrows { it.execute( ExampleClass::bar, arrayOf(testObject, 1, 2, 3) @@ -57,7 +57,7 @@ class TestInvokeInstrumentation { @Test fun testSameResult() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -73,7 +73,7 @@ class TestInvokeInstrumentation { @Test fun testEmptyMethod() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -87,7 +87,7 @@ class TestInvokeInstrumentation { @Test fun testStaticMethodCall() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, StaticExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val res1 = it.execute(StaticExampleClass::inc, arrayOf()) @@ -105,7 +105,7 @@ class TestInvokeInstrumentation { @Test fun testNullableMethod() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, StaticExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val res1 = it.execute( @@ -136,7 +136,7 @@ class TestInvokeInstrumentation { @Test fun testDifferentSignaturesButSameMethodNames() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, ClassWithSameMethodNames::class.java.protectionDomain.codeSource.location.path ).use { val clazz = ClassWithSameMethodNames::class 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 index b1865499e0..c06143e38f 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeWithStaticsInstrumentation.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeWithStaticsInstrumentation.kt @@ -35,7 +35,7 @@ class TestInvokeWithStaticsInstrumentation { @Test fun testIfBranches() { ConcreteExecutor( - InvokeWithStaticsInstrumentation(), + InvokeWithStaticsInstrumentation.Factory, CLASSPATH ).use { val res = it.execute(StaticExampleClass::inc, arrayOf(), null) @@ -52,7 +52,7 @@ class TestInvokeWithStaticsInstrumentation { @Test fun testHiddenClass1() { ConcreteExecutor( - InvokeWithStaticsInstrumentation(), + InvokeWithStaticsInstrumentation.Factory, CLASSPATH ).use { val res = it.execute(TestedClass::slomayInts, arrayOf(), null) @@ -71,7 +71,7 @@ class TestInvokeWithStaticsInstrumentation { @Test fun testHiddenClassRepeatCall() { ConcreteExecutor( - InvokeWithStaticsInstrumentation(), + InvokeWithStaticsInstrumentation.Factory, CLASSPATH ).use { val se = StaticEnvironment( @@ -89,7 +89,7 @@ class TestInvokeWithStaticsInstrumentation { @Test fun testReferenceEquality() { ConcreteExecutor( - InvokeWithStaticsInstrumentation(), + InvokeWithStaticsInstrumentation.Factory, CLASSPATH ).use { 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 index dca3af7c1c..96b3047f6e 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestIsolated.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestIsolated.kt @@ -5,7 +5,7 @@ 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.InstrumentedProcessError import org.utbot.instrumentation.util.Isolated import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf @@ -21,7 +21,7 @@ class TestIsolated { fun testCatchTargetException() { val javaClass = ExampleClass::class.java ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, javaClass.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -37,7 +37,7 @@ class TestIsolated { @Test fun testWrongArgumentsException() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -48,7 +48,7 @@ class TestIsolated { } - val exc = assertThrows { + val exc = assertThrows { isolatedFunction(testObject, 1, 2, 3) } @@ -63,7 +63,7 @@ class TestIsolated { @Test fun testSameResult() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -81,7 +81,7 @@ class TestIsolated { @Test fun testEmptyMethod() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, ExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val testObject = ExampleClass() @@ -97,7 +97,7 @@ class TestIsolated { @Test fun testStaticMethodCall() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, StaticExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val isolatedFunctionInc = Isolated(StaticExampleClass::inc, it) @@ -116,7 +116,7 @@ class TestIsolated { @Test fun testNullableMethod() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, StaticExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val isolatedFunction = Isolated(StaticExampleClass::canBeNull, it) 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 index 6ad3bca179..0a75292636 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestStaticMethods.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestStaticMethods.kt @@ -17,7 +17,7 @@ class TestStaticMethods { @Test fun testStaticMethodCall() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, StaticExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val res1 = it.execute(StaticExampleClass::inc, arrayOf()) @@ -44,7 +44,7 @@ class TestStaticMethods { @Test fun testNullableMethod() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, StaticExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val res1 = it.execute( @@ -75,7 +75,7 @@ class TestStaticMethods { @Test fun testNullableMethodWithoutAnnotations() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, StaticExampleClass::class.java.protectionDomain.codeSource.location.path ).use { val res1 = it.execute( 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 index f49c010b81..ae00e0f7a8 100644 --- a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestWithInstrumentation.kt +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestWithInstrumentation.kt @@ -20,7 +20,7 @@ class TestWithInstrumentation { @Test fun testStaticMethodCall() { withInstrumentation( - CoverageInstrumentation, + CoverageInstrumentation.Factory, StaticExampleClass::class.java.protectionDomain.codeSource.location.path ) { executor -> val res1 = executor.execute(StaticExampleClass::inc, arrayOf()) @@ -47,7 +47,7 @@ class TestWithInstrumentation { @Test fun testDifferentSignaturesButSameMethodNames() { withInstrumentation( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, ClassWithSameMethodNames::class.java.protectionDomain.codeSource.location.path ) { executor -> val clazz = ClassWithSameMethodNames::class @@ -70,7 +70,7 @@ class TestWithInstrumentation { @Test fun testInnerClasses() { withInstrumentation( - CoverageInstrumentation, + CoverageInstrumentation.Factory, ClassWithInnerClasses::class.java.protectionDomain.codeSource.location.path ) { executor -> val innerClazz = ClassWithInnerClasses.InnerClass::class.java 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 index 41ec9da126..d4e9ba2028 100644 --- 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 @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Assertions.assertEquals fun getBasicCoverageTime(count: Int): Double { var time: Long ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, Repeater::class.java.protectionDomain.codeSource.location.path ).use { executor -> val dc0 = Repeater(", ") @@ -51,7 +51,7 @@ fun getNativeCallTime(count: Int): Double { fun getJustResultTime(count: Int): Double { var time: Long ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, Repeater::class.java.protectionDomain.codeSource.location.path ).use { val dc0 = Repeater(", ") 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 index 4c370553de..07d788e4ce 100644 --- 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 @@ -13,7 +13,7 @@ import kotlin.system.measureNanoTime fun getBasicCoverageTime_fib(count: Int): Double { var time: Long ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, Fibonacci::class.java.protectionDomain.codeSource.location.path ).use { val fib = Isolated(Fibonacci::calc, it) @@ -47,7 +47,7 @@ fun getNativeCallTime_fib(count: Int): Double { fun getJustResultTime_fib(count: Int): Double { var time: Long ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, Fibonacci::class.java.protectionDomain.codeSource.location.path ).use { val fib = Isolated(Fibonacci::calc, it) 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 index 81d8240500..f189907c5d 100644 --- 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 @@ -18,7 +18,7 @@ class TestBenchmarkClasses { @Disabled("Ask Sergey to check") fun testRepeater() { ConcreteExecutor( - CoverageInstrumentation, + CoverageInstrumentation.Factory, Repeater::class.java.protectionDomain.codeSource.location.path ).use { val dc0 = Repeater(", ") @@ -36,7 +36,7 @@ class TestBenchmarkClasses { @Test fun testFibonacci() { ConcreteExecutor( - InvokeInstrumentation(), + InvokeInstrumentation.Factory, Fibonacci::class.java.protectionDomain.codeSource.location.path ).use { val 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 index 560a314633..c5ecefeded 100644 --- 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 @@ -25,7 +25,7 @@ class TestMixedExTrace { @Test fun testMixedDoesNotThrow() { ConcreteExecutor( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, CLASSPATH ).use { val A = Isolated(ClassMixedWithNotInstrumented_Instr::a, it) 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 index e7dab32d5f..f8b7224de0 100644 --- 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 @@ -34,7 +34,7 @@ class TestSimpleExTrace { @Test fun testClassSimple() { ConcreteExecutor( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, CLASSPATH ).use { val alwaysThrows = Isolated(ClassSimple::alwaysThrows, it) @@ -89,7 +89,7 @@ class TestSimpleExTrace { @Test fun testClasSimpleCatch() { ConcreteExecutor( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, CLASSPATH ).use { val A = Isolated(ClassSimpleCatch::A, it) @@ -159,7 +159,7 @@ class TestSimpleExTrace { @Test fun testClassSimpleRecursive() { ConcreteExecutor( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, CLASSPATH ).use { val A = Isolated(ClassSimpleRecursive::A, it) @@ -228,7 +228,7 @@ class TestSimpleExTrace { @Test fun testClassBinaryRecursionWithTrickyThrow() { ConcreteExecutor( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, CLASSPATH ).use { val A = Isolated(ClassBinaryRecursionWithTrickyThrow::A, it) @@ -347,7 +347,7 @@ class TestSimpleExTrace { @Test fun testClassBinaryRecursionWithThrow() { ConcreteExecutor( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, CLASSPATH ).use { val A = Isolated(ClassBinaryRecursionWithThrow::A, it) @@ -436,7 +436,7 @@ class TestSimpleExTrace { @Test fun testClassSimpleNPE() { ConcreteExecutor( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, CLASSPATH ).use { val A = Isolated(ClassSimpleNPE::A, it) 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 index f4aef6ead6..22ade07a8c 100644 --- 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 @@ -31,7 +31,7 @@ class StaticsUsageDetectionTest { @Test fun testStaticsUsageOneUsage() { withInstrumentation( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, ObjectWithStaticFieldsExample::class.java.protectionDomain.codeSource.location.path ) { val instance = ObjectWithStaticFieldsExample() @@ -46,7 +46,7 @@ class StaticsUsageDetectionTest { @Test fun testStaticsUsageZeroUsages() { withInstrumentation( - ExecutionTraceInstrumentation(), + ExecutionTraceInstrumentation.Factory, ObjectWithStaticFieldsExample::class.java.protectionDomain.codeSource.location.path ) { val instance = ObjectWithStaticFieldsExample() 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 index de8051a775..a289e1d5fa 100644 --- 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 @@ -123,7 +123,7 @@ private fun methodCoverageWithJaCoCo(kClass: KClass<*>, method: KCallable<*>, ex private fun methodCoverage(kClass: KClass<*>, method: KCallable<*>, executions: List): Pair { return withInstrumentation( - CoverageInstrumentation, + CoverageInstrumentation.Factory, kClass.java.protectionDomain.codeSource.location.path ) { executor -> for (execution in executions) { diff --git a/utbot-instrumentation/build.gradle b/utbot-instrumentation/build.gradle deleted file mode 100644 index e9b76a34bb..0000000000 --- a/utbot-instrumentation/build.gradle +++ /dev/null @@ -1,47 +0,0 @@ -dependencies { - implementation project(':utbot-framework-api') - implementation project(':utbot-rd') - - implementation group: 'org.ow2.asm', name: 'asm', version: asmVersion - implementation group: 'org.ow2.asm', name: 'asm-commons', version: asmVersion - implementation group: 'com.esotericsoftware.kryo', name: 'kryo5', version: kryoVersion - // this is necessary for serialization of some collections - implementation group: 'de.javakaffee', name: 'kryo-serializers', version: kryoSerializersVersion - implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion - - implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: '2022.3.1' - implementation group: 'com.jetbrains.rd', name: 'rd-core', version: '2022.3.1' - implementation group: 'net.java.dev.jna', name: 'jna-platform', version: '5.5.0' - - - // 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' -} - -jar { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - - manifest { - attributes ( - 'Main-Class': 'org.utbot.instrumentation.process.ChildProcessKt', - 'Premain-Class': 'org.utbot.instrumentation.agent.Agent', - ) - } - - // we need only classes from implementation and utbot to execute child process - dependsOn configurations.compileClasspath - from { - configurations.compileClasspath - .findAll { it.isDirectory() || it.name.endsWith('jar') } - .collect { it.isDirectory() ? it : zipTree(it) } - } -} - -configurations { - instrumentationArchive -} - -artifacts { - instrumentationArchive jar -} \ No newline at end of file diff --git a/utbot-instrumentation/build.gradle.kts b/utbot-instrumentation/build.gradle.kts new file mode 100644 index 0000000000..ca8ea1f469 --- /dev/null +++ b/utbot-instrumentation/build.gradle.kts @@ -0,0 +1,99 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer + +val asmVersion: String by rootProject +val kryoVersion: String by rootProject +val kryoSerializersVersion: String by rootProject +val kotlinLoggingVersion: String by rootProject +val rdVersion: String by rootProject +val mockitoVersion: String by rootProject +val mockitoInlineVersion: String by rootProject + +plugins { + id("com.github.johnrengelman.shadow") version "7.1.2" + id("java") + application +} + +tasks.compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +tasks.compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +application { + mainClass.set("org.utbot.instrumentation.process.InstrumentedProcessMainKt") +} + +val fetchSpringCommonsJar: Configuration by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false +} + +dependencies { + implementation(project(":utbot-framework-api")) + implementation(project(":utbot-rd")) + + implementation("org.ow2.asm:asm:$asmVersion") + implementation("org.ow2.asm:asm-commons:$asmVersion") + implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") + + implementation("com.jetbrains.rd:rd-framework:$rdVersion") + implementation("com.jetbrains.rd:rd-core:$rdVersion") + implementation("net.java.dev.jna:jna-platform:5.5.0") + + // TODO: this is necessary for inline classes mocking in UtExecutionInstrumentation + implementation("org.mockito:mockito-core:$mockitoVersion") + implementation("org.mockito:mockito-inline:$mockitoInlineVersion") + + implementation(project(":utbot-spring-commons-api")) + fetchSpringCommonsJar(project(":utbot-spring-commons", configuration = "springCommonsJar")) +} + +/** + * Shadow plugin unpacks the nested `utbot-spring-commons-shadow.jar`. + * But we need it to be packed. Workaround: double-nest the jar. + */ +val shadowJarUnpackWorkaround by tasks.register("shadowBugWorkaround") { + destinationDirectory.set(layout.buildDirectory.dir("build/shadow-bug-workaround")) + from(fetchSpringCommonsJar) { + into("lib") + } +} + +tasks.shadowJar { + dependsOn(shadowJarUnpackWorkaround) + + from(shadowJarUnpackWorkaround) { + into("lib") + } + + manifest { + attributes( + "Main-Class" to "org.utbot.instrumentation.process.InstrumentedProcessMainKt", + "Premain-Class" to "org.utbot.instrumentation.agent.Agent", + ) + } + + transform(Log4j2PluginsCacheFileTransformer::class.java) + archiveFileName.set("utbot-instrumentation-shadow.jar") +} + +val instrumentationArchive: Configuration by configurations.creating { + isCanBeResolved = false + isCanBeConsumed = true +} + +artifacts { + add(instrumentationArchive.name, tasks.shadowJar) +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt index 64a963fd35..5f80edec97 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt @@ -1,43 +1,44 @@ package org.utbot.instrumentation -import com.jetbrains.rd.util.Logger -import com.jetbrains.rd.util.lifetime.Lifetime import com.jetbrains.rd.util.lifetime.LifetimeDefinition import com.jetbrains.rd.util.lifetime.isAlive import com.jetbrains.rd.util.lifetime.throwIfNotAlive import java.io.Closeable import java.util.concurrent.atomic.AtomicLong -import kotlin.concurrent.thread import kotlin.reflect.KCallable import kotlin.reflect.KFunction import kotlin.reflect.KProperty import kotlin.reflect.jvm.javaConstructor import kotlin.reflect.jvm.javaGetter import kotlin.reflect.jvm.javaMethod -import kotlin.streams.toList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import mu.KotlinLogging -import org.utbot.framework.plugin.api.ConcreteExecutionFailureException +import org.utbot.framework.plugin.api.InstrumentedProcessDeathException +import org.utbot.common.logException +import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.ConcreteContextLoadingResult +import org.utbot.framework.plugin.api.SpringRepositoryId import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.signature import org.utbot.instrumentation.instrumentation.Instrumentation -import org.utbot.instrumentation.process.ChildProcessRunner -import org.utbot.instrumentation.rd.UtInstrumentationProcess -import org.utbot.instrumentation.rd.generated.ComputeStaticFieldParams -import org.utbot.instrumentation.rd.generated.InvokeMethodCommandParams -import org.utbot.instrumentation.util.ChildProcessError -import org.utbot.rd.loggers.UtRdKLoggerFactory +import org.utbot.instrumentation.process.generated.ComputeStaticFieldParams +import org.utbot.instrumentation.process.generated.GetSpringRepositoriesParams +import org.utbot.instrumentation.process.generated.InvokeMethodCommandParams +import org.utbot.instrumentation.rd.InstrumentedProcess +import org.utbot.instrumentation.util.InstrumentedProcessError +import org.utbot.rd.generated.synchronizationModel +import org.utbot.rd.loggers.overrideDefaultRdLoggerFactoryWithKLogger private val logger = KotlinLogging.logger {} /** - * Creates [ConcreteExecutor], which delegates `execute` calls to the child process, and applies the given [block] to it. + * Creates [ConcreteExecutor], which delegates `execute` calls to the instrumented process, and applies the given [block] to it. * - * The child process will search for the classes in [pathsToUserClasses] and will use [instrumentation] for instrumenting. + * The instrumented process will search for the classes in [pathsToUserClasses] and will use [instrumentation] for instrumenting. * * Specific instrumentation can add functionality to [ConcreteExecutor] via Kotlin extension functions. * @@ -47,11 +48,10 @@ private val logger = KotlinLogging.logger {} * @see [org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation]. */ inline fun > withInstrumentation( - instrumentation: T, + instrumentationFactory: Instrumentation.Factory, pathsToUserClasses: String, - pathsToDependencyClasses: String = ConcreteExecutor.defaultPathsToDependencyClasses, block: (ConcreteExecutor) -> TBlockResult -) = ConcreteExecutor(instrumentation, pathsToUserClasses, pathsToDependencyClasses).use { +) = ConcreteExecutor(instrumentationFactory, pathsToUserClasses).use { block(it) } @@ -59,21 +59,20 @@ class ConcreteExecutorPool(val maxCount: Int = Settings.defaultConcreteExecutorP private val executors = ArrayDeque>(maxCount) /** - * Tries to find the concrete executor for the supplied [instrumentation] and [pathsToDependencyClasses]. If it + * Tries to find the concrete executor for the supplied [instrumentationFactory] and [pathsToDependencyClasses]. If it * doesn't exist, then creates a new one. */ fun > get( - instrumentation: TInstrumentation, + instrumentationFactory: Instrumentation.Factory, pathsToUserClasses: String, - pathsToDependencyClasses: String ): ConcreteExecutor { executors.removeIf { !it.alive } @Suppress("UNCHECKED_CAST") return executors.firstOrNull { - it.pathsToUserClasses == pathsToUserClasses && it.instrumentation == instrumentation && it.pathsToDependencyClasses == pathsToDependencyClasses + it.pathsToUserClasses == pathsToUserClasses && it.instrumentationFactory == instrumentationFactory } as? ConcreteExecutor - ?: ConcreteExecutor.createNew(instrumentation, pathsToUserClasses, pathsToDependencyClasses).apply { + ?: ConcreteExecutor.createNew(instrumentationFactory, pathsToUserClasses).apply { executors.addFirst(this) if (executors.size > maxCount) { executors.removeLast().close() @@ -96,22 +95,20 @@ class ConcreteExecutorPool(val maxCount: Int = Settings.defaultConcreteExecutorP } /** - * Concrete executor class. Takes [pathsToUserClasses] where the child process will search for the classes. Paths should + * Concrete executor class. Takes [pathsToUserClasses] where the instrumented process will search for the classes. Paths should * be separated with [java.io.File.pathSeparatorChar]. * * If [instrumentation] depends on other classes, they should be passed in [pathsToDependencyClasses]. * - * Also takes [instrumentation] object which will be used in the child process for the instrumentation. + * Also takes [instrumentationFactory] object which will be used in the instrumented process to create an [Instrumentation]. * - * @param TIResult the return type of [Instrumentation.invoke] function for the given [instrumentation]. + * @param TIResult the return type of [Instrumentation.invoke] function for the given [Instrumentation]. */ -class ConcreteExecutor> private constructor( - internal val instrumentation: TInstrumentation, - internal val pathsToUserClasses: String, - internal val pathsToDependencyClasses: String +class ConcreteExecutor> private constructor( + internal val instrumentationFactory: Instrumentation.Factory, + internal val pathsToUserClasses: String ) : Closeable, Executor { private val ldef: LifetimeDefinition = LifetimeDefinition() - private val childProcessRunner: ChildProcessRunner = ChildProcessRunner() companion object { @@ -122,11 +119,9 @@ class ConcreteExecutor> p val lastReceiveTimeMs: Long get() = receiveTimeStamp.get() val defaultPool = ConcreteExecutorPool() - var defaultPathsToDependencyClasses = "" init { - Logger.set(Lifetime.Eternal, UtRdKLoggerFactory(logger)) - Runtime.getRuntime().addShutdownHook(thread(start = false) { defaultPool.close() }) + overrideDefaultRdLoggerFactoryWithKLogger(logger) } /** @@ -134,16 +129,14 @@ class ConcreteExecutor> p * and in case of failure, creates a new one. */ operator fun > invoke( - instrumentation: TInstrumentation, + instrumentationFactory: Instrumentation.Factory, pathsToUserClasses: String, - pathsToDependencyClasses: String = defaultPathsToDependencyClasses - ) = defaultPool.get(instrumentation, pathsToUserClasses, pathsToDependencyClasses) + ) = defaultPool.get(instrumentationFactory, pathsToUserClasses) internal fun > createNew( - instrumentation: TInstrumentation, - pathsToUserClasses: String, - pathsToDependencyClasses: String - ) = ConcreteExecutor(instrumentation, pathsToUserClasses, pathsToDependencyClasses) + instrumentationFactory: Instrumentation.Factory, + pathsToUserClasses: String + ) = ConcreteExecutor(instrumentationFactory, pathsToUserClasses) } var classLoader: ClassLoader? = UtContext.currentContext()?.classLoader @@ -153,21 +146,19 @@ class ConcreteExecutor> p get() = ldef.isAlive private val corMutex = Mutex() - private var processInstance: UtInstrumentationProcess? = null + private var processInstance: InstrumentedProcess? = null // this function is intended to be called under corMutex - private suspend fun regenerate(): UtInstrumentationProcess { + private suspend fun regenerate(): InstrumentedProcess { ldef.throwIfNotAlive() - var proc: UtInstrumentationProcess? = processInstance + var proc: InstrumentedProcess? = processInstance if (proc == null || !proc.lifetime.isAlive) { - proc = UtInstrumentationProcess( + proc = InstrumentedProcess( ldef, - childProcessRunner, - instrumentation, + instrumentationFactory, pathsToUserClasses, - pathsToDependencyClasses, classLoader ) processInstance = proc @@ -177,30 +168,23 @@ class ConcreteExecutor> p } /** - * Main entry point for communicating with child process. + * Main entry point for communicating with instrumented process. * Use this function every time you want to access protocol model. - * This method prepares child process for execution and ensures it is alive before giving it block + * This method prepares instrumented process for execution and ensures it is alive before giving it block * * @param exclusively if true - executes block under mutex. * This guarantees that no one can access protocol model - no other calls made before block completes */ - suspend fun withProcess(exclusively: Boolean = false, block: suspend UtInstrumentationProcess.() -> T): T { - fun throwConcreteIfDead(e: Throwable, proc: UtInstrumentationProcess?) { + suspend fun withProcess(exclusively: Boolean = false, block: suspend InstrumentedProcess.() -> T): T { + fun throwConcreteIfDead(e: Throwable, proc: InstrumentedProcess?) { if (proc?.lifetime?.isAlive != true) { - throw ConcreteExecutionFailureException(e, - childProcessRunner.errorLogFile, - try { - proc?.run { process.inputStream.bufferedReader().lines().toList() } ?: emptyList() - } catch (e: Exception) { - emptyList() - } - ) + throw InstrumentedProcessDeathException(e) } } sendTimestamp.set(System.currentTimeMillis()) - var proc: UtInstrumentationProcess? = null + var proc: InstrumentedProcess? = null try { if (exclusively) { @@ -216,7 +200,7 @@ class ConcreteExecutor> p catch (e: CancellationException) { // cancellation can be from 2 causes // 1. process died, its lifetime terminated, so operation was cancelled - // this clearly indicates child process death -> ConcreteExecutionFailureException + // this clearly indicates instrumented process death -> ConcreteExecutionFailureException throwConcreteIfDead(e, proc) // 2. it can be ordinary timeout from coroutine. then just rethrow throw e @@ -226,7 +210,7 @@ class ConcreteExecutor> p // 1. be dead because of this exception throwConcreteIfDead(e, proc) // 2. might be deliberately thrown and process still can operate - throw ChildProcessError(e) + throw InstrumentedProcessError(e) } finally { receiveTimeStamp.set(System.currentTimeMillis()) @@ -238,22 +222,19 @@ class ConcreteExecutor> p signature: String, arguments: Array, parameters: Any? - ): TIResult = try { + ): TIResult = logger.logException("executeAsync, response(ERROR)") { withProcess { val argumentsByteArray = kryoHelper.writeObject(arguments.asList()) val parametersByteArray = kryoHelper.writeObject(parameters) val params = InvokeMethodCommandParams(className, signature, argumentsByteArray, parametersByteArray) - val ba = protocolModel.invokeMethodCommand.startSuspending(lifetime, params).result - kryoHelper.readObject(ba) + val result = instrumentedProcessModel.invokeMethodCommand.startSuspending(lifetime, params).result + kryoHelper.readObject(result) } - } catch (e: Throwable) { - logger.trace { "executeAsync, response(ERROR): $e" } - throw e } /** - * Executes [kCallable] in the child process with the supplied [arguments] and [parameters], e.g. static environment. + * Executes [kCallable] in the instrumented process with the supplied [arguments] and [parameters], e.g. static environment. * * @return the processed result of the method call. */ @@ -284,7 +265,7 @@ class ConcreteExecutor> p if (alive) { try { processInstance?.run { - protocolModel.stopProcess.start(lifetime, Unit) + protocol.synchronizationModel.stopProcess.fire(Unit) } } catch (_: Exception) {} processInstance = null @@ -298,7 +279,24 @@ class ConcreteExecutor> p fun ConcreteExecutor<*,*>.warmup() = runBlocking { withProcess { - protocolModel.warmup.start(lifetime, Unit) + instrumentedProcessModel.warmup.start(lifetime, Unit) + } +} + +fun ConcreteExecutor<*, *>.getRelevantSpringRepositories(classId: ClassId): Set = runBlocking { + withProcess { + val classId = kryoHelper.writeObject(classId) + val params = GetSpringRepositoriesParams(classId) + val result = instrumentedProcessModel.getRelevantSpringRepositories.startSuspending(lifetime, params) + + kryoHelper.readObject(result.springRepositoryIds) + } +} + +fun ConcreteExecutor<*, *>.tryLoadingSpringContext(): ConcreteContextLoadingResult = runBlocking { + withProcess { + val result = instrumentedProcessModel.tryLoadingSpringContext.startSuspending(lifetime, Unit) + kryoHelper.readObject(result.springContextLoadingResult) } } @@ -310,7 +308,7 @@ fun ConcreteExecutor<*, *>.computeStaticField(fieldId: FieldId): Result = val fieldIdSerialized = kryoHelper.writeObject(fieldId) val params = ComputeStaticFieldParams(fieldIdSerialized) - val result = protocolModel.computeStaticField.startSuspending(lifetime, params) + val result = instrumentedProcessModel.computeStaticField.startSuspending(lifetime, params) kryoHelper.readObject(result.result) } diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/Executor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/Executor.kt index 4744cf765a..80213cb248 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/Executor.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/Executor.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.runBlocking * * @param TResult the type of an execution result. */ -interface Executor { +interface Executor { /** * Main method to override. * Returns the result of the execution of the [ExecutableId] with [arguments] and [parameters]. diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/Settings.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/Settings.kt index 0b3cf8c032..045074a26f 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/Settings.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/Settings.kt @@ -14,29 +14,5 @@ object Settings { const val TRACE_ARRAY_SIZE: Int = 1 shl 20 - // TODO: maybe add this guide to confluence? - /** - * If true, runs the child process with the ability to attach a debugger. - * - * To debug the child process, set the breakpoint in the childProcessRunner.start() line - * and in the child process's main function and run the main process. - * Then run the remote JVM debug configuration in IDEA. - * If you see the message in console about successful connection, then - * the debugger is attached successfully. - * Now you can put the breakpoints in the child process and debug - * both processes simultaneously. - * - * @see [org.utbot.instrumentation.process.ChildProcessRunner.cmds] - */ - const val runChildProcessWithDebug = false - - /** - * Property useful only for idea - * If true - runs engine process with the ability to attach a debugger - * @see runChildProcessWithDebug - * @see org.utbot.intellij.plugin.process.EngineProcess - */ - const val runIdeaProcessWithDebug = false - var defaultConcreteExecutorPoolSize = 10 } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/agent/DynamicClassTransformer.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/agent/DynamicClassTransformer.kt index c80e1a7714..e9ba0651ff 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/agent/DynamicClassTransformer.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/agent/DynamicClassTransformer.kt @@ -6,10 +6,13 @@ import com.jetbrains.rd.util.info import org.utbot.common.asPathToFile import org.utbot.framework.plugin.api.util.UtContext import java.lang.instrument.ClassFileTransformer +import java.nio.file.Paths import java.security.ProtectionDomain +import kotlin.io.path.absolutePathString +import kotlin.properties.Delegates -private val logger = getLogger("DynamicClassTransformer") +private val logger = getLogger() /** * Transformer, which will transform only classes with certain names. @@ -17,6 +20,7 @@ private val logger = getLogger("DynamicClassTransformer") class DynamicClassTransformer : ClassFileTransformer { lateinit var transformer: ClassFileTransformer + var useBytecodeTransformation by Delegates.notNull() private val pathsToUserClasses = mutableSetOf() fun addUserPaths(paths: Iterable) { @@ -31,13 +35,16 @@ class DynamicClassTransformer : ClassFileTransformer { classfileBuffer: ByteArray ): ByteArray? { try { - UtContext.currentContext()?.stopWatch?.stop() - val pathToClassfile = protectionDomain.codeSource?.location?.path?.asPathToFile() + // since we got here we have loaded a new class, meaning program is not stuck and some "meaningful" + // non-repeating actions are performed, so we assume that we should not time out for then next 65 ms + UtContext.currentContext()?.stopWatch?.stop(compensationMillis = 65) + val pathToClassfile = protectionDomain.codeSource?.location?.toURI()?.let(Paths::get)?.absolutePathString() return if (pathToClassfile in pathsToUserClasses || packsToAlwaysTransform.any(className::startsWith) ) { - logger.info { "Transforming: $className" } - transformer.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer) + transformer.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer)?.also { + logger.info { "Transformed: $className" } + } } else { null } @@ -50,12 +57,9 @@ class DynamicClassTransformer : ClassFileTransformer { } companion object { - private val packsToAlwaysTransform = listOf( "org/slf4j", "org/utbot/instrumentation/warmup" ) - } - } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt index 362989dbd5..92042d1cc7 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt @@ -2,6 +2,9 @@ package org.utbot.instrumentation.instrumentation import java.lang.instrument.ClassFileTransformer import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.process.kryo.KryoHelper +import org.utbot.instrumentation.process.generated.InstrumentedProcessModel +import org.utbot.rd.IdleWatchdog /** * Abstract class for the instrumentation. @@ -27,8 +30,12 @@ interface Instrumentation : ClassFileTransformer fun getStaticField(fieldId: FieldId): Result<*> - /** - * Will be called in the very beginning in the child process. - */ - fun init(pathsToUserClasses: Set) {} + fun InstrumentedProcessModel.setupAdditionalRdResponses(kryoHelper: KryoHelper, watchdog: IdleWatchdog) {} + + interface Factory> { + val additionalRuntimeClasspath: Set get() = emptySet() + val forceDisableSandbox: Boolean get() = false + + fun create(): TInstrumentation + } } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeInstrumentation.kt index 7e0e454df7..96a1ef9109 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeInstrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeInstrumentation.kt @@ -9,6 +9,7 @@ import java.lang.reflect.Modifier import java.security.ProtectionDomain import org.utbot.common.withAccessibility import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.isStatic import org.utbot.framework.plugin.api.util.jField typealias ArgumentList = List @@ -116,4 +117,8 @@ class InvokeInstrumentation : Instrumentation> { protectionDomain: ProtectionDomain, classfileBuffer: ByteArray ) = null + + object Factory : Instrumentation.Factory, InvokeInstrumentation> { + override fun create(): InvokeInstrumentation = InvokeInstrumentation() + } } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeWithStaticsInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeWithStaticsInstrumentation.kt index 4b91be37fa..9572944aeb 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeWithStaticsInstrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/InvokeWithStaticsInstrumentation.kt @@ -89,6 +89,10 @@ class InvokeWithStaticsInstrumentation : Instrumentation> { .forEach { it.withAccessibility { it.set(null, savedFields[it.name]) } } } } + + object Factory : Instrumentation.Factory, InvokeWithStaticsInstrumentation> { + override fun create(): InvokeWithStaticsInstrumentation = InvokeWithStaticsInstrumentation() + } } private fun checkField(field: Field) = Modifier.isStatic(field.modifiers) diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt index 1617cd4e21..ab3238a97b 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/coverage/CoverageInstrumentation.kt @@ -8,13 +8,11 @@ import org.utbot.instrumentation.instrumentation.ArgumentList import org.utbot.instrumentation.instrumentation.Instrumentation import org.utbot.instrumentation.instrumentation.InvokeWithStaticsInstrumentation import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter -import org.utbot.instrumentation.rd.generated.CollectCoverageParams import org.utbot.instrumentation.util.CastProbesArrayException -import org.utbot.instrumentation.util.ChildProcessError import org.utbot.instrumentation.util.NoProbesArrayException import java.security.ProtectionDomain import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.util.jField +import org.utbot.instrumentation.process.generated.CollectCoverageParams data class CoverageInfo( val methodToInstrRange: Map, @@ -24,7 +22,7 @@ data class CoverageInfo( /** * This instrumentation allows collecting coverage after several calls. */ -object CoverageInstrumentation : Instrumentation> { +class CoverageInstrumentation : Instrumentation> { private val invokeWithStatics = InvokeWithStaticsInstrumentation() /** @@ -96,6 +94,10 @@ object CoverageInstrumentation : Instrumentation> { return instrumenter.classByteCode } + + object Factory : Instrumentation.Factory, CoverageInstrumentation> { + override fun create(): CoverageInstrumentation = CoverageInstrumentation() + } } /** @@ -105,6 +107,6 @@ fun ConcreteExecutor, CoverageInstrumentation>.collectCoverage(clazz: withProcess { val clazzByteArray = kryoHelper.writeObject(clazz) - kryoHelper.readObject(protocolModel.collectCoverage.startSuspending(lifetime, CollectCoverageParams(clazzByteArray)).coverageInfo) + kryoHelper.readObject(instrumentedProcessModel.collectCoverage.startSuspending(lifetime, CollectCoverageParams(clazzByteArray)).coverageInfo) } } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/et/ExecutionTraceInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/et/ExecutionTraceInstrumentation.kt index c10e90e73b..f62eff9b47 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/et/ExecutionTraceInstrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/et/ExecutionTraceInstrumentation.kt @@ -57,4 +57,8 @@ class ExecutionTraceInstrumentation : Instrumentation { classByteCode } } + + object Factory : Instrumentation.Factory { + override fun create(): ExecutionTraceInstrumentation = ExecutionTraceInstrumentation() + } } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/RemovingConstructFailsUtExecutionInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/RemovingConstructFailsUtExecutionInstrumentation.kt new file mode 100644 index 0000000000..803f0b6e8b --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/RemovingConstructFailsUtExecutionInstrumentation.kt @@ -0,0 +1,155 @@ +package org.utbot.instrumentation.instrumentation.execution + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.utbot.framework.plugin.api.Coverage +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MissingState +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtConcreteExecutionProcessedFailure +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtStatementCallModel +import org.utbot.framework.plugin.api.isNull +import org.utbot.framework.plugin.api.mapper.UtModelDeepMapper +import org.utbot.framework.plugin.api.util.defaultValueModel +import org.utbot.framework.process.kryo.KryoHelper +import org.utbot.instrumentation.instrumentation.ArgumentList +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhaseStop +import org.utbot.instrumentation.instrumentation.execution.phases.PhasesController +import org.utbot.instrumentation.instrumentation.execution.phases.ValueConstructionPhase +import org.utbot.instrumentation.process.generated.InstrumentedProcessModel +import org.utbot.rd.IdleWatchdog +import java.security.ProtectionDomain + +/** + * [UtExecutionInstrumentation] that on [invoke] tries to run [invoke] of the [delegateInstrumentation] + * a few times, each time removing failing [UtStatementCallModel]s, until either max number of reruns + * is reached or [invoke] of the [delegateInstrumentation] no longer fails with [UtConcreteExecutionProcessedFailure]. + * + * @see [UtStatementCallModel.thrownConcreteException] + */ +class RemovingConstructFailsUtExecutionInstrumentation( + instrumentationContext: InstrumentationContext, + delegateInstrumentationFactory: UtExecutionInstrumentation.Factory<*> +) : UtExecutionInstrumentation { + companion object { + private const val MAX_RETRIES = 5 + private val logger = getLogger() + } + + private val delegateInstrumentation = delegateInstrumentationFactory.create(object : InstrumentationContext by instrumentationContext { + override fun handleLastCaughtConstructionException(exception: Throwable) { + throw ExecutionPhaseStop( + phase = ValueConstructionPhase::class.java.simpleName, + result = PreliminaryUtConcreteExecutionResult( + stateAfter = MissingState, + result = UtConcreteExecutionProcessedFailure(exception), + coverage = Coverage() + ) + ) + } + }) + private var runsCompleted = 0 + private var nextRunIndexToLog = 1 // we log `attemptsDistribution` every run that has index that is a power of 10 + private val attemptsDistribution = mutableMapOf() + + override fun invoke( + clazz: Class<*>, + methodSignature: String, + arguments: ArgumentList, + parameters: Any?, + phasesWrapper: PhasesController.(invokeBasePhases: () -> PreliminaryUtConcreteExecutionResult) -> PreliminaryUtConcreteExecutionResult + ): UtConcreteExecutionResult { + @Suppress("NAME_SHADOWING") + var parameters = parameters as UtConcreteExecutionData + var attempt = 0 + var res: UtConcreteExecutionResult + try { + do { + res = delegateInstrumentation.invoke(clazz, methodSignature, arguments, parameters, phasesWrapper) + + if (res.result !is UtConcreteExecutionProcessedFailure) + return res + + parameters = parameters.mapModels(UtModelDeepMapper { model -> + shallowlyRemoveFailingCalls(model) + }) + + // if `thisInstance` is present and became `isNull`, then we should stop trying to + // correct this execution and return `UtConcreteExecutionProcessedFailure` + if (parameters.stateBefore.thisInstance?.isNull() == true) + return res + + } while (attempt++ < MAX_RETRIES) + + return res + } finally { + runsCompleted++ + attemptsDistribution[attempt] = (attemptsDistribution[attempt] ?: 0) + 1 + if (runsCompleted == nextRunIndexToLog) { + nextRunIndexToLog *= 10 + logger.info { "Run: $runsCompleted, attemptsDistribution: $attemptsDistribution" } + } + } + } + + private fun shallowlyRemoveFailingCalls(model: UtModel): UtModel = when { + model !is UtAssembleModel -> model + model.instantiationCall.thrownConcreteException != null -> model.classId.defaultValueModel() + else -> UtAssembleModel( + id = model.id, + classId = model.classId, + modelName = model.modelName, + instantiationCall = model.instantiationCall, + origin = model.origin, + modificationsChainProvider = { + model.modificationsChain.filter { + (it as? UtStatementCallModel)?.thrownConcreteException == null && + (it.instance as? UtAssembleModel)?.instantiationCall?.thrownConcreteException == null + } + } + ) + } + + override fun getStaticField(fieldId: FieldId): Result<*> = delegateInstrumentation.getStaticField(fieldId) + + override fun transform( + loader: ClassLoader?, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain, + classfileBuffer: ByteArray + ): ByteArray? = delegateInstrumentation.transform( + loader, className, classBeingRedefined, protectionDomain, classfileBuffer + ) + + override fun InstrumentedProcessModel.setupAdditionalRdResponses(kryoHelper: KryoHelper, watchdog: IdleWatchdog) = + delegateInstrumentation.run { setupAdditionalRdResponses(kryoHelper, watchdog) } + + class Factory( + private val delegateInstrumentationFactory: UtExecutionInstrumentation.Factory<*> + ) : UtExecutionInstrumentation.Factory { + override val additionalRuntimeClasspath: Set + get() = delegateInstrumentationFactory.additionalRuntimeClasspath + + override val forceDisableSandbox: Boolean + get() = delegateInstrumentationFactory.forceDisableSandbox + + override fun create(instrumentationContext: InstrumentationContext): UtExecutionInstrumentation = + RemovingConstructFailsUtExecutionInstrumentation(instrumentationContext, delegateInstrumentationFactory) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Factory + + return delegateInstrumentationFactory == other.delegateInstrumentationFactory + } + + override fun hashCode(): Int { + return delegateInstrumentationFactory.hashCode() + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SimpleUtExecutionInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SimpleUtExecutionInstrumentation.kt new file mode 100644 index 0000000000..bfbdabb159 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SimpleUtExecutionInstrumentation.kt @@ -0,0 +1,209 @@ +package org.utbot.instrumentation.instrumentation.execution + +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.util.executable +import org.utbot.framework.plugin.api.util.signature +import org.utbot.framework.plugin.api.util.singleExecutableId +import org.utbot.instrumentation.agent.Agent +import org.utbot.instrumentation.instrumentation.ArgumentList +import org.utbot.instrumentation.instrumentation.InvokeInstrumentation +import org.utbot.instrumentation.instrumentation.et.TraceHandler +import org.utbot.instrumentation.instrumentation.execution.constructors.ConstructOnlyUserClassesOrCachedObjectsStrategy +import org.utbot.instrumentation.instrumentation.execution.constructors.StateBeforeAwareIdGenerator +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructor +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.context.SimpleInstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.ndd.NonDeterministicClassVisitor +import org.utbot.instrumentation.instrumentation.execution.ndd.NonDeterministicDetector +import org.utbot.instrumentation.instrumentation.execution.phases.PhasesController +import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter +import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor +import org.utbot.instrumentation.instrumentation.transformation.BytecodeTransformer +import java.security.ProtectionDomain +import kotlin.reflect.jvm.javaMethod + +class SimpleUtExecutionInstrumentation( + private val pathsToUserClasses: Set, + private val instrumentationContext: InstrumentationContext = SimpleInstrumentationContext() +) : UtExecutionInstrumentation { + private val delegateInstrumentation = InvokeInstrumentation() + + private val traceHandler = TraceHandler() + private val ndDetector = NonDeterministicDetector() + + /** + * Ignores [arguments], because concrete arguments will be constructed + * from models passed via [parameters]. + * + * Ignores [clazz] and [methodSignature] if they can be constructed + * from [parameters] (see [EnvironmentModels.executableToCall]). + * + * Argument [parameters] must be of type [UtConcreteExecutionData]. + */ + override fun invoke( + clazz: Class<*>, + methodSignature: String, + arguments: ArgumentList, + parameters: Any?, + phasesWrapper: PhasesController.(invokeBasePhases: () -> PreliminaryUtConcreteExecutionResult) -> PreliminaryUtConcreteExecutionResult + ): UtConcreteExecutionResult { + if (parameters !is UtConcreteExecutionData) { + throw IllegalArgumentException("Argument parameters must be of type UtConcreteExecutionData, but was: ${parameters?.javaClass}") + } + val (stateBefore, instrumentations, timeout) = parameters // smart cast to UtConcreteExecutionData + + lateinit var detectedMockingCandidates: Set + + return PhasesController( + instrumentationContext, + traceHandler, + delegateInstrumentation, + timeout, + idGenerator = StateBeforeAwareIdGenerator.fromUtConcreteExecutionData(parameters) + ).computeConcreteExecutionResult { + detectedMockingCandidates = valueConstructionPhase.detectedMockingCandidates + + phasesWrapper { + try { + // some preparation actions for concrete execution + val constructedData = applyPreprocessing(parameters) + + val (params, statics, cache) = constructedData + + // invocation + val concreteResult = executePhaseInTimeout(invocationPhase) { + val executableToCall = stateBefore.executableToCall?.executable + invoke( + clazz = executableToCall?.declaringClass ?: clazz, + methodSignature = executableToCall?.signature ?: methodSignature, + params = params.map { it.value } + ) + } + + // statistics collection + val (coverage, ndResults) = executePhaseInTimeout(statisticsCollectionPhase) { + getCoverage(clazz) to getNonDeterministicResults() + } + + // model construction + val (executionResult, stateAfter, newInstrumentation) = executePhaseInTimeout(modelConstructionPhase) { + configureConstructor { + this.cache = cache + strategy = ConstructOnlyUserClassesOrCachedObjectsStrategy( + pathsToUserClasses, + cache + ) + } + + val ndStatics = constructStaticInstrumentation(ndResults.statics) + val ndNews = constructNewInstrumentation(ndResults.news, ndResults.calls) + val newInstrumentation = mergeInstrumentations(instrumentations, ndStatics, ndNews) + + val returnType = clazz.singleExecutableId(methodSignature).returnType + val executionResult = convertToExecutionResult(concreteResult, returnType) + + val stateAfterParametersWithThis = constructParameters(params) + val stateAfterStatics = constructStatics(stateBefore, statics) + val (stateAfterThis, stateAfterParameters) = if (stateBefore.thisInstance == null) { + null to stateAfterParametersWithThis + } else { + stateAfterParametersWithThis.first() to stateAfterParametersWithThis.drop(1) + } + val stateAfter = EnvironmentModels( + thisInstance = stateAfterThis, + parameters = stateAfterParameters, + statics = stateAfterStatics, + executableToCall = stateBefore.executableToCall + ) + + Triple(executionResult, stateAfter, newInstrumentation) + } + + PreliminaryUtConcreteExecutionResult( + stateAfter, + executionResult, + coverage, + newInstrumentation + ) + } finally { + // restoring data after concrete execution + applyPostprocessing() + } + } + }.toCompleteUtConcreteExecutionResult( + stateBefore = stateBefore, + detectedMockingCandidates = detectedMockingCandidates, + ) + } + + override fun getStaticField(fieldId: FieldId): Result = + delegateInstrumentation.getStaticField(fieldId).map { value -> + UtModelConstructor.createOnlyUserClassesConstructor( + pathsToUserClasses = pathsToUserClasses, + utModelWithCompositeOriginConstructorFinder = instrumentationContext::findUtModelWithCompositeOriginConstructor + ).construct(value, fieldId.type) + } + + override fun transform( + loader: ClassLoader?, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain, + classfileBuffer: ByteArray + ): ByteArray { + val instrumenter = Instrumenter(classfileBuffer, loader) + + if (Agent.dynamicClassTransformer.useBytecodeTransformation) { + instrumenter.visitClass { writer -> + BytecodeTransformer(writer) + } + } + + traceHandler.registerClass(className) + instrumenter.visitInstructions(traceHandler.computeInstructionVisitor(className)) + + instrumenter.visitClass { writer -> + NonDeterministicClassVisitor(writer, ndDetector) + } + + val mockClassVisitor = instrumenter.visitClass { writer -> + MockClassVisitor( + writer, + InstrumentationContext.MockGetter::getMock.javaMethod!!, + InstrumentationContext.MockGetter::checkCallSite.javaMethod!!, + InstrumentationContext.MockGetter::hasMock.javaMethod!! + ) + } + + mockClassVisitor.signatureToId.forEach { (method, id) -> + instrumentationContext.methodSignatureToId += method to id + } + + return instrumenter.classByteCode + } + + class Factory( + private val pathsToUserClasses: Set + ) : UtExecutionInstrumentation.Factory { + override fun create(): UtExecutionInstrumentation = SimpleUtExecutionInstrumentation(pathsToUserClasses) + + override fun create(instrumentationContext: InstrumentationContext): UtExecutionInstrumentation = + SimpleUtExecutionInstrumentation(pathsToUserClasses, instrumentationContext) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Factory + + return pathsToUserClasses == other.pathsToUserClasses + } + + override fun hashCode(): Int { + return pathsToUserClasses.hashCode() + } + } +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/UtExecutionInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/UtExecutionInstrumentation.kt new file mode 100644 index 0000000000..3986bef965 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/UtExecutionInstrumentation.kt @@ -0,0 +1,100 @@ +package org.utbot.instrumentation.instrumentation.execution + +import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.mapper.UtModelMapper +import org.utbot.framework.plugin.api.mapper.mapModels +import org.utbot.instrumentation.instrumentation.ArgumentList +import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.context.SimpleInstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.phases.PhasesController + +/** + * Consists of the data needed to execute the method concretely. Also includes method arguments stored in models. + * + * @property [stateBefore] is necessary for construction of parameters of a concrete call. + * @property [instrumentation] is necessary for mocking static methods and new instances. + * @property [timeout] is timeout for specific concrete execution (in milliseconds). + * @property [isRerun] reruns can be used to obtain more reproducible results (e.g. on clean Spring application context), + * rerun can take more time due to context reinitialisation. + * + * By default is initialized from [UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis] + */ +data class UtConcreteExecutionData( + val stateBefore: EnvironmentModels, + val instrumentation: List, + val timeout: Long, + val isRerun: Boolean, +) + +fun UtConcreteExecutionData.mapModels(mapper: UtModelMapper) = copy( + stateBefore = stateBefore.mapModels(mapper), + instrumentation = instrumentation.map { it.mapModels(mapper) } +) + +/** + * [UtConcreteExecutionResult] that has not yet been populated with extra data, e.g.: + * - updated [UtConcreteExecutionResult.stateBefore] + * - [UtConcreteExecutionResult.detectedMockingCandidates] + */ +data class PreliminaryUtConcreteExecutionResult( + val stateAfter: EnvironmentModels, + val result: UtExecutionResult, + val coverage: Coverage, + val newInstrumentation: List? = null, +) { + fun toCompleteUtConcreteExecutionResult( + stateBefore: EnvironmentModels, + detectedMockingCandidates: Set + ) = UtConcreteExecutionResult( + stateBefore = stateBefore, + stateAfter = stateAfter, + result = result, + coverage = coverage, + newInstrumentation = newInstrumentation, + detectedMockingCandidates = detectedMockingCandidates, + ) +} + +data class UtConcreteExecutionResult( + val stateBefore: EnvironmentModels, + val stateAfter: EnvironmentModels, + val result: UtExecutionResult, + val coverage: Coverage, + val newInstrumentation: List? = null, + val detectedMockingCandidates: Set, +) { + override fun toString(): String = buildString { + appendLine("UtConcreteExecutionResult(") + appendLine("stateBefore=$stateBefore") + appendLine("stateAfter=$stateAfter") + appendLine("result=$result") + appendLine("coverage=$coverage)") + } +} + +interface UtExecutionInstrumentation : Instrumentation { + override fun invoke( + clazz: Class<*>, + methodSignature: String, + arguments: ArgumentList, + parameters: Any? + ): UtConcreteExecutionResult = invoke( + clazz, methodSignature, arguments, parameters, phasesWrapper = { invokeBasePhases -> invokeBasePhases() } + ) + + fun invoke( + clazz: Class<*>, + methodSignature: String, + arguments: ArgumentList, + parameters: Any?, + phasesWrapper: PhasesController.(invokeBasePhases: () -> PreliminaryUtConcreteExecutionResult) -> PreliminaryUtConcreteExecutionResult + ): UtConcreteExecutionResult + + interface Factory : Instrumentation.Factory { + override fun create(): TInstrumentation = create(SimpleInstrumentationContext()) + + fun create(instrumentationContext: InstrumentationContext): TInstrumentation + } +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/InstrumentationContextAwareValueConstructor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/InstrumentationContextAwareValueConstructor.kt new file mode 100644 index 0000000000..51c398ca36 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/InstrumentationContextAwareValueConstructor.kt @@ -0,0 +1,644 @@ +package org.utbot.instrumentation.instrumentation.execution.constructors + +import org.mockito.Mockito +import org.mockito.stubbing.Answer +import org.objectweb.asm.Type +import org.utbot.common.Reflection +import org.utbot.common.invokeCatching +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.DirectFieldAccessId +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.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtClassRefModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.UtCustomModel +import org.utbot.framework.plugin.api.UtDirectGetFieldModel +import org.utbot.framework.plugin.api.UtDirectSetFieldModel +import org.utbot.framework.plugin.api.UtEnumConstantModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtLambdaModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation +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.UtStatementCallModel +import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation +import org.utbot.framework.plugin.api.UtVoidModel +import org.utbot.framework.plugin.api.isNull +import org.utbot.framework.plugin.api.util.anyInstance +import org.utbot.framework.plugin.api.util.classClassId +import org.utbot.framework.plugin.api.util.constructor +import org.utbot.framework.plugin.api.util.constructor.CapturedArgument +import org.utbot.framework.plugin.api.util.constructor.constructLambda +import org.utbot.framework.plugin.api.util.constructor.constructStaticLambda +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.isStatic +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.jField +import org.utbot.framework.plugin.api.util.method +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.mock.InstanceMockController +import org.utbot.instrumentation.instrumentation.execution.mock.MethodMockController +import org.utbot.instrumentation.instrumentation.execution.mock.MockController +import org.utbot.instrumentation.process.runSandbox +import java.lang.reflect.Modifier +import java.lang.reflect.TypeVariable +import java.security.AccessController +import java.security.PrivilegedAction +import java.util.* +import kotlin.reflect.KClass + +/** + * Constructs values (including mocks) from models. + * + * Uses model->constructed object reference-equality cache. + * + * This class is based on `ValueConstructor.kt`. The main difference is the ability to create mocked objects, mock + * static methods, and [construct context dependent values][InstrumentationContext.constructContextDependentValue]. + * + * Note that `clearState` was deleted! + */ +// TODO: JIRA:1379 -- Refactor ValueConstructor and InstrumentationContextAwareValueConstructor +class InstrumentationContextAwareValueConstructor( + private val instrumentationContext: InstrumentationContext, + private val idGenerator: StateBeforeAwareIdGenerator, +) { + companion object { + private const val MAX_DYNAMIC_MOCK_DEPTH = 5 + } + + private val classLoader: ClassLoader + get() = utContext.classLoader + + val objectToModelCache: IdentityHashMap + get() { + val objectToModel = IdentityHashMap() + constructedObjects.forEach { (model, obj) -> + objectToModel[obj] = model + } + return objectToModel + } + + val detectedMockingCandidates: MutableSet = mutableSetOf() + + var lastCaughtException: Throwable? = null + private set + + // TODO: JIRA:1379 -- replace UtReferenceModel with Int + private val constructedObjects = HashMap() + + /** + * Controllers contain info about mocked methods and have to be closed to restore initial state. + */ + private val controllers = mutableListOf() + + fun constructMethodParameters(models: List): List> = + models.mapIndexed { _, model -> construct(model) } + + fun constructStatics(staticsBefore: Map): Map> = + staticsBefore.mapValues { (_, model) -> construct(model) } + + /** + * Main construction method. + * + * Takes care of nulls. Does not use cache, instead construct(Object/Array/List/FromAssembleModel) method + * uses cache directly. + * + * Takes mock creation context (possible mock target) to create mock if required. + */ + private fun construct(model: UtModel): UtConcreteValue<*> = + when (model) { + is UtNullModel -> UtConcreteValue(null, model.classId.jClass) + is UtPrimitiveModel -> UtConcreteValue(model.value, model.classId.jClass) + is UtEnumConstantModel -> UtConcreteValue(constructEnum(model)) + is UtClassRefModel -> UtConcreteValue(model.value.jClass) + is UtCompositeModel -> UtConcreteValue(constructObject(model), model.classId.jClass) + is UtArrayModel -> UtConcreteValue(constructArray(model)) + is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model), model.classId.jClass) + is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) + is UtVoidModel -> UtConcreteValue(Unit) + else -> { + instrumentationContext.constructContextDependentValue(model) ?: + if (model is UtCustomModel) + construct(model.origin ?: error("Can't construct value for custom model without origin [$model]")) + else + // PythonModel, JsUtModel may be here + throw UnsupportedOperationException("UtModel $model cannot construct UtConcreteValue") + } + } + + /** + * Use this method if you need to use [construct], while being in a sandbox. + * + * Permission elevation is required, because [construct] heavily uses reflection and Mockito. + */ + private fun constructPrivileged(model: UtModel): UtConcreteValue<*> = + AccessController.doPrivileged(PrivilegedAction { construct(model) }) + + /** + * Constructs an Enum<*> instance by model, uses reference-equality cache. + */ + private fun constructEnum(model: UtEnumConstantModel): Any { + constructedObjects[model]?.let { return it } + constructedObjects[model] = model.value + return model.value + } + + /** + * Constructs object by model, uses reference-equality cache. + * + * Returns null for mock cause cannot instantiate it. + */ + private fun constructObject(model: UtCompositeModel): Any { + constructedObjects[model]?.let { return it } + + val javaClass = javaClass(model.classId) + + val classInstance = if (!model.isMock) { + val notMockInstance = javaClass.anyInstance + + constructedObjects[model] = notMockInstance + notMockInstance + } else { + val concreteValues = model.mocks.mapValues { mutableListOf() } + val mockInstance = generateMockitoMock(javaClass, concreteValues, model) + + constructedObjects[model] = mockInstance + + concreteValues.forEach { (executableId, valuesList) -> + val mockModels = model.mocks.getValue(executableId) + // If model is unit, then null should be returned (this model has to be already constructed). + val constructedValues = mockModels.map { model -> construct(model).value.takeIf { it != Unit } } + valuesList.addAll(constructedValues) + } + + if (model.canHaveRedundantOrMissingMocks) { + // we clear `mocks` to avoid redundant mocks, + // actually useful mocks should be later added back as they are used + model.mocks.clear() + } + + mockInstance + } + + model.fields.forEach { (fieldId, fieldModel) -> + val declaredField = fieldId.jField + val accessible = declaredField.isAccessible + declaredField.isAccessible = true + + check(Reflection.isModifiersAccessible()) + + val value = construct(fieldModel).value + val instance = if (Modifier.isStatic(declaredField.modifiers)) null else classInstance + declaredField.set(instance, value) + declaredField.isAccessible = accessible + } + + return classInstance + } + + private fun generateMockitoAnswer(concreteValues: Map>): Answer<*> { + val pointers = concreteValues.mapValues { (_, _) -> 0 }.toMutableMap() + return Answer { invocation -> + with(invocation.method) { + pointers[executableId].let { pointer -> + concreteValues[executableId].let { values -> + if (pointer != null && values != null && pointer < values.size) { + pointers[executableId] = pointer + 1 + values[pointer] + } else { + invocation.callRealMethod() + } + } + } + } + } + } + + private fun generateMockitoAnswerHandlingRedundantAndMissingMocks( + concreteValues: Map>, + mockModel: UtCompositeModel + ): Answer<*> { + class MockedExecutable( + val executableId: ExecutableId, + val answerValues: List, + val answerModels: List, + ) { + private var pointer: Int = 0 + + fun nextAnswer(): Any? { + val answerValue = answerValues[pointer] + val answerModel = answerModels[pointer] + pointer = (pointer + 1) % answerValues.size + + // Record mock answers into `mockModel.mocks` as these answers are used. + // Avoid recording multiple answers if same answer is reused over and over again. + if (answerValues.size > 1 || executableId !in mockModel.mocks) { + (mockModel.mocks.getOrPut(executableId) { mutableListOf() } as MutableList).add(answerModel) + } + + return answerValue + } + } + + val mockedExecutables = concreteValues.mapValues { (executableId, values) -> + MockedExecutable( + executableId = executableId, + answerValues = values, + answerModels = mockModel.mocks.getValue(executableId), + ) + }.toMutableMap() + + return Answer { invocation -> + with(invocation.method) { + mockedExecutables.getOrPut(executableId) { + detectedMockingCandidates.add(executableId) + var answerModel = generateNewAnswerModel(executableId, dynamicMockModelToDepth[mockModel] ?: 0) + val answerValue = runCatching { constructPrivileged(answerModel) }.getOrElse { + // fallback is used, so we still get some value (null) for types + // that can't be mocked, e.g. arrays and sealed interfaces + answerModel = executableId.returnType.defaultValueModel() + constructPrivileged(answerModel) + } + + MockedExecutable( + executableId = executableId, + // `Unit` is replaced with `null`, because in Java `void` methods actually return `null` + answerValues = listOf(answerValue.value.takeUnless { it == Unit }), + answerModels = listOf(answerModel) + ) + }.nextAnswer() + } + } + } + + private val dynamicMockModelToDepth = mutableMapOf() + + private fun generateNewAnswerModel(methodId: MethodId, depth: Int) = + methodId.returnType.defaultValueModel().takeUnless { it.isNull() } ?: when { + // use `null` to avoid false positive `ClassCastException` + methodId.method.genericReturnType is TypeVariable<*> -> UtNullModel(methodId.returnType) + + // mockito can't mock `String` and `Class` + methodId.returnType == stringClassId -> UtNullModel(stringClassId) + methodId.returnType == classClassId -> UtClassRefModel( + id = idGenerator.createId(), + classId = classClassId, + value = classClassId, + ) + depth > MAX_DYNAMIC_MOCK_DEPTH -> UtNullModel(methodId.classId) + + else -> UtCompositeModel( + id = idGenerator.createId(), + // TODO mockito can't mock sealed interfaces, + // we have to mock their implementations or use null + classId = methodId.returnType, + isMock = true, + canHaveRedundantOrMissingMocks = true, + ).also { dynamicMockModelToDepth[it] = depth + 1 } + } + + private fun generateMockitoMock( + clazz: Class<*>, + concreteValues: Map>, + mockModel: UtCompositeModel + ): Any { + val answer = + if (mockModel.canHaveRedundantOrMissingMocks) { + generateMockitoAnswerHandlingRedundantAndMissingMocks(concreteValues, mockModel) + } else { + generateMockitoAnswer(concreteValues) + } + + return Mockito.mock(clazz, answer) + } + + private fun computeConcreteValuesForMethods( + methodToValues: Map>, + ): Map> = methodToValues.mapValues { (_, models) -> + models.map { mockAndGet(it) } + } + + /** + * Mocks methods on [instance] with supplied [methodToValues]. + * + * Also add new controllers to [controllers]. Each controller corresponds to one method. If it is a static method, then the controller + * must be closed. If it is a non-static method and you don't change the mocks behaviour on the passed instance, + * then the controller doesn't have to be closed + * + * @param [instance] must be non-`null` for non-static methods. + * @param [methodToValues] return values for methods. + */ + private fun mockMethods( + instance: Any?, + methodToValues: Map>, + ) { + controllers += computeConcreteValuesForMethods(methodToValues).map { (method, values) -> + if (method !is MethodId) { + throw IllegalArgumentException("Expected MethodId, but got: $method") + } + MethodMockController( + method.classId.jClass, + method.method, + instance, + values, + instrumentationContext + ) + } + + } + + /** + * Mocks static methods according to instrumentations. + */ + fun mockStaticMethods( + instrumentations: List, + ) { + val methodToValues = instrumentations.associate { it.methodId as ExecutableId to it.values } + mockMethods(null, methodToValues) + } + + /** + * Mocks new instances according to instrumentations + */ + fun mockNewInstances( + instrumentations: List, + ) { + controllers += instrumentations.map { mock -> + InstanceMockController( + mock.classId, + mock.instances.map { mockAndGet(it) }, + mock.callSites.map { Type.getType(it.jClass).internalName }.toSet() + ) + } + } + + /** + * Constructs array by model. + * + * Supports arrays of primitive, arrays of arrays and arrays of objects. + * + * Note: does not check isNull, but if isNull set returns empty array because for null array length set to 0. + */ + private fun constructArray(model: UtArrayModel): Any { + constructedObjects[model]?.let { return it } + + with(model) { + 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) } + }.also { constructedObjects[model] = it } + "S" -> ShortArray(length) { primitive(constModel) }.apply { + stores.forEach { (index, model) -> this[index] = primitive(model) } + }.also { constructedObjects[model] = it } + "C" -> CharArray(length) { primitive(constModel) }.apply { + stores.forEach { (index, model) -> this[index] = primitive(model) } + }.also { constructedObjects[model] = it } + "I" -> IntArray(length) { primitive(constModel) }.apply { + stores.forEach { (index, model) -> this[index] = primitive(model) } + }.also { constructedObjects[model] = it } + "J" -> LongArray(length) { primitive(constModel) }.apply { + stores.forEach { (index, model) -> this[index] = primitive(model) } + }.also { constructedObjects[model] = it } + "F" -> FloatArray(length) { primitive(constModel) }.apply { + stores.forEach { (index, model) -> this[index] = primitive(model) } + }.also { constructedObjects[model] = it } + "D" -> DoubleArray(length) { primitive(constModel) }.apply { + stores.forEach { (index, model) -> this[index] = primitive(model) } + }.also { constructedObjects[model] = it } + "Z" -> BooleanArray(length) { primitive(constModel) }.apply { + stores.forEach { (index, model) -> this[index] = primitive(model) } + }.also { constructedObjects[model] = it } + else -> { + val javaClass = javaClass(elementClassId) + val instance = java.lang.reflect.Array.newInstance(javaClass, length) as Array<*> + constructedObjects[model] = instance + for (i in instance.indices) { + val elementModel = stores[i] ?: constModel + val value = construct(elementModel).value + try { + java.lang.reflect.Array.set(instance, i, value) + } catch (iae:IllegalArgumentException) { + throw IllegalArgumentException( + iae.message + " array: ${instance.javaClass.name}; value: ${value?.javaClass?.name}" , iae + ) + } + } + instance + } + } + } + } + + /** + * Constructs object with [UtAssembleModel]. + */ + private fun constructFromAssembleModel(assembleModel: UtAssembleModel): Any { + constructedObjects[assembleModel]?.let { return it } + + val instantiationExecutableCall = assembleModel.instantiationCall + val result = updateWithStatementCallModel(instantiationExecutableCall).getOrThrow() + + // Executions that get `null` in a complicated way (e.g. like this: `new Pair(null, null).getFirst()`) + // are only produced by fuzzer and are considered undesirable because fuzzer can also produce simpler + // executions that just use `null` literal. + // + // So for such executions we throw `IllegalStateException` that indicates that construction of arguments + // has failed and causes execution to terminate with `UtConcreteExecutionProcessedFailure` execution result. + // + // That is also helpful, because it allows to safely use the constructed value where primitive type is expected, + // otherwise some models generated by `FieldValueProvider` can lead to false-positive NPEs. + checkNotNull(result) { + "Tracked instance can't be null for call ${instantiationExecutableCall.statement} in model $assembleModel" + } + constructedObjects[assembleModel] = result + + assembleModel.modificationsChain.forEach { statementModel -> + when (statementModel) { + is UtStatementCallModel -> updateWithStatementCallModel(statementModel) + is UtDirectSetFieldModel -> updateWithDirectSetFieldModel(statementModel) + } + } + + return constructedObjects[assembleModel] ?: error("Can't assemble model: $assembleModel") + } + + private fun constructFromLambdaModel(lambdaModel: UtLambdaModel): Any { + constructedObjects[lambdaModel]?.let { return it } + // A class representing a functional interface. + val samType: Class<*> = lambdaModel.samType.jClass + // A class where the lambda is declared. + val declaringClass: Class<*> = lambdaModel.declaringClass.jClass + // A name of the synthetic method that represents a lambda. + val lambdaName = lambdaModel.lambdaName + + val lambda = if (lambdaModel.lambdaMethodId.isStatic) { + val capturedArguments = lambdaModel.capturedValues + .map { model -> CapturedArgument(type = model.classId.jClass, value = value(model)) } + .toTypedArray() + constructStaticLambda(samType, declaringClass, lambdaName, *capturedArguments) + } else { + val capturedReceiverModel = lambdaModel.capturedValues.firstOrNull() + ?: error("Non-static lambda must capture `this` instance, so there must be at least one captured value") + + // Values that the given lambda has captured. + val capturedReceiver = value(capturedReceiverModel) + val capturedArguments = lambdaModel.capturedValues.subList(1, lambdaModel.capturedValues.size) + .map { model -> CapturedArgument(type = model.classId.jClass, value = value(model)) } + .toTypedArray() + constructLambda(samType, declaringClass, lambdaName, capturedReceiver, *capturedArguments) + } + constructedObjects[lambdaModel] = lambda + return lambda + } + + /** + * Updates instance state with [callModel] invocation. + * + * @return the result of [callModel] invocation + */ + private fun updateWithStatementCallModel(callModel: UtStatementCallModel): Result<*> = + runCatching { + when (callModel) { + is UtExecutableCallModel -> { + val executable = callModel.executable + val instanceValue = callModel.instance?.let { value(it) } + val params = callModel.params.map { value(it) } + + when (executable) { + is MethodId -> executable.call(params, instanceValue) + is ConstructorId -> executable.call(params) + } + } + + is UtDirectGetFieldModel -> { + val fieldAccess = callModel.fieldAccess + val instanceValue = value(callModel.instance) + + fieldAccess.get(instanceValue) + } + } + } + .also { result -> + result + .exceptionOrNull() + ?.let { + lastCaughtException = it + callModel.thrownConcreteException = it.javaClass.id + } + } + + + /** + * Updates instance with [UtDirectSetFieldModel] execution. + */ + private fun updateWithDirectSetFieldModel(directSetterModel: UtDirectSetFieldModel) { + val instanceModel = directSetterModel.instance + val instance = value(instanceModel) + + val fieldModel = directSetterModel.fieldModel + + val field = directSetterModel.fieldId.jField + val isAccessible = field.isAccessible + + try { + //set field accessible to support protected or package-private direct setters + field.isAccessible = true + + //construct and set the value + val fieldValue = construct(fieldModel).value + field.set(instance, fieldValue) + } finally { + //restore accessibility property of the field + field.isAccessible = isAccessible + } + } + + /** + * Constructs value from [UtModel]. + */ + private fun value(model: UtModel) = construct(model).value + + private fun mockAndGet(model: UtModel): Any? { + return construct(model).value + } + + private fun MethodId.call(args: List, instance: Any?): Any? = + method.runSandbox(bypassesSandbox) { + invokeCatching(obj = instance, args = args).getOrThrow() + } + + private fun ConstructorId.call(args: List): Any? = + constructor.runSandbox(bypassesSandbox) { + newInstance(*args.toTypedArray()) + } + + private fun DirectFieldAccessId.get(instance: Any?): Any? { + val field = fieldId.jField + return field.runSandbox { + field.get(instance) + } + } + + /** + * Fetches primitive value from NutsModel to create array of primitives. + */ + private inline fun primitive(model: UtModel): T = (model as UtPrimitiveModel).value as T + + private fun javaClass(id: ClassId) = kClass(id).java + + private fun kClass(id: ClassId) = + if (id.elementClassId != null) { + arrayClassOf(id.elementClassId!!) + } else { + when (id.jvmName) { + "B" -> Byte::class + "S" -> Short::class + "C" -> Char::class + "I" -> Int::class + "J" -> Long::class + "F" -> Float::class + "D" -> Double::class + "Z" -> Boolean::class + else -> classLoader.loadClass(id.name).kotlin + } + } + + private fun arrayClassOf(elementClassId: ClassId): KClass<*> = + if (elementClassId.elementClassId != null) { + val elementClass = arrayClassOf(elementClassId.elementClassId!!) + java.lang.reflect.Array.newInstance(elementClass.java, 0)::class + } else { + when (elementClassId.jvmName) { + "B" -> ByteArray::class + "S" -> ShortArray::class + "C" -> CharArray::class + "I" -> IntArray::class + "J" -> LongArray::class + "F" -> FloatArray::class + "D" -> DoubleArray::class + "Z" -> BooleanArray::class + else -> { + val elementClass = classLoader.loadClass(elementClassId.name) + java.lang.reflect.Array.newInstance(elementClass, 0)::class + } + } + } + + fun resetMockedMethods() { + controllers.forEach { it.close() } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/IterableConstructors.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/IterableConstructors.kt similarity index 85% rename from utbot-framework/src/main/kotlin/org/utbot/framework/concrete/IterableConstructors.kt rename to utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/IterableConstructors.kt index 6866000d06..45149c94a8 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/IterableConstructors.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/IterableConstructors.kt @@ -1,4 +1,4 @@ -package org.utbot.framework.concrete +package org.utbot.instrumentation.instrumentation.execution.constructors import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId @@ -7,9 +7,9 @@ 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.booleanClassId -import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.collectionClassId +import org.utbot.framework.plugin.api.util.mapClassId import org.utbot.framework.plugin.api.util.objectClassId -import org.utbot.framework.util.valueToClassId internal class CollectionConstructor : UtAssembleModelConstructorBase() { override fun UtAssembleModel.provideModificationChain( @@ -23,9 +23,7 @@ internal class CollectionConstructor : UtAssembleModelConstructorBase() { // This value will be constructed as UtCompositeModel. val models = value.map { internalConstructor.construct(it, valueToClassId(it)) } - val classId = value::class.java.id - - val addMethodId = MethodId(classId, "add", booleanClassId, listOf(objectClassId)) + val addMethodId = MethodId(collectionClassId, "add", booleanClassId, listOf(objectClassId)) return models.map { UtExecutableCallModel(this, addMethodId, listOf(it)) } } @@ -64,7 +62,7 @@ internal class MapConstructor : UtAssembleModelConstructorBase() { internalConstructor.run { construct(key, valueToClassId(key)) to construct(value, valueToClassId(value)) } } - val putMethodId = MethodId(classId, "put", objectClassId, listOf(objectClassId, objectClassId)) + val putMethodId = MethodId(mapClassId, "put", objectClassId, listOf(objectClassId, objectClassId)) return keyToValueModels.map { (key, value) -> UtExecutableCallModel(this, putMethodId, listOf(key, value)) diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/JavaStdLibCustomModelConstructors.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/JavaStdLibCustomModelConstructors.kt new file mode 100644 index 0000000000..72419f9c49 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/JavaStdLibCustomModelConstructors.kt @@ -0,0 +1,79 @@ +package org.utbot.instrumentation.instrumentation.execution.constructors + +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.primitiveWrappers +import org.utbot.framework.plugin.api.util.voidWrapperClassId + +val javaStdLibModelWithCompositeOriginConstructors: Map, () -> UtModelWithCompositeOriginConstructor> = + mutableMapOf, () -> UtAssembleModelConstructorBase>( + /** + * Optionals + */ + java.util.OptionalInt::class.java to { OptionalIntConstructor() }, + java.util.OptionalLong::class.java to { OptionalLongConstructor() }, + java.util.OptionalDouble::class.java to { OptionalDoubleConstructor() }, + java.util.Optional::class.java to { OptionalConstructor() }, + + /** + * Lists + */ + java.util.LinkedList::class.java to { CollectionConstructor() }, + java.util.ArrayList::class.java to { CollectionConstructor() }, + java.util.AbstractList::class.java to { CollectionConstructor() }, + java.util.List::class.java to { CollectionConstructor() }, + java.util.concurrent.CopyOnWriteArrayList::class.java to { CollectionConstructor() }, + + /** + * Queues, deques + */ + java.util.PriorityQueue::class.java to { CollectionConstructor() }, + java.util.ArrayDeque::class.java to { CollectionConstructor() }, + java.util.concurrent.LinkedBlockingQueue::class.java to { CollectionConstructor() }, + java.util.concurrent.LinkedBlockingDeque::class.java to { CollectionConstructor() }, + java.util.concurrent.ConcurrentLinkedQueue::class.java to { CollectionConstructor() }, + java.util.concurrent.ConcurrentLinkedDeque::class.java to { CollectionConstructor() }, + java.util.Queue::class.java to { CollectionConstructor() }, + java.util.Deque::class.java to { CollectionConstructor() }, + + /** + * Sets + */ + java.util.HashSet::class.java to { CollectionConstructor() }, + java.util.TreeSet::class.java to { CollectionConstructor() }, + java.util.LinkedHashSet::class.java to { CollectionConstructor() }, + java.util.AbstractSet::class.java to { CollectionConstructor() }, + java.util.Set::class.java to { CollectionConstructor() }, + + /** + * Maps + */ + java.util.HashMap::class.java to { MapConstructor() }, + java.util.TreeMap::class.java to { MapConstructor() }, + java.util.LinkedHashMap::class.java to { MapConstructor() }, + java.util.AbstractMap::class.java to { MapConstructor() }, + java.util.concurrent.ConcurrentMap::class.java to { MapConstructor() }, + java.util.concurrent.ConcurrentHashMap::class.java to { MapConstructor() }, + java.util.IdentityHashMap::class.java to { MapConstructor() }, + java.util.WeakHashMap::class.java to { MapConstructor() }, + + /** + * Hashtables + */ + java.util.Hashtable::class.java to { MapConstructor() }, + + /** + * String wrapper + */ + java.lang.String::class.java.let { it to { PrimitiveWrapperConstructor() } }, + + /** + * TODO: JIRA:1405 -- Add assemble constructors for another standard classes as well. + */ + ).apply { + /** + * Primitive wrappers + */ + this += primitiveWrappers + .filter { it != voidWrapperClassId } + .associate { it.jClass to { PrimitiveWrapperConstructor() } } + } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/OptionalConstructors.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/OptionalConstructors.kt similarity index 97% rename from utbot-framework/src/main/kotlin/org/utbot/framework/concrete/OptionalConstructors.kt rename to utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/OptionalConstructors.kt index ae3b062c47..76b9e87eda 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/OptionalConstructors.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/OptionalConstructors.kt @@ -1,5 +1,10 @@ -package org.utbot.framework.concrete +package org.utbot.instrumentation.instrumentation.execution.constructors +import java.util.Optional +import java.util.OptionalDouble +import java.util.OptionalInt +import java.util.OptionalLong +import kotlin.reflect.KFunction1 import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.UtAssembleModel @@ -10,11 +15,6 @@ import org.utbot.framework.plugin.api.util.intClassId import org.utbot.framework.plugin.api.util.jClass import org.utbot.framework.plugin.api.util.longClassId import org.utbot.framework.plugin.api.util.objectClassId -import java.util.Optional -import java.util.OptionalDouble -import java.util.OptionalInt -import java.util.OptionalLong -import kotlin.reflect.KFunction1 internal sealed class OptionalConstructorBase : UtAssembleModelConstructorBase() { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/PrimitiveWrapperConstructor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/PrimitiveWrapperConstructor.kt similarity index 94% rename from utbot-framework/src/main/kotlin/org/utbot/framework/concrete/PrimitiveWrapperConstructor.kt rename to utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/PrimitiveWrapperConstructor.kt index d9521caf3f..ca73e24295 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/PrimitiveWrapperConstructor.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/PrimitiveWrapperConstructor.kt @@ -1,4 +1,4 @@ -package org.utbot.framework.concrete +package org.utbot.instrumentation.instrumentation.execution.constructors import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.UtAssembleModel diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/StateBeforeAwareIdGenerator.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/StateBeforeAwareIdGenerator.kt new file mode 100644 index 0000000000..4192283aec --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/StateBeforeAwareIdGenerator.kt @@ -0,0 +1,26 @@ +package org.utbot.instrumentation.instrumentation.execution.constructors + +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtReferenceModel +import org.utbot.framework.plugin.api.mapper.UtModelDeepMapper.Companion.collectAllModels +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionData +import org.utbot.instrumentation.instrumentation.execution.mapModels + +class StateBeforeAwareIdGenerator(allPreExistingModels: Collection) { + private val seenIds = allPreExistingModels + .filterIsInstance() + .mapNotNull { it.id } + .toMutableSet() + + private var nextId = 0 + + fun createId(): Int { + while (nextId in seenIds) nextId++ + return nextId++ + } + + companion object { + fun fromUtConcreteExecutionData(data: UtConcreteExecutionData): StateBeforeAwareIdGenerator = + StateBeforeAwareIdGenerator(collectAllModels { collector -> data.mapModels(collector) }) + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/StreamConstructors.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/StreamConstructors.kt new file mode 100644 index 0000000000..eeb578433b --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/StreamConstructors.kt @@ -0,0 +1,120 @@ +package org.utbot.instrumentation.instrumentation.execution.constructors + +import org.utbot.framework.plugin.api.ClassId +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.UtModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.UtStatementModel +import org.utbot.framework.plugin.api.util.defaultValueModel +import org.utbot.framework.plugin.api.util.doubleArrayClassId +import org.utbot.framework.plugin.api.util.doubleStreamClassId +import org.utbot.framework.plugin.api.util.intArrayClassId +import org.utbot.framework.plugin.api.util.intStreamClassId +import org.utbot.framework.plugin.api.util.isPrimitiveWrapper +import org.utbot.framework.plugin.api.util.longArrayClassId +import org.utbot.framework.plugin.api.util.longStreamClassId +import org.utbot.framework.plugin.api.util.methodId +import org.utbot.framework.plugin.api.util.objectArrayClassId +import org.utbot.framework.plugin.api.util.streamClassId + +/** + * Max number of elements in any concrete stream. + */ +private const val STREAM_ELEMENTS_LIMIT: Int = 1_000_000 + +internal abstract class AbstractStreamConstructor( + private val streamClassId: ClassId, + private val elementsClassId: ClassId, +) : UtAssembleModelConstructorBase() { + private val singleElementClassId: ClassId = elementsClassId.elementClassId + ?: error("Stream $streamClassId elements have to be an array but $elementsClassId found") + + private val elementDefaultValueModel: UtModel = singleElementClassId.defaultValueModel() + + override fun provideInstantiationCall( + internalConstructor: UtModelConstructorInterface, + value: Any, + classId: ClassId, + ): UtExecutableCallModel { + value as java.util.stream.BaseStream<*, *> + + val valueAsArray = value + .iterator() + .asSequence() + .take(STREAM_ELEMENTS_LIMIT) + .toList() + .toTypedArray() + + if (valueAsArray.isEmpty()) { + return UtExecutableCallModel( + instance = null, + executable = emptyMethodId, + params = emptyList() + ) + } + + // If [valueAsArray] constructed incorrectly (some inner transient fields are null, etc.) this may fail. + // This value will be constructed as UtCompositeModel. + val arrayModel = (internalConstructor.construct(valueAsArray, valueToClassId(valueAsArray)) as UtArrayModel) + .copy(classId = elementsClassId, constModel = elementDefaultValueModel) + .apply { stores.replaceAll { _, m -> m.wrapperModelToPrimitiveModel() } } + + return UtExecutableCallModel( + instance = null, + executable = ofMethodId, + params = listOf(arrayModel) + ) + } + + override fun UtAssembleModel.provideModificationChain( + internalConstructor: UtModelConstructorInterface, + value: Any + ): List = emptyList() + + private val emptyMethodId: MethodId = methodId( + classId = this.streamClassId, + name = "empty", + returnType = this.streamClassId, + arguments = emptyArray() + ) + + private val ofMethodId: MethodId = methodId( + classId = this.streamClassId, + name = "of", + returnType = this.streamClassId, + arguments = arrayOf(elementsClassId) // vararg + ) + + /** + * Transforms [this] to [UtPrimitiveModel] if it is an [UtAssembleModel] for the corresponding wrapper + * (primitive int and wrapper Integer, etc.), and throws an error otherwise. + */ + private fun UtModel.wrapperModelToPrimitiveModel(): UtModel { + if (!classId.isPrimitiveWrapper) { + // We do not need to transform classes other than primitive wrappers + return this + } + + require(this !is UtNullModel) { + "Unexpected null value in wrapper for primitive stream ${this@AbstractStreamConstructor}" + } + + require(this is UtAssembleModel) { + "Unexpected not wrapper assemble model $this for value in wrapper " + + "for primitive stream ${this@AbstractStreamConstructor.streamClassId}" + } + + return (instantiationCall.params.firstOrNull() as? UtPrimitiveModel) + ?: error("No primitive value parameter for wrapper constructor $instantiationCall in model $this " + + "in wrapper for primitive stream ${this@AbstractStreamConstructor.streamClassId}") + } +} + +internal class BaseStreamConstructor : AbstractStreamConstructor(streamClassId, objectArrayClassId) +internal class IntStreamConstructor : AbstractStreamConstructor(intStreamClassId, intArrayClassId) +internal class LongStreamConstructor : AbstractStreamConstructor(longStreamClassId, longArrayClassId) +internal class DoubleStreamConstructor : AbstractStreamConstructor(doubleStreamClassId, doubleArrayClassId) diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtAssembleModelConstructors.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtAssembleModelConstructors.kt new file mode 100644 index 0000000000..f6acd29bba --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtAssembleModelConstructors.kt @@ -0,0 +1,53 @@ +package org.utbot.instrumentation.instrumentation.execution.constructors + +import java.util.stream.BaseStream +import java.util.stream.DoubleStream +import java.util.stream.IntStream +import java.util.stream.LongStream +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtStatementModel + +internal fun findStreamConstructor(stream: BaseStream<*, *>): UtAssembleModelConstructorBase = + when (stream) { + is IntStream -> IntStreamConstructor() + is LongStream -> LongStreamConstructor() + is DoubleStream -> DoubleStreamConstructor() + else -> BaseStreamConstructor() + } + +internal abstract class UtAssembleModelConstructorBase : UtModelWithCompositeOriginConstructor { + override fun constructModelWithCompositeOrigin( + internalConstructor: UtModelConstructorInterface, + value: Any, + valueClassId: ClassId, + id: Int?, + saveToCache: (UtModel) -> Unit + ): UtAssembleModel { + val baseName = valueClassId.simpleName.decapitalize() + val instantiationCall = provideInstantiationCall(internalConstructor, value, valueClassId) + return UtAssembleModel(id, valueClassId, nextModelName(baseName), instantiationCall) { + saveToCache(this) + provideModificationChain(internalConstructor, value) + } + } + + protected abstract fun provideInstantiationCall( + internalConstructor: UtModelConstructorInterface, + value: Any, + classId: ClassId + ): UtExecutableCallModel + + protected abstract fun UtAssembleModel.provideModificationChain( + internalConstructor: UtModelConstructorInterface, + value: Any + ): List +} + +internal fun UtAssembleModelConstructorBase.checkClassCast(expected: Class<*>, actual: Class<*>) { + require(expected.isAssignableFrom(actual)) { + "Can't cast $actual to $expected in $this assemble constructor." + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelConstructor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelConstructor.kt new file mode 100644 index 0000000000..14f4cb16bf --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelConstructor.kt @@ -0,0 +1,450 @@ +package org.utbot.instrumentation.instrumentation.execution.constructors + +import org.utbot.common.asPathToFile +import org.utbot.common.withAccessibility +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.* +import org.utbot.framework.plugin.api.visible.UtStreamConsumingException +import java.lang.reflect.Modifier +import java.lang.reflect.Proxy +import java.util.* +import java.util.stream.BaseStream + +/** + * Represents common interface for model constructors. + */ +interface UtModelConstructorInterface { + /** + * Constructs a UtModel from a concrete [value] with a specific [classId]. + */ + fun construct(value: Any?, classId: ClassId): UtModel +} + +/** + * Constructs models from concrete values. + * + * Uses reflection to traverse fields recursively ignoring static final fields. Also uses object->constructed model + * reference-equality cache. + * + * @param objectToModelCache cache used for the model construction with respect to stateBefore. For each object, it first + * @param compositeModelStrategy decides whether we should construct a composite model for a certain value or not. + * @param maxDepth determines max depth for composite and assemble model nesting + * searches in [objectToModelCache] for [UtReferenceModel.id]. + */ +class UtModelConstructor( + private val objectToModelCache: IdentityHashMap, + private val idGenerator: StateBeforeAwareIdGenerator, + private val utModelWithCompositeOriginConstructorFinder: (ClassId) -> UtModelWithCompositeOriginConstructor?, + private val compositeModelStrategy: UtCompositeModelStrategy = AlwaysConstructStrategy, + private val maxDepth: Long = DEFAULT_MAX_DEPTH +) : UtModelConstructorInterface { + private val constructedObjects = IdentityHashMap() + + companion object { + const val DEFAULT_MAX_DEPTH = 7L + + fun createOnlyUserClassesConstructor( + pathsToUserClasses: Set, + utModelWithCompositeOriginConstructorFinder: (ClassId) -> UtModelWithCompositeOriginConstructor? + ): UtModelConstructor { + val cache = IdentityHashMap() + val strategy = ConstructOnlyUserClassesOrCachedObjectsStrategy( + pathsToUserClasses, cache + ) + return UtModelConstructor( + objectToModelCache = cache, + idGenerator = StateBeforeAwareIdGenerator(allPreExistingModels = emptySet()), + utModelWithCompositeOriginConstructorFinder = utModelWithCompositeOriginConstructorFinder, + compositeModelStrategy = strategy + ) + } + } + + private fun computeUnusedIdAndUpdate(): Int = idGenerator.createId() + + private fun handleId(value: Any): Int { + return objectToModelCache[value]?.let { (it as? UtReferenceModel)?.id } ?: computeUnusedIdAndUpdate() + } + + private val proxyLambdaSubstring = "$\$Lambda$" + + private fun isProxyLambda(value: Any?): Boolean { + if (value == null) { + return false + } + return proxyLambdaSubstring in value::class.java.name + } + + private fun constructFakeLambda(value: Any, classId: ClassId): UtLambdaModel { + val baseClassName = value::class.java.name.substringBefore(proxyLambdaSubstring) + val baseClass = utContext.classLoader.loadClass(baseClassName).id + return UtLambdaModel.createFake(handleId(value), classId, baseClass) + } + + private fun isProxy(value: Any?): Boolean = + value != null && Proxy.isProxyClass(value::class.java) + + /** + * Using `UtAssembleModel` for dynamic proxies helps to avoid exceptions like + * `java.lang.ClassNotFoundException: jdk.proxy3.$Proxy184` during code generation. + */ + private fun constructProxy(value: Any, classId: ClassId): UtAssembleModel { + val newProxyInstanceExecutableId = java.lang.reflect.Proxy::newProxyInstance.executableId + + // we don't want to construct deep models for invocationHandlers, since they can be quite large + val argsRemainingDepth = 0L + + val classLoader = UtAssembleModel( + id = computeUnusedIdAndUpdate(), + classId = newProxyInstanceExecutableId.parameters[0], + modelName = "systemClassLoader", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = ClassLoader::getSystemClassLoader.executableId, + params = emptyList() + ) + ) + val interfaces = construct( + value::class.java.interfaces, + newProxyInstanceExecutableId.parameters[1], + remainingDepth = argsRemainingDepth + ) + val invocationHandler = construct( + Proxy.getInvocationHandler(value), + newProxyInstanceExecutableId.parameters[2], + remainingDepth = argsRemainingDepth + ) + + return UtAssembleModel( + id = handleId(value), + classId = classId, + modelName = "dynamicProxy", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = newProxyInstanceExecutableId, + params = listOf(classLoader, interfaces, invocationHandler) + ) + ) + } + + /** + * Constructs a UtModel from a concrete [value] with a specific [classId]. The result can be a [UtAssembleModel] + * as well. + * + * Handles cache on stateBefore values. + */ + override fun construct(value: Any?, classId: ClassId): UtModel = + construct(value, classId, maxDepth) + + private fun construct(value: Any?, classId: ClassId, remainingDepth: Long): UtModel { + objectToModelCache[value]?.let { model -> + if (model is UtLambdaModel) { + return model + } + } + if (isProxyLambda(value)) { + return constructFakeLambda(value!!, classId) + } + if (isProxy(value)) { + return constructProxy(value!!, classId) + } + return when (value) { + null -> UtNullModel(classId) + is Unit -> UtVoidModel + is Byte, + is Short, + is Char, + is Int, + is Long, + is Float, + is Double, + is Boolean -> { + if (classId.isPrimitive) UtPrimitiveModel(value) + else constructFromAny(value, classId, remainingDepth) + } + + is ByteArray -> constructFromByteArray(value, remainingDepth) + is ShortArray -> constructFromShortArray(value, remainingDepth) + is CharArray -> constructFromCharArray(value, remainingDepth) + is IntArray -> constructFromIntArray(value, remainingDepth) + is LongArray -> constructFromLongArray(value, remainingDepth) + is FloatArray -> constructFromFloatArray(value, remainingDepth) + is DoubleArray -> constructFromDoubleArray(value, remainingDepth) + is BooleanArray -> constructFromBooleanArray(value, remainingDepth) + is Array<*> -> constructFromArray(value, remainingDepth) + is Enum<*> -> constructFromEnum(value) + is Class<*> -> constructFromClass(value) + is BaseStream<*, *> -> constructFromStream(value) + else -> constructFromAny(value, classId, remainingDepth) + } + } + + fun constructMock(instance: Any, classId: ClassId, mocks: Map>): UtModel = + constructedObjects.getOrElse(instance) { + val utModel = UtCompositeModel( + handleId(instance), + classId, + isMock = true, + mocks = mocks.mapValuesTo(mutableMapOf()) { (method, values) -> + values.map { construct(it, method.returnType) } + } + ) + constructedObjects[instance] = utModel + utModel + } + + // Q: Is there a way to get rid of duplicated code? + + private fun constructFromDoubleArray(array: DoubleArray, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(array) { + val stores = mutableMapOf() + val utModel = + UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toDouble()), stores) + constructedObjects[array] = utModel + array.forEachIndexed { idx, value -> + stores[idx] = construct(value, doubleClassId, remainingDepth - 1) + } + utModel + } + + private fun constructFromFloatArray(array: FloatArray, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(array) { + val stores = mutableMapOf() + val utModel = + UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toFloat()), stores) + constructedObjects[array] = utModel + array.forEachIndexed { idx, value -> + stores[idx] = construct(value, floatClassId, remainingDepth - 1) + } + utModel + } + + private fun constructFromLongArray(array: LongArray, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(array) { + val stores = mutableMapOf() + val utModel = + UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toLong()), stores) + constructedObjects[array] = utModel + array.forEachIndexed { idx, value -> + stores[idx] = construct(value, longClassId, remainingDepth - 1) + } + utModel + } + + private fun constructFromIntArray(array: IntArray, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(array) { + val stores = mutableMapOf() + val utModel = UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0), stores) + constructedObjects[array] = utModel + array.forEachIndexed { idx, value -> + stores[idx] = construct(value, intClassId, remainingDepth - 1) + } + utModel + } + + private fun constructFromCharArray(array: CharArray, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(array) { + val stores = mutableMapOf() + val utModel = + UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toChar()), stores) + constructedObjects[array] = utModel + array.forEachIndexed { idx, value -> + stores[idx] = construct(value, charClassId, remainingDepth - 1) + } + utModel + } + + private fun constructFromShortArray(array: ShortArray, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(array) { + val stores = mutableMapOf() + val utModel = + UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toShort()), stores) + constructedObjects[array] = utModel + array.forEachIndexed { idx, value -> + stores[idx] = construct(value, shortClassId, remainingDepth - 1) + } + utModel + } + + private fun constructFromByteArray(array: ByteArray, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(array) { + val stores = mutableMapOf() + val utModel = + UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(0.toByte()), stores) + constructedObjects[array] = utModel + array.forEachIndexed { idx, value -> + stores[idx] = construct(value, byteClassId, remainingDepth - 1) + } + utModel + } + + private fun constructFromBooleanArray(array: BooleanArray, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(array) { + val stores = mutableMapOf() + val utModel = + UtArrayModel(handleId(array), array::class.java.id, array.size, UtPrimitiveModel(false), stores) + constructedObjects[array] = utModel + array.forEachIndexed { idx, value -> + stores[idx] = construct(value, booleanClassId, remainingDepth - 1) + } + utModel + } + + private fun constructFromArray(array: Array<*>, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(array) { + val stores = mutableMapOf() + val utModel = + UtArrayModel(handleId(array), array::class.java.id, array.size, UtNullModel(objectClassId), stores) + constructedObjects[array] = utModel + array.forEachIndexed { idx, value -> + stores[idx] = construct(value, objectClassId, remainingDepth - 1) + } + utModel + } + + private fun constructFromEnum(enum: Enum<*>): UtModel = + constructedObjects.getOrElse(enum) { + val utModel = UtEnumConstantModel(handleId(enum), enum::class.java.id, enum) + constructedObjects[enum] = utModel + utModel + } + + private fun constructFromClass(clazz: Class<*>): UtModel = + constructedObjects.getOrElse(clazz) { + val utModel = UtClassRefModel(handleId(clazz), clazz::class.java.id, clazz.id) + constructedObjects[clazz] = utModel + utModel + } + + private fun constructFromStream(stream: BaseStream<*, *>): UtModel = + constructedObjects.getOrElse(stream) { + val streamConstructor = findStreamConstructor(stream) + + try { + streamConstructor.constructModelWithCompositeOrigin(this, stream, valueToClassId(stream), handleId(stream)) { + constructedObjects[stream] = it + } + } catch (e: Exception) { + // An exception occurs during consuming of the stream - + // remove the constructed object and throw this exception as a result + constructedObjects.remove(stream) + throw UtStreamConsumingException(e) + } + } + + /** + * First tries to construct UtAssembleModel. If failure, constructs UtCompositeModel. + */ + private fun constructFromAny(value: Any, classId: ClassId, remainingDepth: Long): UtModel = + constructedObjects.getOrElse(value) { + tryConstructCustomModel(value, remainingDepth) + ?: findEqualValueOfWellKnownType(value) + ?.takeIf { (_, replacementClassId) -> replacementClassId isSubtypeOf classId } + ?.let { (replacement, replacementClassId) -> + // right now replacements only work with `UtAssembleModel` + (tryConstructCustomModel(replacement, remainingDepth) as? UtAssembleModel) + ?.copy(classId = replacementClassId) + } + ?: constructCompositeModel(value, remainingDepth) + } + + private fun findEqualValueOfWellKnownType(value: Any): Pair? = runCatching { + when (value) { + is List<*> -> ArrayList(value) to listClassId + is Set<*> -> LinkedHashSet(value) to setClassId + is Map<*, *> -> LinkedHashMap(value) to mapClassId + else -> null + } + }.getOrNull() + + /** + * Constructs custom UtModel but does it only for predefined list of classes. + * + * Uses runtime class of [value]. + */ + private fun tryConstructCustomModel(value: Any, remainingDepth: Long): UtModel? = + utModelWithCompositeOriginConstructorFinder(value::class.java.id)?.let { modelConstructor -> + try { + modelConstructor.constructModelWithCompositeOrigin( + internalConstructor = this.withMaxDepth(remainingDepth - 1), + value = value, + valueClassId = valueToClassId(value), + id = handleId(value), + ) { + constructedObjects[value] = it + } + } catch (e: Exception) { // If UtAssembleModel constructor failed, we need to remove model and return null + constructedObjects.remove(value) + null + } + } + + /** + * Constructs UtCompositeModel. + * + * Uses runtime javaClass to collect ALL fields, except final static fields, and builds this model recursively. + */ + private fun constructCompositeModel(value: Any, remainingDepth: Long): UtCompositeModel { + // value can be mock only if it was previously constructed from UtCompositeModel + val isMock = objectToModelCache[value]?.isMockModel() ?: false + + val javaClazz = if (isMock) objectToModelCache.getValue(value).classId.jClass else value::class.java + if (remainingDepth <= 0 || !compositeModelStrategy.shouldConstruct(value, javaClazz)) { + return UtCompositeModel( + handleId(value), + javaClazz.id, + isMock, + fields = mutableMapOf() // we don't want to construct any further fields. + ) + } + + val fields = mutableMapOf() + val utModel = UtCompositeModel(handleId(value), javaClazz.id, isMock, fields) + constructedObjects[value] = utModel + generateSequence(javaClazz) { it.superclass }.forEach { clazz -> + val allFields = clazz.declaredFields + allFields + .asSequence() + .filter { !(Modifier.isFinal(it.modifiers) && Modifier.isStatic(it.modifiers)) } // TODO: what about static final fields? + .filterNot { it.fieldId.isInaccessibleViaReflection } + .forEach { it.withAccessibility { fields[it.fieldId] = construct(it.get(value), it.type.id, remainingDepth - 1) } } + } + return utModel + } + + private fun withMaxDepth(newMaxDepth: Long) = object : UtModelConstructorInterface { + override fun construct(value: Any?, classId: ClassId): UtModel = + construct(value, classId, newMaxDepth) + } +} + +/** + * Decides, should we construct a UtCompositeModel from a value or not. + */ +interface UtCompositeModelStrategy { + fun shouldConstruct(value: Any, clazz: Class<*>): Boolean +} + +internal object AlwaysConstructStrategy : UtCompositeModelStrategy { + override fun shouldConstruct(value: Any, clazz: Class<*>): Boolean = true +} + +/** + * This class constructs only user classes or values which are already in [objectToModelCache]. + * + * [objectToModelCache] is a cache which we build in the time of creating concrete values from [UtModel]s. + */ +internal class ConstructOnlyUserClassesOrCachedObjectsStrategy( + private val userDependencyPaths: Set, + private val objectToModelCache: IdentityHashMap +) : UtCompositeModelStrategy { + /** + * Check whether [clazz] is a user class or [value] is in cache. + */ + override fun shouldConstruct(value: Any, clazz: Class<*>): Boolean = + isUserClass(clazz) || value in objectToModelCache + + private fun isUserClass(clazz: Class<*>): Boolean = + clazz.protectionDomain.codeSource?.let { it.location.path.asPathToFile() in userDependencyPaths } ?: false + +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelWithCompositeOriginConstructor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelWithCompositeOriginConstructor.kt new file mode 100644 index 0000000000..034f7a2306 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelWithCompositeOriginConstructor.kt @@ -0,0 +1,30 @@ +package org.utbot.instrumentation.instrumentation.execution.constructors + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtModelWithCompositeOrigin + +/** + * Responsible for constructing [UtModelWithCompositeOrigin]s of some specific type, that are more human-readable + * when rendered by the code generation compared to [UtCompositeModel]s. + */ +interface UtModelWithCompositeOriginConstructor { + + /** + * @param internalConstructor constructor to use for constructing child models + * (e.g. when [value] is a list, [internalConstructor] is used for constructing list elements) + * @param value object to construct model for + * @param valueClassId [ClassId] to use for constructed model + * @param saveToCache function that should be called on the returned model right after constructing it, + * but before adding any modifications, so [internalConstructor] doesn't have to reconstruct it for every modification + * and recursive values (e.g. list containing itself) are constructed correctly + */ + fun constructModelWithCompositeOrigin( + internalConstructor: UtModelConstructorInterface, + value: Any, + valueClassId: ClassId, + id: Int?, + saveToCache: (UtModel) -> Unit + ): UtModelWithCompositeOrigin +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/Utils.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/Utils.kt new file mode 100644 index 0000000000..e79ba0aefd --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/Utils.kt @@ -0,0 +1,11 @@ +package org.utbot.instrumentation.instrumentation.execution.constructors + +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.objectClassId +import java.util.concurrent.atomic.AtomicInteger + +internal fun valueToClassId(value: Any?) = value?.let { it::class.java.id } ?: objectClassId + +val concreteModelId = AtomicInteger() + +fun nextModelName(base: String): String = "${base}_concrete_${concreteModelId.incrementAndGet()}" \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/InstrumentationContext.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/InstrumentationContext.kt new file mode 100644 index 0000000000..7c95db4dcb --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/InstrumentationContext.kt @@ -0,0 +1,114 @@ +package org.utbot.instrumentation.instrumentation.execution.context + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtConcreteExecutionProcessedFailure +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtStatementCallModel +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelWithCompositeOriginConstructor +import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhase +import org.utbot.instrumentation.instrumentation.execution.phases.ValueConstructionPhase +import java.lang.reflect.Method +import java.util.IdentityHashMap +import org.utbot.instrumentation.instrumentation.mock.computeKeyForMethod + +/** + * Some information, which is fully computed after classes instrumentation. + * + * This information will be used later in `invoke` function to construct values and models. + */ +interface InstrumentationContext { + /** + * Contains unique id for each method, which is required for this method mocking. + */ + val methodSignatureToId: MutableMap + + /** + * Constructs value that is dependent on the context provided by supported frameworks used in project (e.g. Spring). + * Returns `null` if no context dependent value can be constructed for specified [model]. + * + * NOTE! Doesn't attempt to construct context independent values, + * constructing such values is a responsibility of the user of this method. + */ + fun constructContextDependentValue(model: UtModel): UtConcreteValue<*>? + + /** + * Finds [UtModelWithCompositeOriginConstructor] that should be used to + * construct models for instances of specified [class][classId]. + */ + fun findUtModelWithCompositeOriginConstructor(classId: ClassId): UtModelWithCompositeOriginConstructor? + + /** + * Called when [timedOutedPhase] times out. + * This method is executed in the same thread that [timedOutedPhase] was run in. + * Implementor is expected to only perform some clean up operations (e.g. rollback transactions in Spring). + */ + fun onPhaseTimeout(timedOutedPhase: ExecutionPhase) + + /** + * At the very end of the [ValueConstructionPhase], instrumentation context gets to decide what to do + * with last caught [UtStatementCallModel.thrownConcreteException] (it can be caught if the call + * is non-essential for value construction, i.e. it's in [UtAssembleModel.modificationsChain]). + * + * A reasonable implementation may: + * - ignore the [exception] + * - cause phase to terminate with [UtConcreteExecutionProcessedFailure] + */ + fun handleLastCaughtConstructionException(exception: Throwable) + + object MockGetter { + data class MockContainer(private val values: List<*>) { + private var ptr: Int = 0 + fun hasNext(): Boolean = ptr < values.size + fun nextValue(): Any? = values[ptr++] + } + + /** + * Instance -> method -> list of values in the return order + */ + private val mocks = IdentityHashMap>() + private val callSites = HashMap>() + + /** + * Returns possibility of taking mock object of method with supplied [methodSignature] on an [obj] object. + */ + @JvmStatic + fun hasMock(obj: Any?, methodSignature: String): Boolean = + mocks[obj]?.get(methodSignature)?.hasNext() ?: false + + /** + * Returns the next value for mocked method with supplied [methodSignature] on an [obj] object. + * + * This function has only to be called from the instrumented bytecode everytime + * we need a next value for a mocked method. + */ + @JvmStatic + fun getMock(obj: Any?, methodSignature: String): Any? = + mocks[obj]?.get(methodSignature).let { container -> + container ?: error("Can't get mock container for method [$obj\$$methodSignature]") + container.nextValue() + } + + /** + * Returns current callSites for mocking new instance of [instanceType] contains [callSite] or not + */ + @JvmStatic + fun checkCallSite(instanceType: String, callSite: String): Boolean { + return callSites.getOrDefault(instanceType, emptySet()).contains(callSite) + } + + fun updateCallSites(instanceType: String, instanceCallSites: Set) { + callSites[instanceType] = instanceCallSites + } + + fun updateMocks(obj: Any?, methodSignature: String, values: List<*>) { + val methodMocks = mocks.getOrPut(obj) { mutableMapOf() } + methodMocks[methodSignature] = MockContainer(values) + } + + fun updateMocks(obj: Any?, method: Method, values: List<*>) { + updateMocks(obj, computeKeyForMethod(method), values) + } + } +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SimpleInstrumentationContext.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SimpleInstrumentationContext.kt new file mode 100644 index 0000000000..f0d91da558 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SimpleInstrumentationContext.kt @@ -0,0 +1,29 @@ +package org.utbot.instrumentation.instrumentation.execution.context + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelWithCompositeOriginConstructor +import org.utbot.instrumentation.instrumentation.execution.constructors.javaStdLibModelWithCompositeOriginConstructors +import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhase + +/** + * Simple instrumentation context, that is used for pure JVM projects without + * any frameworks with special support from UTBot (like Spring) + */ +class SimpleInstrumentationContext : InstrumentationContext { + override val methodSignatureToId = mutableMapOf() + + /** + * There are no context dependent values for pure JVM projects + */ + override fun constructContextDependentValue(model: UtModel): UtConcreteValue<*>? = null + + override fun findUtModelWithCompositeOriginConstructor(classId: ClassId): UtModelWithCompositeOriginConstructor? = + javaStdLibModelWithCompositeOriginConstructors[classId.jClass]?.invoke() + + override fun onPhaseTimeout(timedOutedPhase: ExecutionPhase) = Unit + + override fun handleLastCaughtConstructionException(exception: Throwable) = Unit +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/InstanceMockController.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstanceMockController.kt similarity index 79% rename from utbot-framework/src/main/kotlin/org/utbot/framework/concrete/InstanceMockController.kt rename to utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstanceMockController.kt index c7f3e214bd..e2952ab112 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/InstanceMockController.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstanceMockController.kt @@ -1,8 +1,9 @@ -package org.utbot.framework.concrete +package org.utbot.instrumentation.instrumentation.execution.mock import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.util.jClass import org.objectweb.asm.Type +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext class InstanceMockController( clazz: ClassId, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MethodMockController.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MethodMockController.kt similarity index 81% rename from utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MethodMockController.kt rename to utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MethodMockController.kt index 342c867a15..73af206f5c 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MethodMockController.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MethodMockController.kt @@ -1,11 +1,12 @@ -package org.utbot.framework.concrete +package org.utbot.instrumentation.instrumentation.execution.mock -import org.utbot.common.withAccessibility -import org.utbot.framework.plugin.api.util.signature -import org.utbot.instrumentation.instrumentation.mock.MockConfig import java.lang.reflect.Field import java.lang.reflect.Method import java.lang.reflect.Modifier +import org.utbot.common.withAccessibility +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import org.utbot.instrumentation.instrumentation.mock.MockConfig +import org.utbot.instrumentation.instrumentation.mock.computeKeyForMethod /** @@ -31,7 +32,8 @@ class MethodMockController( error("$method is an instance method, but instance is null!") } - val id = instrumentationContext.methodSignatureToId[method.signature] + val computedSignature = computeKeyForMethod(method) + val id = instrumentationContext.methodSignatureToId[computedSignature] isMockField = clazz.declaredFields.firstOrNull { it.name == MockConfig.IS_MOCK_FIELD + id } ?: error("No field ${MockConfig.IS_MOCK_FIELD + id} in $clazz") diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MockController.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MockController.kt new file mode 100644 index 0000000000..a5595c3edf --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MockController.kt @@ -0,0 +1,5 @@ +package org.utbot.instrumentation.instrumentation.execution.mock + +import java.io.Closeable + +interface MockController : Closeable \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicBytecodeInserter.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicBytecodeInserter.kt new file mode 100644 index 0000000000..39607565a0 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicBytecodeInserter.kt @@ -0,0 +1,85 @@ +package org.utbot.instrumentation.instrumentation.execution.ndd + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type + +class NonDeterministicBytecodeInserter { + private val internalName = Type.getInternalName(NonDeterministicResultStorage::class.java) + + private fun String.getUnifiedParamsTypes(): List { + val list = mutableListOf() + var readObject = false + for (c in this) { + if (c == '(') { + continue + } + if (c == ')') { + break + } + if (readObject) { + if (c == ';') { + readObject = false + list.add("Ljava/lang/Object;") + } + } else if (c == 'L') { + readObject = true + } else { + list.add(c.toString()) + } + } + + return list + } + + private fun String.unifyTypeDescriptor(): String = + if (startsWith('L')) { + "Ljava/lang/Object;" + } else { + this + } + + private fun String.getReturnType(): String = + substringAfter(')') + + private fun getStoreDescriptor(descriptor: String): String = buildString { + append('(') + append(descriptor.getReturnType().unifyTypeDescriptor()) + append("Ljava/lang/String;)V") + } + + private fun MethodVisitor.invoke(name: String, descriptor: String) { + visitMethodInsn(Opcodes.INVOKESTATIC, internalName, name, descriptor, false) + } + + fun insertAfterNDMethod(mv: MethodVisitor, owner: String, name: String, descriptor: String, isStatic: Boolean) { + mv.visitInsn(Opcodes.DUP) + mv.visitLdcInsn(NonDeterministicResultStorage.makeSignature(owner, name, descriptor)) + mv.invoke(if (isStatic) "storeStatic" else "storeCall", getStoreDescriptor(descriptor)) + } + + fun insertBeforeNDMethod(mv: MethodVisitor, descriptor: String, isStatic: Boolean) { + if (isStatic) { + return + } + + val params = descriptor.getUnifiedParamsTypes() + + params.asReversed().forEach { + mv.invoke("putParameter${it[0]}", "($it)V") + } + + mv.visitInsn(Opcodes.DUP) + mv.invoke("saveInstance", "(Ljava/lang/Object;)V") + + params.forEach { + mv.invoke("peakParameter${it[0]}", "()$it") + } + } + + fun insertAfterNDInstanceConstructor(mv: MethodVisitor, callSite: String) { + mv.visitInsn(Opcodes.DUP) + mv.visitLdcInsn(callSite) + mv.invoke("registerInstance", "(Ljava/lang/Object;Ljava/lang/String;)V") + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicClassVisitor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicClassVisitor.kt new file mode 100644 index 0000000000..83ac459594 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicClassVisitor.kt @@ -0,0 +1,68 @@ +package org.utbot.instrumentation.instrumentation.execution.ndd + +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.utbot.instrumentation.Settings + +class NonDeterministicClassVisitor( + classVisitor: ClassVisitor, + private val detector: NonDeterministicDetector +) : ClassVisitor(Settings.ASM_API, classVisitor) { + + private lateinit var currentClass: String + + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array? + ) { + currentClass = name + super.visit(version, access, name, signature, superName, interfaces) + } + + override fun visitMethod( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array? + ): MethodVisitor { + val mv = cv.visitMethod(access, name, descriptor, signature, exceptions) + return object : MethodVisitor(Settings.ASM_API, mv) { + override fun visitMethodInsn( + opcodeAndSource: Int, + owner: String, + name: String, + descriptor: String, + isInterface: Boolean + ) { + if (name == "") { + mv.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface) + if (detector.isNonDeterministicClass(owner)) { + detector.inserter.insertAfterNDInstanceConstructor(mv, currentClass) + } + return + } + + val (isND, isStatic) = if (opcodeAndSource == Opcodes.INVOKESTATIC) { + detector.isNonDeterministicStaticFunction(owner, name, descriptor) to true + } else { + detector.isNonDeterministicClass(owner) to false + } + + if (isND) { + detector.inserter.insertBeforeNDMethod(mv, descriptor, isStatic) + mv.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface) + detector.inserter.insertAfterNDMethod(mv, owner, name, descriptor, isStatic) + } else { + mv.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface) + } + + } + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicDetector.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicDetector.kt new file mode 100644 index 0000000000..d6ef35c2be --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicDetector.kt @@ -0,0 +1,21 @@ +package org.utbot.instrumentation.instrumentation.execution.ndd + +class NonDeterministicDetector { + private val nonDeterministicStaticMethods: HashSet = HashSet() + + private val nonDeterministicClasses: HashSet = buildList { + add("java/util/Random") + add("kotlin/random/Random") + }.toHashSet() + + val inserter = NonDeterministicBytecodeInserter() + + fun isNonDeterministicStaticFunction(owner: String, name: String, descriptor: String): Boolean { + return nonDeterministicStaticMethods.contains("$owner $name$descriptor") + } + + fun isNonDeterministicClass(clazz: String): Boolean { + return nonDeterministicClasses.contains(clazz) + } + +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicResultStorage.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicResultStorage.kt new file mode 100644 index 0000000000..da612f63cc --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/ndd/NonDeterministicResultStorage.kt @@ -0,0 +1,239 @@ +@file:Suppress("UNUSED") + +package org.utbot.instrumentation.instrumentation.execution.ndd + +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.utContext +import java.util.* + + +object NonDeterministicResultStorage { + + data class NDMethodResult(val signature: String, val result: Any?) + data class NDInstanceInfo(val instanceNumber: Int, val callSite: String) + + private var currentInstance: Any? = null + private val parameters: MutableList = mutableListOf() + + val staticStorage: MutableList = mutableListOf() + val callStorage: IdentityHashMap> = IdentityHashMap() + val ndInstances: IdentityHashMap = IdentityHashMap() + private var nextInstanceNumber = 1 + + fun clear() { + staticStorage.clear() + callStorage.clear() + ndInstances.clear() + nextInstanceNumber = 1 + } + + fun makeSignature(owner: String, name: String, descriptor: String): String { + return "$owner $name$descriptor" + } + + fun signatureToMethod(signature: String): MethodId? { + val sign = signature.split(' ') + val clazz = utContext.classLoader.loadClass( + sign[0].replace('/', '.') + ).id + return clazz.allMethods.find { it.signature == sign[1] } + } + + @JvmStatic + fun registerInstance(instance: Any, callSite: String) { + ndInstances[instance] = NDInstanceInfo(nextInstanceNumber++, callSite) + } + + @JvmStatic + fun saveInstance(instance: Any) { + currentInstance = instance + } + + // putParameter[type](type) + // peakParameter[type](): type + + @JvmStatic + fun putParameterZ(value: Boolean) { + parameters.add(value) + } + + @JvmStatic + fun peakParameterZ(): Boolean { + return parameters.removeLast() as Boolean + } + + @JvmStatic + fun putParameterB(value: Byte) { + parameters.add(value) + } + + @JvmStatic + fun peakParameterB(): Byte { + return parameters.removeLast() as Byte + } + + @JvmStatic + fun putParameterC(value: Char) { + parameters.add(value) + } + + @JvmStatic + fun peakParameterC(): Char { + return parameters.removeLast() as Char + } + + @JvmStatic + fun putParameterS(value: Short) { + parameters.add(value) + } + + @JvmStatic + fun peakParameterS(): Short { + return parameters.removeLast() as Short + } + + @JvmStatic + fun putParameterI(value: Int) { + parameters.add(value) + } + + @JvmStatic + fun peakParameterI(): Int { + return parameters.removeLast() as Int + } + + @JvmStatic + fun putParameterJ(value: Long) { + parameters.add(value) + } + + @JvmStatic + fun peakParameterJ(): Long { + return parameters.removeLast() as Long + } + + @JvmStatic + fun putParameterF(value: Float) { + parameters.add(value) + } + + @JvmStatic + fun peakParameterF(): Float { + return parameters.removeLast() as Float + } + + @JvmStatic + fun putParameterD(value: Double) { + parameters.add(value) + } + + @JvmStatic + fun peakParameterD(): Double { + return parameters.removeLast() as Double + } + + @JvmStatic + fun putParameterL(value: Any?) { + parameters.add(value) + } + + @JvmStatic + fun peakParameterL(): Any? { + return parameters.removeLast() + } + + // storeStatic(type, sign) + + @JvmStatic + fun storeStatic(result: Boolean, signature: String) { + staticStorage.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeStatic(result: Byte, signature: String) { + staticStorage.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeStatic(result: Char, signature: String) { + staticStorage.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeStatic(result: Short, signature: String) { + staticStorage.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeStatic(result: Int, signature: String) { + staticStorage.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeStatic(result: Long, signature: String) { + staticStorage.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeStatic(result: Float, signature: String) { + staticStorage.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeStatic(result: Double, signature: String) { + staticStorage.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeStatic(result: Any?, signature: String) { + staticStorage.add(NDMethodResult(signature, result)) + } + + // storeCall(type, sign) + + @JvmStatic + fun storeCall(result: Boolean, signature: String) { + callStorage.getOrPut(currentInstance) { mutableListOf() }.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeCall(result: Byte, signature: String) { + callStorage.getOrPut(currentInstance) { mutableListOf() }.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeCall(result: Char, signature: String) { + callStorage.getOrPut(currentInstance) { mutableListOf() }.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeCall(result: Short, signature: String) { + callStorage.getOrPut(currentInstance) { mutableListOf() }.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeCall(result: Int, signature: String) { + callStorage.getOrPut(currentInstance) { mutableListOf() }.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeCall(result: Long, signature: String) { + callStorage.getOrPut(currentInstance) { mutableListOf() }.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeCall(result: Float, signature: String) { + callStorage.getOrPut(currentInstance) { mutableListOf() }.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeCall(result: Double, signature: String) { + callStorage.getOrPut(currentInstance) { mutableListOf() }.add(NDMethodResult(signature, result)) + } + + @JvmStatic + fun storeCall(result: Any?, signature: String) { + callStorage.getOrPut(currentInstance) { mutableListOf() }.add(NDMethodResult(signature, result)) + } +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ExecutionPhase.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ExecutionPhase.kt new file mode 100644 index 0000000000..b9d846b0cf --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ExecutionPhase.kt @@ -0,0 +1,36 @@ +package org.utbot.instrumentation.instrumentation.execution.phases + +import com.jetbrains.rd.util.getLogger +import org.utbot.common.measureTime +import org.utbot.instrumentation.instrumentation.execution.PreliminaryUtConcreteExecutionResult +import org.utbot.rd.loggers.debug + +private val logger = getLogger() + +abstract class ExecutionPhaseException(override val message: String) : Exception() + +// Execution will be stopped, exception will be thrown in engine process +class ExecutionPhaseError(phase: String, override val cause: Throwable) : ExecutionPhaseException(phase) + +// Execution will be stopped, but considered successful, result will be returned +class ExecutionPhaseStop(phase: String, val result: PreliminaryUtConcreteExecutionResult) : ExecutionPhaseException(phase) + +interface ExecutionPhase { + fun wrapError(e: Throwable): ExecutionPhaseException +} + +fun T.start(block: T.() -> R): R = + try { + logger.debug().measureTime({ this.javaClass.simpleName } ) { + this.block() + } + } catch (e: ExecutionPhaseStop) { + throw e + } catch (e: Throwable) { + throw this.wrapError(e) + } + +abstract class ExecutionPhaseFailingOnAnyException : ExecutionPhase { + override fun wrapError(e: Throwable): ExecutionPhaseException = + ExecutionPhaseError(this::class.java.simpleName, e) +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/InvocationPhase.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/InvocationPhase.kt new file mode 100644 index 0000000000..0338d7ad50 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/InvocationPhase.kt @@ -0,0 +1,40 @@ +package org.utbot.instrumentation.instrumentation.execution.phases + +import org.utbot.framework.plugin.api.Coverage +import org.utbot.framework.plugin.api.MissingState +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.framework.plugin.api.UtTimeoutException +import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.instrumentation.execution.PreliminaryUtConcreteExecutionResult + + +/** + * This phase is about invoking user's code using [delegateInstrumentation]. + */ +class InvocationPhase( + private val delegateInstrumentation: Instrumentation> +) : ExecutionPhase { + + override fun wrapError(e: Throwable): ExecutionPhaseException { + val message = this.javaClass.simpleName + return when (e) { + is TimeoutException -> ExecutionPhaseStop( + message, + PreliminaryUtConcreteExecutionResult( + stateAfter = MissingState, + result = UtTimeoutException(e), + coverage = Coverage() + ) + ) + + else -> ExecutionPhaseError(message, e) + } + } + + + fun invoke( + clazz: Class<*>, + methodSignature: String, + params: List, + ): Result<*> = delegateInstrumentation.invoke(clazz, methodSignature, params) +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ModelConstructionPhase.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ModelConstructionPhase.kt new file mode 100644 index 0000000000..0620fd67dc --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ModelConstructionPhase.kt @@ -0,0 +1,170 @@ +package org.utbot.instrumentation.instrumentation.execution.phases + +import org.utbot.common.withAccessibility +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jField +import org.utbot.framework.plugin.api.visible.UtStreamConsumingException +import org.utbot.instrumentation.instrumentation.et.ExplicitThrowInstruction +import org.utbot.instrumentation.instrumentation.et.TraceHandler +import org.utbot.instrumentation.instrumentation.execution.PreliminaryUtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.constructors.StateBeforeAwareIdGenerator +import org.utbot.instrumentation.instrumentation.execution.constructors.UtCompositeModelStrategy +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelWithCompositeOriginConstructor +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructor +import java.security.AccessControlException +import java.util.* + +/** + * This phase of model construction from concrete values. + */ +class ModelConstructionPhase( + private val traceHandler: TraceHandler, + private val utModelWithCompositeOriginConstructorFinder: (ClassId) -> UtModelWithCompositeOriginConstructor?, + private val idGenerator: StateBeforeAwareIdGenerator, +) : ExecutionPhase { + + override fun wrapError(e: Throwable): ExecutionPhaseException { + val message = this.javaClass.simpleName + return when (e) { + is TimeoutException -> ExecutionPhaseStop( + message, + PreliminaryUtConcreteExecutionResult( + stateAfter = MissingState, + result = UtTimeoutException(e), + coverage = Coverage() + ) + ) + + else -> ExecutionPhaseError(message, e) + } + } + + private val constructorConfiguration = ConstructorConfiguration() + private lateinit var constructor: UtModelConstructor + + class ConstructorConfiguration { + lateinit var cache: IdentityHashMap + lateinit var strategy: UtCompositeModelStrategy + var maxDepth: Long = UtModelConstructor.DEFAULT_MAX_DEPTH + } + + fun preconfigureConstructor(block: ConstructorConfiguration.() -> Unit) { + constructorConfiguration.block() + } + + fun configureConstructor(block: ConstructorConfiguration.() -> Unit) { + constructorConfiguration.run { + block() + constructor = UtModelConstructor( + objectToModelCache = cache, + utModelWithCompositeOriginConstructorFinder = utModelWithCompositeOriginConstructorFinder, + compositeModelStrategy = strategy, + idGenerator = idGenerator, + maxDepth = maxDepth, + ) + } + } + + fun mergeInstrumentations( + oldInstrumentations: List, + statics: List, + news: List + ): List = mutableListOf().apply { + val method2Static = statics.associateBy { it.methodId } + val class2New = news.associateBy { it.classId } + + addAll(oldInstrumentations.filterNot { + when (it) { + is UtStaticMethodInstrumentation -> method2Static.contains(it.methodId) + is UtNewInstanceInstrumentation -> class2New.contains(it.classId) + } + }) + addAll(statics) + addAll(news) + } + + fun constructStaticInstrumentation(statics: Map>): List = + statics.map { (method, values) -> + UtStaticMethodInstrumentation(method, values.map { constructor.construct(it, method.returnType) }) + } + + fun constructNewInstrumentation( + news: Map, Set>>, + calls: IdentityHashMap>>, + ): List = news.map { (classId, info) -> + val models = info.first.map { instance -> + constructor.constructMock(instance, classId, calls[instance] ?: emptyMap()) + } + + UtNewInstanceInstrumentation(classId, models, info.second) + } + + fun constructParameters(params: List>): List = + params.map { + constructor.construct(it.value, it.clazz.id) + } + + fun constructStatics( + stateBefore: EnvironmentModels, + staticFields: Map> + ): Map = + staticFields.keys.associateWith { fieldId -> + fieldId.jField.run { + val computedValue = withAccessibility { get(null) } + val knownModel = stateBefore.statics[fieldId] + val knownValue = staticFields[fieldId]?.value + if (knownModel != null && knownValue != null && knownValue == computedValue) { + knownModel + } else { + constructor.construct(computedValue, fieldId.type) + } + } + } + + fun convertToExecutionResult(concreteResult: Result<*>, returnClassId: ClassId): UtExecutionResult { + val result = concreteResult.fold({ + try { + val model = constructor.construct(it, returnClassId) + UtExecutionSuccess(model) + } catch (e: Exception) { + processExceptionDuringModelConstruction(e) + } + }) { + sortOutException(it) + } + return result + } + + private fun sortOutException(exception: Throwable): UtExecutionFailure { + if (exception is TimeoutException) { + return UtTimeoutException(exception) + } + if (exception is AccessControlException || + exception is ExceptionInInitializerError && exception.exception is AccessControlException + ) { + return UtSandboxFailure(exception) + } + // there also can be other cases, when we need to wrap internal exception... I suggest adding them on demand + + val instrs = traceHandler.computeInstructionList() + 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) + } + + } + + private fun processExceptionDuringModelConstruction(e: Exception): UtExecutionResult = + when (e) { + is UtStreamConsumingException -> UtStreamConsumingFailure(e) + else -> throw e + } + +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt new file mode 100644 index 0000000000..4138d4882e --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt @@ -0,0 +1,132 @@ +package org.utbot.instrumentation.instrumentation.execution.phases + +import com.jetbrains.rd.util.getLogger +import org.utbot.common.StopWatch +import org.utbot.common.ThreadBasedExecutor +import org.utbot.framework.plugin.api.Coverage +import org.utbot.framework.plugin.api.MissingState +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.framework.plugin.api.UtSandboxFailure +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.instrumentation.et.TraceHandler +import org.utbot.instrumentation.instrumentation.execution.PreliminaryUtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionData +import org.utbot.instrumentation.instrumentation.execution.constructors.StateBeforeAwareIdGenerator +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import java.security.AccessControlException + +class PhasesController( + private val instrumentationContext: InstrumentationContext, + traceHandler: TraceHandler, + delegateInstrumentation: Instrumentation>, + private val timeout: Long, + idGenerator: StateBeforeAwareIdGenerator, +) { + private var currentlyElapsed = 0L + val valueConstructionPhase = ValueConstructionPhase( + instrumentationContext, + idGenerator, + ) + + val preparationPhase = PreparationPhase(traceHandler) + + val invocationPhase = InvocationPhase(delegateInstrumentation) + + val statisticsCollectionPhase = StatisticsCollectionPhase(traceHandler) + + val modelConstructionPhase = ModelConstructionPhase( + traceHandler = traceHandler, + utModelWithCompositeOriginConstructorFinder = instrumentationContext::findUtModelWithCompositeOriginConstructor, + idGenerator = idGenerator, + ) + + val postprocessingPhase = PostprocessingPhase() + + inline fun computeConcreteExecutionResult(block: PhasesController.() -> PreliminaryUtConcreteExecutionResult): PreliminaryUtConcreteExecutionResult { + try { + return this.block() + } catch (e: ExecutionPhaseStop) { + return e.result + } catch (e: ExecutionPhaseError) { + if (e.cause.cause is AccessControlException) { + return PreliminaryUtConcreteExecutionResult( + stateAfter = MissingState, + result = UtSandboxFailure(e.cause.cause!!), + coverage = Coverage() + ) + } + + throw e + } + } + + companion object { + private val logger = getLogger() + } + + fun executePhaseInTimeout(phase: R, block: R.() -> T): T = phase.start { + val stopWatch = StopWatch() + val context = UtContext(utContext.classLoader, stopWatch) + val timeoutForCurrentPhase = timeout - currentlyElapsed + val executor = ThreadBasedExecutor.threadLocal + val result = executor.invokeWithTimeout(timeout - currentlyElapsed, stopWatch) { + withUtContext(context) { + try { + phase.block() + } finally { + executor.runCleanUpIfTimedOut { + instrumentationContext.onPhaseTimeout(phase) + } + } + } + } ?: throw TimeoutException("Timeout $timeoutForCurrentPhase ms for phase ${phase.javaClass.simpleName} elapsed, controller timeout - $timeout") + + val blockElapsed = stopWatch.get() + currentlyElapsed += blockElapsed + + return@start result.getOrThrow() as T + } + + fun executePhaseWithoutTimeout(phase: R, block: R.() -> T): T = phase.start { + return@start ThreadBasedExecutor.threadLocal.invokeWithoutTimeout { + phase.block() + }.getOrThrow() as T + } + + fun applyPreprocessing(parameters: UtConcreteExecutionData): ConstructedData { + + val constructedData = executePhaseInTimeout(valueConstructionPhase) { + val params = constructParameters(parameters.stateBefore) + val statics = constructStatics(parameters.stateBefore) + + // here static methods and instances are mocked + mock(parameters.instrumentation) + + lastCaughtException?.let { instrumentationContext.handleLastCaughtConstructionException(it) } + + ConstructedData(params, statics, getCache()) + } + + // invariants: + // 1. phase must always complete if started as static reset relies on it + // 2. phase must be fast as there are no incremental changes + postprocessingPhase.setStaticFields(preparationPhase.start { + val result = setStaticFields(constructedData.statics) + resetTrace() + resetND() + result + }) + + return constructedData + } + + fun applyPostprocessing() { + postprocessingPhase.start { + resetStaticFields() + valueConstructionPhase.resetMockMethods() + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PostprocessingPhase.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PostprocessingPhase.kt new file mode 100644 index 0000000000..b94b25fe1d --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PostprocessingPhase.kt @@ -0,0 +1,31 @@ +package org.utbot.instrumentation.instrumentation.execution.phases + +import org.utbot.common.withAccessibility +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.jField + + +/** + * The responsibility of this phase is resetting environment to the initial state. + */ +class PostprocessingPhase : ExecutionPhase { + + private var savedStaticsInstance: Map? = null + + fun setStaticFields(savedStatics: Map) { + savedStaticsInstance = savedStatics + } + + override fun wrapError(e: Throwable): ExecutionPhaseException = ExecutionPhaseError(this.javaClass.simpleName, e) + + fun resetStaticFields() { + savedStaticsInstance?.forEach { (fieldId, value) -> + fieldId.jField.run { + withAccessibility { + set(null, value) + } + } + } + } + +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PreparationPhase.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PreparationPhase.kt new file mode 100644 index 0000000000..a733192fd5 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PreparationPhase.kt @@ -0,0 +1,42 @@ +package org.utbot.instrumentation.instrumentation.execution.phases + +import org.utbot.common.withAccessibility +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.util.jField +import org.utbot.instrumentation.instrumentation.et.TraceHandler +import org.utbot.instrumentation.instrumentation.execution.ndd.NonDeterministicResultStorage + + +/** + * The responsibility of this phase is environment preparation before execution. + */ +class PreparationPhase( + private val traceHandler: TraceHandler +) : ExecutionPhase { + + override fun wrapError(e: Throwable): ExecutionPhaseException = + ExecutionPhaseError(this.javaClass.simpleName, e) + + fun setStaticFields(staticFieldsValues: Map>): Map { + val savedStaticFields = mutableMapOf() + staticFieldsValues.forEach { (fieldId, value) -> + fieldId.jField.run { + withAccessibility { + savedStaticFields[fieldId] = get(null) + set(null, value.value) + } + } + } + return savedStaticFields + } + + fun resetTrace() { + traceHandler.resetTrace() + } + + fun resetND() { + NonDeterministicResultStorage.clear() + } + +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/StatisticsCollectionPhase.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/StatisticsCollectionPhase.kt new file mode 100644 index 0000000000..1d75d32663 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/StatisticsCollectionPhase.kt @@ -0,0 +1,87 @@ +package org.utbot.instrumentation.instrumentation.execution.phases + +import org.objectweb.asm.Type +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.instrumentation.instrumentation.et.EtInstruction +import org.utbot.instrumentation.instrumentation.et.TraceHandler +import org.utbot.instrumentation.instrumentation.execution.PreliminaryUtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.ndd.NonDeterministicResultStorage +import java.util.* + +/** + * This phase is about collection statistics such as coverage. + */ +class StatisticsCollectionPhase( + private val traceHandler: TraceHandler +) : ExecutionPhase { + + override fun wrapError(e: Throwable): ExecutionPhaseException { + val message = this.javaClass.simpleName + return when (e) { + is TimeoutException -> ExecutionPhaseStop( + message, + PreliminaryUtConcreteExecutionResult( + stateAfter = MissingState, + result = UtTimeoutException(e), + coverage = Coverage() + ) + ) + + else -> ExecutionPhaseError(message, e) + } + } + + data class NDResults( + val statics: Map>, + val news: Map, Set>>, + val calls: IdentityHashMap>> + ) + + fun getNonDeterministicResults(): NDResults { + val storage = NonDeterministicResultStorage + + val statics = storage.staticStorage + .groupBy { storage.signatureToMethod(it.signature)!! } + .mapValues { (_, values) -> values.map { it.result } } + + val news = storage.ndInstances.entries + .groupBy { it.key.javaClass.id } + .mapValues { (_, entries) -> + val values = entries.sortedBy { it.value.instanceNumber }.map { it.key } + val callSites = entries.map { + utContext.classLoader.loadClass(it.value.callSite.replace('/', '.')).id + }.toSet() + values to callSites + } + + val calls = storage.callStorage + .mapValuesTo(IdentityHashMap()) { (_, methodResults) -> + methodResults + .groupBy { storage.signatureToMethod(it.signature)!! } + .mapValues { (_, values) -> values.map { it.result } } + } + + return NDResults(statics, news, calls) + } + + fun getCoverage(clazz: Class<*>): Coverage { + return traceHandler + .computeInstructionList() + .toApiCoverage( + traceHandler.processingStorage.getInstructionsCount( + Type.getInternalName(clazz) + ) + ) + } + + /** + * Transforms a list of internal [EtInstruction]s to a list of api [Instruction]s. + */ + private fun List.toApiCoverage(instructionsCount: Long? = null): Coverage = + Coverage( + map { Instruction(it.className, it.methodSignature, it.line, it.id) }, + instructionsCount + ) +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ValueConstructionPhase.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ValueConstructionPhase.kt new file mode 100644 index 0000000000..aa71548fd4 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ValueConstructionPhase.kt @@ -0,0 +1,80 @@ +package org.utbot.instrumentation.instrumentation.execution.phases + +import org.utbot.framework.plugin.api.* +import java.util.IdentityHashMap +import org.utbot.instrumentation.instrumentation.execution.constructors.InstrumentationContextAwareValueConstructor +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import org.utbot.framework.plugin.api.util.isInaccessibleViaReflection +import org.utbot.instrumentation.instrumentation.execution.PreliminaryUtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.constructors.StateBeforeAwareIdGenerator + +typealias ConstructedParameters = List> +typealias ConstructedStatics = Map> +typealias ConstructedCache = IdentityHashMap + +data class ConstructedData( + val params: ConstructedParameters, + val statics: ConstructedStatics, + val cache: ConstructedCache, +) + +/** + * This phase of values instantiation from given models. + */ +class ValueConstructionPhase( + instrumentationContext: InstrumentationContext, + idGenerator: StateBeforeAwareIdGenerator, +) : ExecutionPhase { + + override fun wrapError(e: Throwable): ExecutionPhaseException = ExecutionPhaseStop( + phase = this.javaClass.simpleName, + result = PreliminaryUtConcreteExecutionResult( + stateAfter = MissingState, + result = when(e) { + is TimeoutException -> UtTimeoutException(e) + else -> UtConcreteExecutionProcessedFailure(e) + }, + coverage = Coverage() + ) + ) + + private val constructor = InstrumentationContextAwareValueConstructor( + instrumentationContext, + idGenerator, + ) + val detectedMockingCandidates: Set get() = constructor.detectedMockingCandidates + val lastCaughtException: Throwable? get() = constructor.lastCaughtException + + fun getCache(): ConstructedCache { + return constructor.objectToModelCache + } + + fun constructParameters(state: EnvironmentModels): ConstructedParameters { + val parametersModels = listOfNotNull(state.thisInstance) + state.parameters + return constructor.constructMethodParameters(parametersModels) + } + + fun constructStatics(state: EnvironmentModels): ConstructedStatics = + constructor.constructStatics( + state.statics.filterKeys { !it.isInaccessibleViaReflection } + ) + + fun mock(instrumentations: List) { + mockStaticMethods(instrumentations) + mockNewInstances(instrumentations) + } + + private fun mockStaticMethods(instrumentations: List) { + val staticMethodsInstrumentation = instrumentations.filterIsInstance() + constructor.mockStaticMethods(staticMethodsInstrumentation) + } + + private fun mockNewInstances(instrumentations: List) { + val newInstanceInstrumentation = instrumentations.filterIsInstance() + constructor.mockNewInstances(newInstanceInstrumentation) + } + + fun resetMockMethods() { + constructor.resetMockedMethods() + } +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/instrumenter/Instrumenter.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/instrumenter/Instrumenter.kt index 70ce2a6515..dfb403e0ac 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/instrumenter/Instrumenter.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/instrumenter/Instrumenter.kt @@ -4,26 +4,11 @@ import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter import org.objectweb.asm.Opcodes -import org.objectweb.asm.Type -import org.objectweb.asm.tree.ClassNode -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.instrumentation.Settings import org.utbot.instrumentation.instrumentation.instrumenter.visitors.MethodToProbesVisitor -import org.utbot.instrumentation.instrumentation.instrumenter.visitors.util.AddFieldAdapter -import org.utbot.instrumentation.instrumentation.instrumenter.visitors.util.AddStaticFieldAdapter -import org.utbot.instrumentation.instrumentation.instrumenter.visitors.util.IInstructionVisitor -import org.utbot.instrumentation.instrumentation.instrumenter.visitors.util.InstanceFieldInitializer -import org.utbot.instrumentation.instrumentation.instrumenter.visitors.util.InstructionVisitorAdapter -import org.utbot.instrumentation.instrumentation.instrumenter.visitors.util.StaticFieldInitializer +import org.utbot.instrumentation.instrumentation.instrumenter.visitors.util.* import org.utbot.instrumentation.process.HandlerClassesLoader -import java.io.File import java.io.IOException import java.io.InputStream -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.reflect.KFunction -import kotlin.reflect.jvm.javaMethod // TODO: handle with flags EXPAND_FRAMES, etc. @@ -39,7 +24,7 @@ class Instrumenter(classByteCode: ByteArray, val classLoader: ClassLoader? = nul var classByteCode: ByteArray = classByteCode.clone() private set - constructor(clazz: Class<*>) : this(computeClassBytecode(clazz)) + constructor(clazz: Class<*>) : this(adapter.computeClassBytecode(clazz)) fun visitClass(classVisitorBuilder: ClassVisitorBuilder): T { val reader = ClassReader(classByteCode) @@ -77,68 +62,7 @@ class Instrumenter(classByteCode: ByteArray, val classLoader: ClassLoader? = nul } companion object { - private fun computeClassBytecode(clazz: Class<*>): ByteArray { - val reader = - ClassReader(clazz.classLoader.getResourceAsStream(Type.getInternalName(clazz) + ".class")) - val writer = ClassWriter(reader, 0) - reader.accept(writer, 0) - return writer.toByteArray() - } - - private fun findByteClass(className: String): ClassReader? { - val path = className.replace(".", File.separator) + ".class" - return try { - val classReader = UtContext.currentContext()?.classLoader?.getResourceAsStream(path) - ?.readBytes() - ?.let { ClassReader(it) } - ?: ClassReader(className) - classReader - } catch (e: IOException) { - //TODO: SAT-1222 - null - } - } - - // TODO: move the following methods to another file - private fun computeSourceFileName(className: String): String? { - val classReader = findByteClass(className) - val sourceFileAdapter = ClassNode(Settings.ASM_API) - classReader?.accept(sourceFileAdapter, 0) - return sourceFileAdapter.sourceFile - } - - fun computeSourceFileName(clazz: Class<*>): String? { - return computeSourceFileName(clazz.name) - } - - fun computeSourceFileByMethod(method: KFunction<*>, directoryToSearchRecursively: Path = Paths.get("")): File? = - method.javaMethod?.declaringClass?.let { - computeSourceFileByClass(it, directoryToSearchRecursively) - } - - fun computeSourceFileByClass( - className: String, - packageName: String?, - directoryToSearchRecursively: Path = Paths.get("") - ): File? { - val sourceFileName = computeSourceFileName(className) ?: return null - val files = - Files.walk(directoryToSearchRecursively).filter { it.toFile().isFile && it.endsWith(sourceFileName) } - var fileWithoutPackage: File? = null - val pathWithPackage = packageName?.let { Paths.get(it, sourceFileName) } - for (f in files) { - if (pathWithPackage == null || f.endsWith(pathWithPackage)) { - return f.toFile() - } - fileWithoutPackage = f.toFile() - } - return fileWithoutPackage - } - - fun computeSourceFileByClass(clazz: Class<*>, directoryToSearchRecursively: Path = Paths.get("")): File? { - val packageName = clazz.`package`?.name?.replace('.', File.separatorChar) - return computeSourceFileByClass(clazz.name, packageName, directoryToSearchRecursively) - } + var adapter = InstrumenterAdapter() } } diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/instrumenter/InstrumenterAdapter.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/instrumenter/InstrumenterAdapter.kt new file mode 100644 index 0000000000..1a8ff6cba3 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/instrumenter/InstrumenterAdapter.kt @@ -0,0 +1,76 @@ +package org.utbot.instrumentation.instrumentation.instrumenter + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Type +import org.objectweb.asm.tree.ClassNode +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.instrumentation.Settings +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.reflect.KFunction +import kotlin.reflect.jvm.javaMethod + + +open class InstrumenterAdapter { + fun computeClassBytecode(clazz: Class<*>): ByteArray { + val reader = ClassReader(clazz.classLoader.getResourceAsStream(Type.getInternalName(clazz) + ".class")) + val writer = ClassWriter(reader, 0) + reader.accept(writer, 0) + return writer.toByteArray() + } + + private fun findByteClass(className: String): ClassReader? { + val path = className.replace(".", File.separator) + ".class" + return try { + val classReader = UtContext.currentContext()?.classLoader?.getResourceAsStream(path)?.readBytes() + ?.let { ClassReader(it) } ?: ClassReader(className) + classReader + } catch (e: IOException) { + //TODO: SAT-1222 + null + } + } + + // TODO: move the following methods to another file + private fun computeSourceFileName(className: String): String? { + val classReader = findByteClass(className) + val sourceFileAdapter = ClassNode(Settings.ASM_API) + classReader?.accept(sourceFileAdapter, 0) + return sourceFileAdapter.sourceFile + } + + fun computeSourceFileName(clazz: Class<*>): String? { + return computeSourceFileName(clazz.name) + } + + fun computeSourceFileByMethod(method: KFunction<*>, directoryToSearchRecursively: Path = Paths.get("")): File? = + method.javaMethod?.declaringClass?.let { + computeSourceFileByClass(it, directoryToSearchRecursively) + } + + fun computeSourceFileByNameAndPackage( + className: String, packageName: String?, directoryToSearchRecursively: Path + ): File? { + val sourceFileName = computeSourceFileName(className) ?: return null + val files = + Files.walk(directoryToSearchRecursively).filter { it.toFile().isFile && it.endsWith(sourceFileName) } + var fileWithoutPackage: File? = null + val pathWithPackage = packageName?.let { Paths.get(it, sourceFileName) } + for (f in files) { + if (pathWithPackage == null || f.endsWith(pathWithPackage)) { + return f.toFile() + } + fileWithoutPackage = f.toFile() + } + return fileWithoutPackage + } + + open fun computeSourceFileByClass(clazz: Class<*>, directoryToSearchRecursively: Path = Paths.get("")): File? { + val packageName = clazz.`package`?.name?.replace('.', File.separatorChar) + return computeSourceFileByNameAndPackage(clazz.name, packageName, directoryToSearchRecursively) + } +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/mock/MockClassVisitor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/mock/MockClassVisitor.kt index 37dceb7efe..72f4e560a3 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/mock/MockClassVisitor.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/mock/MockClassVisitor.kt @@ -10,11 +10,21 @@ import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.commons.AdviceAdapter import org.objectweb.asm.commons.Method.getMethod +import org.utbot.framework.plugin.api.util.signature object MockConfig { const val IS_MOCK_FIELD = "\$__is_mock_" } +/** + * Computes key for method that is used for mocking. + */ +fun computeKeyForMethod(internalType: String, methodSignature: String) = + "$internalType@$methodSignature" + +fun computeKeyForMethod(method: Method) = + computeKeyForMethod(Type.getInternalName(method.declaringClass), method.signature) + class MockClassVisitor( classVisitor: ClassVisitor, mockGetter: Method, @@ -55,8 +65,8 @@ class MockClassVisitor( exceptions: Array? ): MethodVisitor { val isNotSynthetic = access.and(Opcodes.ACC_SYNTHETIC) == 0 - // we do not want to mock or or synthetic methods - return if (name != "" && name != "" && isNotSynthetic) { + // we do not want to mock or synthetic methods + return if (name != "" && isNotSynthetic) { visitStaticMethod(access, name, descriptor, signature, exceptions) } else { cv.visitMethod(access, name, descriptor, signature, exceptions) @@ -73,7 +83,7 @@ class MockClassVisitor( val isStatic = access and Opcodes.ACC_STATIC != 0 val isVoidMethod = Type.getReturnType(descriptor) == Type.VOID_TYPE - val computedSignature = name + descriptor + val computedSignature = computeKeyForMethod(internalClassName, "$name$descriptor") val id = signatureToId.size signatureToId[computedSignature] = id diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringInstrumentationContext.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringInstrumentationContext.kt new file mode 100644 index 0000000000..2e23eb37ff --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringInstrumentationContext.kt @@ -0,0 +1,61 @@ +package org.utbot.instrumentation.instrumentation.spring + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.SpringSettings.* +import org.utbot.framework.plugin.api.SpringConfiguration.* +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtSpringContextModel +import org.utbot.framework.plugin.api.UtSpringEntityManagerModel +import org.utbot.framework.plugin.api.util.SpringModelUtils.resultActionsClassId +import org.utbot.framework.plugin.api.util.isSubtypeOf +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelWithCompositeOriginConstructor +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhase +import org.utbot.spring.api.SpringApi +import org.utbot.spring.api.provider.SpringApiProviderFacade +import org.utbot.spring.api.provider.InstantiationSettings + +class SpringInstrumentationContext( + private val springSettings: PresentSpringSettings, + private val delegateInstrumentationContext: InstrumentationContext, +) : InstrumentationContext by delegateInstrumentationContext { + // TODO: recreate context/app every time whenever we change method under test + val springApiProviderResult: SpringApiProviderFacade.ProviderResult by lazy { + val classLoader = utContext.classLoader + Thread.currentThread().contextClassLoader = classLoader + + val instantiationSettings = InstantiationSettings( + configurationClasses = arrayOf( + // TODO: for now we prohibit generating integration tests with XML configuration supplied, + // so we expect JavaConfigurations only. + // After fix rewrite the following. + classLoader.loadClass( + (springSettings.configuration as? JavaBasedConfiguration)?.configBinaryName + ?: error("JavaConfiguration was expected, but ${springSettings.configuration.javaClass.name} was provided.") + ) + ), + profiles = springSettings.profiles.toTypedArray(), + ) + + SpringApiProviderFacade + .getInstance(classLoader) + .provideMostSpecificAvailableApi(instantiationSettings) + } + + val springApi get() = springApiProviderResult.result.getOrThrow() + + override fun constructContextDependentValue(model: UtModel): UtConcreteValue<*>? = when (model) { + is UtSpringContextModel -> UtConcreteValue(springApi.getOrLoadSpringApplicationContext()) + is UtSpringEntityManagerModel -> UtConcreteValue(springApi.getEntityManager()) + else -> delegateInstrumentationContext.constructContextDependentValue(model) + } + + override fun findUtModelWithCompositeOriginConstructor(classId: ClassId): UtModelWithCompositeOriginConstructor? = + if (classId.isSubtypeOf(resultActionsClassId)) UtMockMvcResultActionsModelConstructor() + else delegateInstrumentationContext.findUtModelWithCompositeOriginConstructor(classId) + + override fun onPhaseTimeout(timedOutedPhase: ExecutionPhase) = + springApi.afterTestMethod() +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringUtExecutionInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringUtExecutionInstrumentation.kt new file mode 100644 index 0000000000..ea61498c73 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringUtExecutionInstrumentation.kt @@ -0,0 +1,240 @@ +package org.utbot.instrumentation.instrumentation.spring + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.utbot.common.JarUtils +import org.utbot.common.hasOnClasspath +import org.utbot.framework.plugin.api.BeanDefinitionData +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.ConcreteContextLoadingResult +import org.utbot.framework.plugin.api.Coverage +import org.utbot.framework.plugin.api.MissingState +import org.utbot.framework.plugin.api.SpringRepositoryId +import org.utbot.framework.plugin.api.SpringSettings.* +import org.utbot.framework.plugin.api.UtConcreteExecutionProcessedFailure +import org.utbot.framework.plugin.api.isNull +import org.utbot.framework.plugin.api.util.SpringModelUtils.mockMvcPerformMethodId +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.process.kryo.KryoHelper +import org.utbot.instrumentation.instrumentation.ArgumentList +import org.utbot.instrumentation.instrumentation.execution.PreliminaryUtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionData +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhaseFailingOnAnyException +import org.utbot.instrumentation.instrumentation.execution.phases.PhasesController +import org.utbot.instrumentation.process.generated.GetSpringRepositoriesResult +import org.utbot.instrumentation.process.generated.InstrumentedProcessModel +import org.utbot.instrumentation.process.generated.TryLoadingSpringContextResult +import org.utbot.rd.IdleWatchdog +import org.utbot.spring.api.SpringApi +import java.io.File +import java.net.URL +import java.net.URLClassLoader +import java.security.ProtectionDomain + +/** + * UtExecutionInstrumentation wrapper that is aware of Spring configuration and profiles and initialises Spring context + */ +class SpringUtExecutionInstrumentation( + instrumentationContext: InstrumentationContext, + delegateInstrumentationFactory: UtExecutionInstrumentation.Factory<*>, + springSettings: PresentSpringSettings, + private val beanDefinitions: List, + buildDirs: Array, +) : UtExecutionInstrumentation { + private val instrumentationContext = SpringInstrumentationContext(springSettings, instrumentationContext) + private val delegateInstrumentation = delegateInstrumentationFactory.create(this.instrumentationContext) + private val userSourcesClassLoader = URLClassLoader(buildDirs, null) + + private val relatedBeansCache = mutableMapOf, Set>() + private val nonRepositoryRelatedBeansCache = mutableMapOf, Set>() + private val repositoryDescriptionsCache = mutableMapOf, Set>() + + private val springApi: SpringApi get() = instrumentationContext.springApi + + private object SpringBeforeTestMethodPhase : ExecutionPhaseFailingOnAnyException() + private object SpringAfterTestMethodPhase : ExecutionPhaseFailingOnAnyException() + + companion object { + private val logger = getLogger() + private const val SPRING_COMMONS_JAR_FILENAME = "utbot-spring-commons-shadow.jar" + + val springCommonsJar: File by lazy { + JarUtils.extractJarFileFromResources( + jarFileName = SPRING_COMMONS_JAR_FILENAME, + jarResourcePath = "lib/$SPRING_COMMONS_JAR_FILENAME", + targetDirectoryName = "spring-commons" + ) + } + } + + private fun tryLoadingSpringContext(): ConcreteContextLoadingResult { + val apiProviderResult = instrumentationContext.springApiProviderResult + return ConcreteContextLoadingResult( + contextLoaded = apiProviderResult.result.isSuccess, + exceptions = apiProviderResult.exceptions + ) + } + + override fun invoke( + clazz: Class<*>, + methodSignature: String, + arguments: ArgumentList, + parameters: Any?, + phasesWrapper: PhasesController.(invokeBasePhases: () -> PreliminaryUtConcreteExecutionResult) -> PreliminaryUtConcreteExecutionResult + ): UtConcreteExecutionResult = synchronized(this) { + if (parameters !is UtConcreteExecutionData) { + throw IllegalArgumentException("Argument parameters must be of type UtConcreteExecutionData, but was: ${parameters?.javaClass}") + } + + if (parameters.isRerun) + springApi.resetContext() + + // `RemovingConstructFailsUtExecutionInstrumentation` may detect that we fail to + // construct `RequestBuilder` and use `requestBuilder = null`, leading to a nonsensical + // test `mockMvc.perform((RequestBuilder) null)`, which we should discard + if (parameters.stateBefore.executableToCall == mockMvcPerformMethodId && parameters.stateBefore.parameters.single().isNull()) + return UtConcreteExecutionResult( + stateBefore = parameters.stateBefore, + stateAfter = MissingState, + result = UtConcreteExecutionProcessedFailure(IllegalStateException("requestBuilder can't be null")), + coverage = Coverage(), + detectedMockingCandidates = emptySet() + ) + + return try { + delegateInstrumentation.invoke(clazz, methodSignature, arguments, parameters) { invokeBasePhases -> + if (!parameters.isRerun) + modelConstructionPhase.preconfigureConstructor { maxDepth = 0 } + + phasesWrapper { + // NB! beforeTestMethod() and afterTestMethod() are intentionally called inside phases, + // so they are executed in one thread with method under test + // NB! beforeTestMethod() and afterTestMethod() are executed without timeout, because: + // - if the invokeBasePhases() times out, we still want to execute afterTestMethod() + // - first call to beforeTestMethod() can take significant amount of time due to class loading & transformation + executePhaseWithoutTimeout(SpringBeforeTestMethodPhase) { springApi.beforeTestMethod() } + try { + invokeBasePhases() + } finally { + executePhaseWithoutTimeout(SpringAfterTestMethodPhase) { springApi.afterTestMethod() } + } + } + } + } finally { + getNonRepositoryRelevantBeans(clazz).forEach { beanName -> springApi.resetBean(beanName) } + } + } + + override fun getStaticField(fieldId: FieldId): Result<*> = delegateInstrumentation.getStaticField(fieldId) + + private fun getRelevantBeans(clazz: Class<*>): Set = relatedBeansCache.getOrPut(clazz) { + beanDefinitions + .filter { it.beanTypeName == clazz.name } + // forces `getBean()` to load Spring classes, + // otherwise execution of method under test may fail with timeout + .onEach { springApi.getBean(it.beanName) } + .flatMap { springApi.getDependenciesForBean(it.beanName, userSourcesClassLoader) } + .toSet() + .also { logger.info { "Detected relevant beans for class ${clazz.name}: $it" } } + } + + private fun getNonRepositoryRelevantBeans(clazz: Class<*>): Set = nonRepositoryRelatedBeansCache.getOrPut(clazz) { + getRelevantBeans(clazz).subtract(getRepositoryDescriptions(clazz).map { it.repositoryBeanName }.toSet()) + } + + private fun getRepositoryDescriptions(clazz: Class<*>): Set = repositoryDescriptionsCache.getOrPut(clazz) { + val relevantBeanNames = getRelevantBeans(clazz) + val repositoryDescriptions = springApi.resolveRepositories(relevantBeanNames.toSet(), userSourcesClassLoader) + return repositoryDescriptions.map { repositoryDescription -> + SpringRepositoryId( + repositoryDescription.beanName, + ClassId(repositoryDescription.repositoryName), + ClassId(repositoryDescription.entityName), + ) + }.toSet() + } + + override fun transform( + loader: ClassLoader?, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain, + classfileBuffer: ByteArray + ): ByteArray? { + // always transform `MockMvc` to avoid empty coverage when testing controllers + val isMockMvc = className == "org/springframework/test/web/servlet/MockMvc" + + // we do not transform Spring classes as it takes too much time + + // maybe we should still transform classes related to data validation + // (e.g. from packages "javax/persistence" and "jakarta/persistence"), + // since traces from such classes can be particularly useful for feedback to fuzzer + return if (isMockMvc || userSourcesClassLoader.hasOnClasspath(className.replace("/", "."))) { + delegateInstrumentation.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer) + } else { + null + } + } + + override fun InstrumentedProcessModel.setupAdditionalRdResponses(kryoHelper: KryoHelper, watchdog: IdleWatchdog) { + watchdog.measureTimeForActiveCall(getRelevantSpringRepositories, "Getting Spring repositories") { params -> + val classId: ClassId = kryoHelper.readObject(params.classId) + val repositoryDescriptions = getRepositoryDescriptions(classId.jClass) + GetSpringRepositoriesResult(kryoHelper.writeObject(repositoryDescriptions)) + } + watchdog.measureTimeForActiveCall(tryLoadingSpringContext, "Trying to load Spring application context") { params -> + val contextLoadingResult = tryLoadingSpringContext() + TryLoadingSpringContextResult(kryoHelper.writeObject(contextLoadingResult)) + } + delegateInstrumentation.run { setupAdditionalRdResponses(kryoHelper, watchdog) } + } + + class Factory( + private val delegateInstrumentationFactory: UtExecutionInstrumentation.Factory<*>, + private val springSettings: PresentSpringSettings, + private val beanDefinitions: List, + private val buildDirs: Array, + ) : UtExecutionInstrumentation.Factory { + override val additionalRuntimeClasspath: Set + get() = super.additionalRuntimeClasspath + springCommonsJar.path + + // TODO may be we can use some alternative sandbox that has more permissions + // (at the very least we need `ReflectPermission("suppressAccessChecks")` + // to let Jackson work with private fields when `@RequestBody` is used) + override val forceDisableSandbox: Boolean + get() = true + + override fun create(instrumentationContext: InstrumentationContext): SpringUtExecutionInstrumentation = + SpringUtExecutionInstrumentation( + instrumentationContext, + delegateInstrumentationFactory, + springSettings, + beanDefinitions, + buildDirs + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Factory + + if (delegateInstrumentationFactory != other.delegateInstrumentationFactory) return false + if (springSettings != other.springSettings) return false + if (beanDefinitions != other.beanDefinitions) return false + return buildDirs.contentEquals(other.buildDirs) + } + + override fun hashCode(): Int { + var result = delegateInstrumentationFactory.hashCode() + result = 31 * result + springSettings.hashCode() + result = 31 * result + beanDefinitions.hashCode() + result = 31 * result + buildDirs.contentHashCode() + return result + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/UtMockMvcResultActionsModelConstructor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/UtMockMvcResultActionsModelConstructor.kt new file mode 100644 index 0000000000..05f307a373 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/UtMockMvcResultActionsModelConstructor.kt @@ -0,0 +1,43 @@ +package org.utbot.instrumentation.instrumentation.spring + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtSpringMockMvcResultActionsModel +import org.utbot.framework.plugin.api.util.SpringModelUtils.modelAndViewGetModelMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.modelAndViewGetViewNameMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.mvcResultGetModelAndViewMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.mvcResultGetResponseMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.responseGetContentAsStringMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.responseGetErrorMessageMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.responseGetStatusMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.resultActionsAndReturnMethodId +import org.utbot.framework.plugin.api.util.mapClassId +import org.utbot.framework.plugin.api.util.method +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelWithCompositeOriginConstructor +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructorInterface + +class UtMockMvcResultActionsModelConstructor : UtModelWithCompositeOriginConstructor { + override fun constructModelWithCompositeOrigin( + internalConstructor: UtModelConstructorInterface, + value: Any, + valueClassId: ClassId, + id: Int?, + saveToCache: (UtModel) -> Unit + ): UtSpringMockMvcResultActionsModel { + val mvcResult = resultActionsAndReturnMethodId.method.invoke(value) + val response = mvcResultGetResponseMethodId.method.invoke(mvcResult) + val modelAndView = mvcResultGetModelAndViewMethodId.method.invoke(mvcResult) + + return UtSpringMockMvcResultActionsModel( + id = id, + origin = null, // replace with actual origin if needed + status = responseGetStatusMethodId.method.invoke(response) as Int, + errorMessage = responseGetErrorMessageMethodId.method.invoke(response) as String?, + contentAsString = responseGetContentAsStringMethodId.method.invoke(response) as String, + viewName = modelAndView?.let { modelAndViewGetViewNameMethodId.method.invoke(modelAndView) } as String?, + model = modelAndView?.let { modelAndViewGetModelMethodId.method.invoke(modelAndView) }?.let { + internalConstructor.construct((it as Map<*, *>).toMap(), mapClassId) + } + ).also(saveToCache) + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/BytecodeTransformation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/BytecodeTransformation.kt new file mode 100644 index 0000000000..92d38616ae --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/BytecodeTransformation.kt @@ -0,0 +1,44 @@ +package org.utbot.instrumentation.instrumentation.transformation + +import org.utbot.framework.plugin.api.FieldId +import org.utbot.instrumentation.instrumentation.ArgumentList +import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.instrumentation.InvokeInstrumentation +import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter +import java.security.ProtectionDomain + +/** + * This instrumentation transforms bytecode and delegates invoking a given function to [InvokeInstrumentation]. + */ +class BytecodeTransformation : Instrumentation> { + private val invokeInstrumentation = InvokeInstrumentation() + + override fun invoke( + clazz: Class<*>, + methodSignature: String, + arguments: ArgumentList, + parameters: Any? + ): Result<*> = invokeInstrumentation.invoke(clazz, methodSignature, arguments, parameters) + + override fun getStaticField(fieldId: FieldId): Result<*> = invokeInstrumentation.getStaticField(fieldId) + + override fun transform( + loader: ClassLoader?, + className: String?, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain?, + classfileBuffer: ByteArray + ): ByteArray { + val instrumenter = Instrumenter(classfileBuffer, loader) + + instrumenter.visitClass { writer -> + BytecodeTransformer(writer) + } + + return instrumenter.classByteCode + } + + object Factory : Instrumentation.Factory, BytecodeTransformation> { + override fun create(): BytecodeTransformation = BytecodeTransformation() + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/BytecodeTransformer.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/BytecodeTransformer.kt new file mode 100644 index 0000000000..6cdf1295df --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/BytecodeTransformer.kt @@ -0,0 +1,23 @@ +package org.utbot.instrumentation.instrumentation.transformation + +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor +import org.utbot.instrumentation.Settings +import org.utbot.instrumentation.instrumentation.transformation.adapters.StringMethodsAdapter + +/** + * Main class for the transformation. + * Bytecode transformations will be combined in this class. + */ +class BytecodeTransformer(classVisitor: ClassVisitor) : ClassVisitor(Settings.ASM_API, classVisitor) { + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array? + ): MethodVisitor { + val methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions) + return StringMethodsAdapter(api, access, descriptor, methodVisitor) + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/adapters/PatternMethodAdapter.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/adapters/PatternMethodAdapter.kt new file mode 100644 index 0000000000..d35bf0d5a2 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/adapters/PatternMethodAdapter.kt @@ -0,0 +1,117 @@ +package org.utbot.instrumentation.instrumentation.transformation.adapters + +import org.objectweb.asm.Handle +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.commons.LocalVariablesSorter + + +/** + * Abstract class for the pattern method adapters. + * + * The idea of stateful bytecode transformation was described in the [ASM library user guide](https://asm.ow2.io/asm4-guide.pdf) in chapter 3.2.5. + */ +abstract class PatternMethodAdapter( + api: Int, + access: Int, + descriptor: String?, + methodVisitor: MethodVisitor +) : LocalVariablesSorter(api, access, descriptor, methodVisitor) { + protected abstract fun resetState() + + override fun visitFrame(type: Int, numLocal: Int, local: Array?, numStack: Int, stack: Array?) { + resetState() + mv.visitFrame(type, numLocal, local, numStack, stack) + } + + override fun visitInsn(opcode: Int) { + resetState() + mv.visitInsn(opcode) + } + + override fun visitIntInsn(opcode: Int, operand: Int) { + resetState() + mv.visitIntInsn(opcode, operand) + } + + override fun visitVarInsn(opcode: Int, localVariable: Int) { + resetState() + mv.visitVarInsn(opcode, localVariable) + } + + override fun visitTypeInsn(opcode: Int, type: String?) { + resetState() + mv.visitTypeInsn(opcode, type) + } + + override fun visitFieldInsn(opcode: Int, owner: String?, name: String?, descriptor: String?) { + resetState() + mv.visitFieldInsn(opcode, owner, name, descriptor) + } + + override fun visitMethodInsn( + opcode: Int, + owner: String?, + name: String?, + descriptor: String?, + isInterface: Boolean + ) { + resetState() + mv.visitMethodInsn(opcode, owner, name, descriptor, isInterface) + } + + override fun visitInvokeDynamicInsn( + name: String?, + descriptor: String?, + bootstrapMethodHandle: Handle?, + vararg bootstrapMethodArguments: Any? + ) { + resetState() + mv.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments) + } + + override fun visitJumpInsn(opcode: Int, label: Label?) { + resetState() + mv.visitJumpInsn(opcode, label) + } + + override fun visitLabel(label: Label?) { + resetState() + mv.visitLabel(label) + } + + override fun visitLdcInsn(value: Any?) { + resetState() + mv.visitLdcInsn(value) + } + + override fun visitIincInsn(`var`: Int, increment: Int) { + resetState() + mv.visitIincInsn(`var`, increment) + } + + override fun visitTableSwitchInsn(min: Int, max: Int, dflt: Label?, vararg labels: Label?) { + resetState() + mv.visitTableSwitchInsn(min, max, dflt, *labels) + } + + override fun visitLookupSwitchInsn(dflt: Label?, keys: IntArray?, labels: Array?) { + resetState() + mv.visitLookupSwitchInsn(dflt, keys, labels) + } + + override fun visitMultiANewArrayInsn(descriptor: String?, numDimensions: Int) { + resetState() + mv.visitMultiANewArrayInsn(descriptor, numDimensions) + } + + override fun visitTryCatchBlock(start: Label?, end: Label?, handler: Label?, type: String?) { + resetState() + mv.visitTryCatchBlock(start, end, handler, type) + } + + override fun visitMaxs(maxStack: Int, maxLocals: Int) { + resetState() + mv.visitMaxs(maxStack, maxLocals) + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/adapters/StringMethodsAdapter.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/adapters/StringMethodsAdapter.kt new file mode 100644 index 0000000000..b73e9d552f --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/transformation/adapters/StringMethodsAdapter.kt @@ -0,0 +1,191 @@ +package org.utbot.instrumentation.instrumentation.transformation.adapters + +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import kotlin.properties.Delegates + +/** + * Class for transforming an if statement with call of the [String.equals], [String.startsWith] or [String.endsWith] method + * with a constant string into a sequence of comparisons of each char of the string with each char of the constant string. + */ +class StringMethodsAdapter( + api: Int, + access: Int, + descriptor: String?, + methodVisitor: MethodVisitor +) : PatternMethodAdapter(api, access, descriptor, methodVisitor) { + private enum class State { + SEEN_NOTHING, + SEEN_ALOAD, + SEEN_LDC_STRING_CONST, + SEEN_INVOKEVIRTUAL_STRING_EQUALS, + SEEN_INVOKEVIRTUAL_STRING_STARTSWITH, + SEEN_INVOKEVIRTUAL_STRING_ENDSWITH + } + + private var state: State = State.SEEN_NOTHING + + private var indexOfLocalVariable by Delegates.notNull() + private lateinit var constString: String + + override fun resetState() { + when (state) { + State.SEEN_ALOAD -> mv.visitVarInsn(Opcodes.ALOAD, indexOfLocalVariable) + State.SEEN_LDC_STRING_CONST -> { + mv.visitVarInsn(Opcodes.ALOAD, indexOfLocalVariable) + mv.visitLdcInsn(constString) + } + + State.SEEN_INVOKEVIRTUAL_STRING_EQUALS -> { + mv.visitVarInsn(Opcodes.ALOAD, indexOfLocalVariable) + mv.visitLdcInsn(constString) + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false) + } + + State.SEEN_INVOKEVIRTUAL_STRING_STARTSWITH -> { + mv.visitVarInsn(Opcodes.ALOAD, indexOfLocalVariable) + mv.visitLdcInsn(constString) + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/lang/String", + "startsWith", + "(Ljava/lang/String;)Z", + false + ) + } + + State.SEEN_INVOKEVIRTUAL_STRING_ENDSWITH -> { + mv.visitVarInsn(Opcodes.ALOAD, indexOfLocalVariable) + mv.visitLdcInsn(constString) + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/lang/String", + "endsWith", + "(Ljava/lang/String;)Z", + false + ) + } + + else -> {} + } + state = State.SEEN_NOTHING + } + + override fun visitVarInsn(opcode: Int, localVariable: Int) { + if (state == State.SEEN_NOTHING && opcode == Opcodes.ALOAD) { + state = State.SEEN_ALOAD + indexOfLocalVariable = localVariable + return + } + resetState() + if (opcode == Opcodes.ALOAD) { + state = State.SEEN_ALOAD + indexOfLocalVariable = localVariable + return + } + mv.visitVarInsn(opcode, localVariable) + } + + override fun visitMethodInsn( + opcode: Int, + owner: String?, + name: String?, + descriptor: String?, + isInterface: Boolean + ) { + if (state == State.SEEN_LDC_STRING_CONST && !isInterface && opcode == Opcodes.INVOKEVIRTUAL && owner == "java/lang/String") { + when { + name == "equals" && descriptor == "(Ljava/lang/Object;)Z" -> { + state = State.SEEN_INVOKEVIRTUAL_STRING_EQUALS + return + } + + name == "startsWith" && descriptor == "(Ljava/lang/String;)Z" -> { + state = State.SEEN_INVOKEVIRTUAL_STRING_STARTSWITH + return + } + + name == "endsWith" && descriptor == "(Ljava/lang/String;)Z" -> { + state = State.SEEN_INVOKEVIRTUAL_STRING_ENDSWITH + return + } + } + } + resetState() + mv.visitMethodInsn(opcode, owner, name, descriptor, isInterface) + } + + override fun visitLdcInsn(value: Any?) { + if (state == State.SEEN_ALOAD && value is String) { + state = State.SEEN_LDC_STRING_CONST + constString = value + return + } + resetState() + mv.visitLdcInsn(value) + } + + override fun visitJumpInsn(opcode: Int, label: Label?) { + if (setOf( + State.SEEN_INVOKEVIRTUAL_STRING_EQUALS, + State.SEEN_INVOKEVIRTUAL_STRING_STARTSWITH, + State.SEEN_INVOKEVIRTUAL_STRING_ENDSWITH + ).any { state == it } && opcode == Opcodes.IFEQ + ) { + // code transformation + // if (str.length() == constString.length()) for equals method + // if (str.length() >= constString.length()) for startsWith and endsWith methods + mv.visitVarInsn(Opcodes.ALOAD, indexOfLocalVariable) + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "length", "()I", false) + mv.visitIntInsn(Opcodes.BIPUSH, constString.length) + if (state == State.SEEN_INVOKEVIRTUAL_STRING_EQUALS) { + mv.visitJumpInsn(Opcodes.IF_ICMPNE, label) + } else { + mv.visitJumpInsn(Opcodes.IF_ICMPLT, label) + } + + if (constString.isEmpty()) { + state = State.SEEN_NOTHING + return + } + + if (state == State.SEEN_INVOKEVIRTUAL_STRING_ENDSWITH) { + // int length = str.length() + mv.visitLabel(Label()) + mv.visitVarInsn(Opcodes.ALOAD, indexOfLocalVariable) + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "length", "()I", false) + val length = newLocal(Type.INT_TYPE) + mv.visitVarInsn(Opcodes.ISTORE, length) + + // reverse constant string to compare chars from the end + constString.reversed().forEachIndexed { index, c -> + // if (str.charAt(length - (index + 1)) == c) + mv.visitLabel(Label()) + mv.visitVarInsn(Opcodes.ALOAD, indexOfLocalVariable) + mv.visitVarInsn(Opcodes.ILOAD, length) + mv.visitIntInsn(Opcodes.BIPUSH, index + 1) + mv.visitInsn(Opcodes.ISUB) + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "charAt", "(I)C", false) + mv.visitIntInsn(Opcodes.BIPUSH, c.code) + mv.visitJumpInsn(Opcodes.IF_ICMPNE, label) + } + } else { + constString.forEachIndexed { index, c -> + // if (str.charAt(index) == c) + mv.visitLabel(Label()) + mv.visitVarInsn(Opcodes.ALOAD, indexOfLocalVariable) + mv.visitIntInsn(Opcodes.BIPUSH, index) + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "charAt", "(I)C", false) + mv.visitIntInsn(Opcodes.BIPUSH, c.code) + mv.visitJumpInsn(Opcodes.IF_ICMPNE, label) + } + } + state = State.SEEN_NOTHING + return + } + resetState() + mv.visitJumpInsn(opcode, label) + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcess.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcess.kt deleted file mode 100644 index e49e207fa2..0000000000 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcess.kt +++ /dev/null @@ -1,162 +0,0 @@ -package org.utbot.instrumentation.process - -import com.jetbrains.rd.util.* -import com.jetbrains.rd.util.lifetime.Lifetime -import kotlinx.coroutines.* -import org.utbot.common.* -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.instrumentation.agent.Agent -import org.utbot.instrumentation.instrumentation.Instrumentation -import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation -import org.utbot.instrumentation.rd.generated.ChildProcessModel -import org.utbot.instrumentation.rd.generated.CollectCoverageResult -import org.utbot.instrumentation.rd.generated.InvokeMethodCommandResult -import org.utbot.instrumentation.rd.generated.childProcessModel -import org.utbot.instrumentation.util.KryoHelper -import org.utbot.rd.CallsSynchronizer -import org.utbot.rd.ClientProtocolBuilder -import org.utbot.rd.findRdPort -import org.utbot.rd.loggers.UtRdConsoleLoggerFactory -import java.io.File -import java.io.OutputStream -import java.io.PrintStream -import java.net.URLClassLoader -import java.security.AllPermission -import kotlin.system.measureTimeMillis -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -/** - * We use this ClassLoader to separate user's classes and our dependency classes. - * Our classes won't be instrumented. - */ -internal object HandlerClassesLoader : URLClassLoader(emptyArray()) { - fun addUrls(urls: Iterable) { - urls.forEach { super.addURL(File(it).toURI().toURL()) } - } - - /** - * System classloader can find org.slf4j thus when we want to mock something from org.slf4j - * we also want this class will be loaded by [HandlerClassesLoader] - */ - override fun loadClass(name: String, resolve: Boolean): Class<*> { - if (name.startsWith("org.slf4j")) { - return (findLoadedClass(name) ?: findClass(name)).apply { - if (resolve) resolveClass(this) - } - } - return super.loadClass(name, resolve) - } -} - -/** - * Command-line option to disable the sandbox - */ -const val DISABLE_SANDBOX_OPTION = "--disable-sandbox" -const val ENABLE_LOGS_OPTION = "--enable-logs" -private val logger = getLogger("ChildProcess") -private val messageFromMainTimeout: Duration = 120.seconds - -fun logLevelArgument(level: LogLevel): String { - return "$ENABLE_LOGS_OPTION=$level" -} - -private fun findLogLevel(args: Array): LogLevel { - val logArgument = args.find{ it.contains(ENABLE_LOGS_OPTION) } ?: return LogLevel.Fatal - - return enumValueOf(logArgument.split("=").last()) -} - -/** - * It should be compiled into separate jar file (child_process.jar) and be run with an agent (agent.jar) option. - */ -fun main(args: Array) = runBlocking { - // We don't want user code to litter the standard output, so we redirect it. - val tmpStream = PrintStream(object : OutputStream() { - override fun write(b: Int) {} - }) - - System.setOut(tmpStream) - - if (!args.contains(DISABLE_SANDBOX_OPTION)) { - permissions { - // Enable all permissions for instrumentation. - // SecurityKt.sandbox() is used to restrict these permissions. - +AllPermission() - } - } - - val logLevel: LogLevel = findLogLevel(args) - Logger.set(Lifetime.Eternal, UtRdConsoleLoggerFactory(logLevel, System.err)) - - val port = findRdPort(args) - - try { - ClientProtocolBuilder().withProtocolTimeout(messageFromMainTimeout).start(port) { - val kryoHelper = KryoHelper(lifetime) - logger.info { "setup started" } - childProcessModel.setup(kryoHelper, it) - logger.info { "setup ended" } - } - } catch (e: Throwable) { - logger.error { "Terminating process because exception occurred: ${e.stackTraceToString()}" } - } - logger.info { "runBlocking ending" } -}.also { - logger.info { "runBlocking ended" } -} - -private lateinit var pathsToUserClasses: Set -private lateinit var pathsToDependencyClasses: Set -private lateinit var instrumentation: Instrumentation<*> - -private fun ChildProcessModel.setup(kryoHelper: KryoHelper, synchronizer: CallsSynchronizer) { - synchronizer.measureExecutionForTermination(warmup) { - logger.debug { "received warmup request" } - val time = measureTimeMillis { - HandlerClassesLoader.scanForClasses("").toList() // here we transform classes - } - logger.debug { "warmup finished in $time ms" } - } - synchronizer.measureExecutionForTermination(invokeMethodCommand) { params -> - logger.debug { "received invokeMethod request: ${params.classname}, ${params.signature}" } - val clazz = HandlerClassesLoader.loadClass(params.classname) - val res = instrumentation.invoke( - clazz, - params.signature, - kryoHelper.readObject(params.arguments), - kryoHelper.readObject(params.parameters) - ) - - logger.debug { "invokeMethod result: $res" } - InvokeMethodCommandResult(kryoHelper.writeObject(res)) - } - synchronizer.measureExecutionForTermination(setInstrumentation) { params -> - logger.debug { "setInstrumentation request" } - instrumentation = kryoHelper.readObject(params.instrumentation) - logger.trace { "instrumentation - ${instrumentation.javaClass.name} " } - Agent.dynamicClassTransformer.transformer = instrumentation // classTransformer is set - Agent.dynamicClassTransformer.addUserPaths(pathsToUserClasses) - instrumentation.init(pathsToUserClasses) - } - synchronizer.measureExecutionForTermination(addPaths) { params -> - logger.debug { "addPaths request" } - pathsToUserClasses = params.pathsToUserClasses.split(File.pathSeparatorChar).toSet() - pathsToDependencyClasses = params.pathsToDependencyClasses.split(File.pathSeparatorChar).toSet() - HandlerClassesLoader.addUrls(pathsToUserClasses) - HandlerClassesLoader.addUrls(pathsToDependencyClasses) - kryoHelper.setKryoClassLoader(HandlerClassesLoader) // Now kryo will use our classloader when it encounters unregistered class. - UtContext.setUtContext(UtContext(HandlerClassesLoader)) - } - synchronizer.measureExecutionForTermination(stopProcess) { - logger.debug { "stop request" } - synchronizer.stopProtocol() - } - synchronizer.measureExecutionForTermination(collectCoverage) { params -> - logger.debug { "collect coverage request" } - val anyClass: Class<*> = kryoHelper.readObject(params.clazz) - logger.debug { "class - ${anyClass.name}" } - val result = (instrumentation as CoverageInstrumentation).collectCoverageInfo(anyClass) - CollectCoverageResult(kryoHelper.writeObject(result)) - } -} \ 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 deleted file mode 100644 index 0a6ac39fa1..0000000000 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcessRunner.kt +++ /dev/null @@ -1,126 +0,0 @@ -package org.utbot.instrumentation.process - -import mu.KotlinLogging -import org.utbot.common.* -import org.utbot.common.scanForResourcesContaining -import org.utbot.common.utBotTempDirectory -import org.utbot.framework.plugin.services.JdkInfoService -import org.utbot.framework.UtSettings -import org.utbot.framework.plugin.services.WorkingDirService -import org.utbot.framework.process.OpenModulesContainer -import org.utbot.instrumentation.Settings -import org.utbot.instrumentation.agent.DynamicClassTransformer -import org.utbot.rd.rdPortArgument -import java.io.File -import kotlin.random.Random - -private val logger = KotlinLogging.logger {} - -class ChildProcessRunner { - private val id = Random.nextLong() - private var processSeqN = 0 - private val cmds: List by lazy { - val debugCmd = listOfNotNull(DEBUG_RUN_CMD.takeIf { Settings.runChildProcessWithDebug }) - val javaVersionSpecificArguments = OpenModulesContainer.javaVersionSpecificArguments - val pathToJava = JdkInfoService.provide().path - - listOf(pathToJava.resolve("bin${File.separatorChar}${osSpecificJavaExecutable()}").toString()) + - debugCmd + - javaVersionSpecificArguments + - listOf("-javaagent:$jarFile", "-ea", "-jar", "$jarFile") - } - - var errorLogFile: File = NULL_FILE - - fun start(rdPort: Int): Process { - val portArgument = rdPortArgument(rdPort) - - logger.debug { "Starting child process: ${cmds.joinToString(" ")} $portArgument" } - processSeqN++ - - if (UtSettings.logConcreteExecutionErrors) { - UT_BOT_TEMP_DIR.mkdirs() - errorLogFile = File(UT_BOT_TEMP_DIR, "${id}-${processSeqN}.log") - } - - val directory = WorkingDirService.provide().toFile() - val commandsWithOptions = buildList { - addAll(cmds) - if (!UtSettings.useSandbox) { - add(DISABLE_SANDBOX_OPTION) - } - if (UtSettings.logConcreteExecutionErrors) { - add(logLevelArgument(UtSettings.childProcessLogLevel)) - } - add(portArgument) - } - - val processBuilder = ProcessBuilder(commandsWithOptions) - .redirectError(errorLogFile) - .directory(directory) - - return processBuilder.start().also { - logger.info { "Process started with PID=${it.getPid}" } - - if (UtSettings.logConcreteExecutionErrors) { - logger.info { "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 = "lib" - - private const val DEBUG_RUN_CMD = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=5006" - - 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. - * - * * In case of failure, searches for utbot-instrumentation jar in the resources and extracts it to the - * temporary directory. This jar file must be placed to the resources by `processResources` gradle task - * in the gradle configuration of the project which depends on utbot-instrumentation module. - */ - private val jarFile: File by lazy { - logger.debug().bracket("Finding $UTBOT_INSTRUMENTATION jar") { - run { - logger.debug("Trying to find jar in the resources.") - val tempDir = utBotTempDirectory.toFile() - val unzippedJarName = "$UTBOT_INSTRUMENTATION-${currentProcessPid}.jar" - val instrumentationJarFile = File(tempDir, unzippedJarName) - - ChildProcessRunner::class.java.classLoader - .firstOrNullResourceIS(INSTRUMENTATION_LIB) { resourcePath -> - resourcePath.contains(UTBOT_INSTRUMENTATION) && resourcePath.endsWith(".jar") - } - ?.use { input -> - instrumentationJarFile.writeBytes(input.readBytes()) - } - ?: return@run null - instrumentationJarFile - } ?: run { - logger.debug("Failed to find jar in the resources. Trying to find it in the classpath.") - ChildProcessRunner::class.java.classLoader - .scanForResourcesContaining(DynamicClassTransformer::class.java.nameOfPackage) - .firstOrNull { - it.absolutePath.contains(UTBOT_INSTRUMENTATION) && it.extension == "jar" - } - } ?: error(""" - Can't find file: $UTBOT_INSTRUMENTATION-.jar. - Make sure you added $UTBOT_INSTRUMENTATION-.jar to the resources folder from gradle. - """.trimIndent()) - } - } - } -} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt new file mode 100644 index 0000000000..12b2f996b3 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt @@ -0,0 +1,161 @@ +package org.utbot.instrumentation.process + +import com.jetbrains.rd.util.* +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.reactive.adviseOnce +import kotlinx.coroutines.* +import org.mockito.Mockito +import org.utbot.common.* +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.process.kryo.KryoHelper +import org.utbot.instrumentation.agent.Agent +import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.process.generated.CollectCoverageResult +import org.utbot.instrumentation.process.generated.InstrumentedProcessModel +import org.utbot.instrumentation.process.generated.InvokeMethodCommandResult +import org.utbot.instrumentation.process.generated.instrumentedProcessModel +import org.utbot.rd.IdleWatchdog +import org.utbot.rd.ClientProtocolBuilder +import org.utbot.rd.RdSettingsContainerFactory +import org.utbot.rd.generated.loggerModel +import org.utbot.rd.generated.settingsModel +import org.utbot.rd.loggers.UtRdRemoteLoggerFactory +import java.io.File +import java.net.URLClassLoader +import java.security.AllPermission +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * We use this ClassLoader to separate user's classes and our dependency classes. + * Our classes won't be instrumented. + */ +internal object HandlerClassesLoader : URLClassLoader(emptyArray()) { + fun addUrls(urls: Iterable) { + urls.forEach { super.addURL(File(it).toURI().toURL()) } + } + + /** + * System classloader can find org.slf4j thus when we want to mock something from org.slf4j + * we also want this class will be loaded by [HandlerClassesLoader] + */ + override fun loadClass(name: String, resolve: Boolean): Class<*> { + if (name.startsWith("org.slf4j")) { + return (findLoadedClass(name) ?: findClass(name)).apply { + if (resolve) resolveClass(this) + } + } + return super.loadClass(name, resolve) + } +} + +/** + * Command-line option to disable the sandbox + */ +const val DISABLE_SANDBOX_OPTION = "--disable-sandbox" +private val logger = getLogger() +private val messageFromMainTimeout: Duration = 120.seconds + +interface DummyForMockitoWarmup { + fun method1() +} + +/** + * Mockito initialization take ~0.5-1 sec, which forces first `invoke` request to timeout + * it is crucial in tests as we start process just for 1-2 such requests + */ +fun warmupMockito() { + try { + Mockito.mock(DummyForMockitoWarmup::class.java) + } catch (e: Throwable) { + logger.warn { "Exception during mockito warmup: ${e.stackTraceToString()}" } + } +} + +@Suppress("unused") +object InstrumentedProcessMain + +/** + * It should be compiled into separate jar file (instrumented_process.jar) and be run with an agent (agent.jar) option. + */ +fun main(args: Array) = runBlocking { + if (!args.contains(DISABLE_SANDBOX_OPTION)) { + permissions { + // Enable all permissions for instrumentation. + // SecurityKt.sandbox() is used to restrict these permissions. + +AllPermission() + } + } + + try { + ClientProtocolBuilder().withProtocolTimeout(messageFromMainTimeout).start(args) { + loggerModel.initRemoteLogging.adviseOnce(lifetime) { + Logger.set(Lifetime.Eternal, UtRdRemoteLoggerFactory(loggerModel)) + this.protocol.scheduler.queue { warmupMockito() } + } + val kryoHelper = KryoHelper(lifetime) + logger.info { "setup started" } + AbstractSettings.setupFactory(RdSettingsContainerFactory(protocol.settingsModel)) + instrumentedProcessModel.setup(kryoHelper, it) + logger.info { "setup ended" } + } + } catch (e: Throwable) { + logger.error { "Terminating process because exception occurred: ${e.stackTraceToString()}" } + } +} + +private lateinit var pathsToUserClasses: Set +private lateinit var instrumentation: Instrumentation<*> + +private var warmupDone = false + +private fun InstrumentedProcessModel.setup(kryoHelper: KryoHelper, watchdog: IdleWatchdog) { + watchdog.measureTimeForActiveCall(warmup, "Classloader warmup request") { + if (!warmupDone) { + HandlerClassesLoader.scanForClasses("").toList() // here we transform classes + warmupDone = true + } else { + logger.info { "warmup already happened" } + } + } + watchdog.measureTimeForActiveCall(invokeMethodCommand, "Invoke method request") { params -> + val clazz = HandlerClassesLoader.loadClass(params.classname) + val res = kotlin.runCatching { + instrumentation.invoke( + clazz, + params.signature, + kryoHelper.readObject(params.arguments), + kryoHelper.readObject(params.parameters) + ) + } + res.fold({ + InvokeMethodCommandResult(kryoHelper.writeObject(it)) + }) { + throw it + } + } + watchdog.measureTimeForActiveCall(setInstrumentation, "Instrumentation setup") { params -> + logger.debug { "setInstrumentation request" } + val instrumentationFactory = kryoHelper.readObject>(params.instrumentation) + HandlerClassesLoader.addUrls(instrumentationFactory.additionalRuntimeClasspath) + instrumentation = instrumentationFactory.create() + logger.debug { "instrumentation - ${instrumentation.javaClass.name} " } + Agent.dynamicClassTransformer.useBytecodeTransformation = params.useBytecodeTransformation + Agent.dynamicClassTransformer.transformer = instrumentation + Agent.dynamicClassTransformer.addUserPaths(pathsToUserClasses) + instrumentation.run { setupAdditionalRdResponses(kryoHelper, watchdog) } + } + watchdog.measureTimeForActiveCall(addPaths, "User and dependency classpath setup") { params -> + pathsToUserClasses = params.pathsToUserClasses.split(File.pathSeparatorChar).toSet() + HandlerClassesLoader.addUrls(pathsToUserClasses) + kryoHelper.setKryoClassLoader(HandlerClassesLoader) // Now kryo will use our classloader when it encounters unregistered class. + UtContext.setUtContext(UtContext(HandlerClassesLoader)) + } + watchdog.measureTimeForActiveCall(collectCoverage, "Coverage") { params -> + val anyClass: Class<*> = kryoHelper.readObject(params.clazz) + logger.debug { "class - ${anyClass.name}" } + val result = (instrumentation as CoverageInstrumentation).collectCoverageInfo(anyClass) + CollectCoverageResult(kryoHelper.writeObject(result)) + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/Security.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/Security.kt index 70bc6b2e3a..e506c50541 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/Security.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/Security.kt @@ -32,11 +32,12 @@ internal fun permissions(block: SimplePolicy.() -> Unit) { /** * Make this [AccessibleObject] accessible and run a block inside sandbox. + * + * If [bypassSandbox] is `true` then block is run without sandbox. */ -fun O.runSandbox(block: O.() -> R): R { - return withAccessibility { - sandbox { block() } - } +fun O.runSandbox(bypassSandbox: Boolean = false, block: O.() -> R): R = withAccessibility { + if (bypassSandbox) block() + else sandbox { block() } } /** diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentedProcess.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentedProcess.kt new file mode 100644 index 0000000000..d752643d3f --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentedProcess.kt @@ -0,0 +1,141 @@ +package org.utbot.instrumentation.rd + +import com.jetbrains.rd.util.lifetime.Lifetime +import mu.KotlinLogging +import org.utbot.common.JarUtils +import org.utbot.common.debug +import org.utbot.common.getPid +import org.utbot.common.measureTime +import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.services.WorkingDirService +import org.utbot.framework.process.AbstractRDProcessCompanion +import org.utbot.framework.process.kryo.KryoHelper +import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.process.DISABLE_SANDBOX_OPTION +import org.utbot.instrumentation.process.generated.AddPathsParams +import org.utbot.instrumentation.process.generated.InstrumentedProcessModel +import org.utbot.instrumentation.process.generated.SetInstrumentationParams +import org.utbot.instrumentation.process.generated.instrumentedProcessModel +import org.utbot.rd.ProcessWithRdServer +import org.utbot.rd.exceptions.InstantProcessDeathException +import org.utbot.rd.generated.LoggerModel +import org.utbot.rd.generated.loggerModel +import org.utbot.rd.loggers.setup +import org.utbot.rd.onSchedulerBlocking +import org.utbot.rd.startUtProcessWithRdServer +import org.utbot.rd.terminateOnException +import java.io.File + +private val logger = KotlinLogging.logger { } + +private const val UTBOT_INSTRUMENTATION_JAR_FILENAME = "utbot-instrumentation-shadow.jar" + +private val instrumentationJarFile: File = + logger.debug().measureTime({ "Finding $UTBOT_INSTRUMENTATION_JAR_FILENAME jar" } ) { + try { + JarUtils.extractJarFileFromResources( + jarFileName = UTBOT_INSTRUMENTATION_JAR_FILENAME, + jarResourcePath = "lib/$UTBOT_INSTRUMENTATION_JAR_FILENAME", + targetDirectoryName = "utbot-instrumentation" + ) + } catch (e: Exception) { + throw IllegalStateException( + """ + Can't find file: $UTBOT_INSTRUMENTATION_JAR_FILENAME. + Make sure you added $UTBOT_INSTRUMENTATION_JAR_FILENAME to the resources folder from gradle. + """.trimIndent(), + e + ) + } + } + +class InstrumentedProcessInstantDeathException : + InstantProcessDeathException(UtSettings.instrumentedProcessDebugPort, UtSettings.runInstrumentedProcessWithDebug) + +/** + * Main goals of this class: + * 1. prepare started instrumented process for execution - initializing rd, sending paths and instrumentation + * 2. expose bound model + */ +class InstrumentedProcess private constructor( + private val classLoader: ClassLoader?, + private val rdProcess: ProcessWithRdServer +) : ProcessWithRdServer by rdProcess { + val kryoHelper = KryoHelper(lifetime.createNested()).apply { + classLoader?.let { setKryoClassLoader(it) } + } + val instrumentedProcessModel: InstrumentedProcessModel = onSchedulerBlocking { protocol.instrumentedProcessModel } + val loggerModel: LoggerModel = onSchedulerBlocking { protocol.loggerModel } + + companion object : AbstractRDProcessCompanion( + debugPort = UtSettings.instrumentedProcessDebugPort, + runWithDebug = UtSettings.runInstrumentedProcessWithDebug, + suspendExecutionInDebugMode = UtSettings.suspendInstrumentedProcessExecutionInDebugMode, + processSpecificCommandLineArgs = { + buildList { + add("-javaagent:${instrumentationJarFile.path}") + add("-ea") + add("-jar") + add(instrumentationJarFile.path) + if (!UtSettings.useSandbox) + add(DISABLE_SANDBOX_OPTION) + } + }) { + + suspend operator fun > invoke( + parent: Lifetime, + instrumentationFactory: Instrumentation.Factory, + pathsToUserClasses: String, + classLoader: ClassLoader? + ): InstrumentedProcess = parent.createNested().terminateOnException { lifetime -> + val rdProcess: ProcessWithRdServer = startUtProcessWithRdServer( + lifetime = lifetime + ) { port -> + val cmd = obtainProcessCommandLine(port) + listOfNotNull( + DISABLE_SANDBOX_OPTION.takeIf { instrumentationFactory.forceDisableSandbox } + ) + logger.debug { "Starting instrumented process: $cmd" } + val directory = WorkingDirService.provide().toFile() + val processBuilder = ProcessBuilder(cmd) + .directory(directory) + val process = processBuilder.start() + logger.info { + "------------------------------------------------------------------\n" + + "--------Instrumented process started with PID=${process.getPid}--------\n" + + "------------------------------------------------------------------" + } + if (!process.isAlive) { + throw InstrumentedProcessInstantDeathException() + } + process + }.awaitProcessReady() + + logger.trace("rd process started") + + val proc = InstrumentedProcess(classLoader, rdProcess) + proc.loggerModel.setup(logger, proc.lifetime) + + proc.lifetime.onTermination { + logger.trace { "process is terminating" } + } + + logger.trace("sending add paths") + proc.instrumentedProcessModel.addPaths.startSuspending( + proc.lifetime, AddPathsParams( + pathsToUserClasses, + ) + ) + + logger.trace("sending instrumentation") + proc.instrumentedProcessModel.setInstrumentation.startSuspending( + proc.lifetime, SetInstrumentationParams( + proc.kryoHelper.writeObject(instrumentationFactory), + UtSettings.useBytecodeTransformation + ) + ) + logger.trace("start commands sent") + + return proc + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/UtInstrumentationProcess.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/UtInstrumentationProcess.kt deleted file mode 100644 index ba7d09d62e..0000000000 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/UtInstrumentationProcess.kt +++ /dev/null @@ -1,96 +0,0 @@ -package org.utbot.instrumentation.rd - -import com.jetbrains.rd.util.lifetime.Lifetime -import mu.KotlinLogging -import org.utbot.instrumentation.instrumentation.Instrumentation -import org.utbot.instrumentation.process.ChildProcessRunner -import org.utbot.instrumentation.rd.generated.AddPathsParams -import org.utbot.instrumentation.rd.generated.ChildProcessModel -import org.utbot.instrumentation.rd.generated.SetInstrumentationParams -import org.utbot.instrumentation.rd.generated.childProcessModel -import org.utbot.instrumentation.util.KryoHelper -import org.utbot.rd.ProcessWithRdServer -import org.utbot.rd.startUtProcessWithRdServer -import org.utbot.rd.terminateOnException - -private val logger = KotlinLogging.logger {} - -/** - * Main goals of this class: - * 1. prepare started child process for execution - initializing rd, sending paths and instrumentation - * 2. expose bound model - */ -class UtInstrumentationProcess private constructor( - private val classLoader: ClassLoader?, - private val rdProcess: ProcessWithRdServer -) : ProcessWithRdServer by rdProcess { - val kryoHelper = KryoHelper(lifetime.createNested()).apply { - classLoader?.let { setKryoClassLoader(it) } - } - val protocolModel: ChildProcessModel - get() = protocol.childProcessModel - - companion object { - private suspend fun > invokeImpl( - lifetime: Lifetime, - childProcessRunner: ChildProcessRunner, - instrumentation: TInstrumentation, - pathsToUserClasses: String, - pathsToDependencyClasses: String, - classLoader: ClassLoader? - ): UtInstrumentationProcess { - val rdProcess: ProcessWithRdServer = startUtProcessWithRdServer( - lifetime = lifetime - ) { - childProcessRunner.start(it) - }.initModels { childProcessModel }.awaitSignal() - - logger.trace("rd process started") - - val proc = UtInstrumentationProcess( - classLoader, - rdProcess - ) - - proc.lifetime.onTermination { - logger.trace { "process is terminating" } - } - - logger.trace("sending add paths") - proc.protocolModel.addPaths.startSuspending( - proc.lifetime, AddPathsParams( - pathsToUserClasses, - pathsToDependencyClasses - ) - ) - - logger.trace("sending instrumentation") - proc.protocolModel.setInstrumentation.startSuspending( - proc.lifetime, SetInstrumentationParams( - proc.kryoHelper.writeObject(instrumentation) - ) - ) - logger.trace("start commands sent") - - return proc - } - - suspend operator fun > invoke( - lifetime: Lifetime, - childProcessRunner: ChildProcessRunner, - instrumentation: TInstrumentation, - pathsToUserClasses: String, - pathsToDependencyClasses: String, - classLoader: ClassLoader? - ): UtInstrumentationProcess = lifetime.createNested().terminateOnException { - invokeImpl( - it, - childProcessRunner, - instrumentation, - pathsToUserClasses, - pathsToDependencyClasses, - classLoader - ) - } - } -} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ChildProcessModel.Generated.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ChildProcessModel.Generated.kt deleted file mode 100644 index ae552c67a5..0000000000 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ChildProcessModel.Generated.kt +++ /dev/null @@ -1,656 +0,0 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") -package org.utbot.instrumentation.rd.generated - -import com.jetbrains.rd.framework.* -import com.jetbrains.rd.framework.base.* -import com.jetbrains.rd.framework.impl.* - -import com.jetbrains.rd.util.lifetime.* -import com.jetbrains.rd.util.reactive.* -import com.jetbrains.rd.util.string.* -import com.jetbrains.rd.util.* -import kotlin.reflect.KClass -import kotlin.jvm.JvmStatic - - - -/** - * #### Generated from [ChildProcessModel.kt:7] - */ -class ChildProcessModel private constructor( - private val _addPaths: RdCall, - private val _warmup: RdCall, - private val _setInstrumentation: RdCall, - private val _invokeMethodCommand: RdCall, - private val _stopProcess: RdCall, - private val _collectCoverage: RdCall, - private val _computeStaticField: RdCall -) : RdExtBase() { - //companion - - companion object : ISerializersOwner { - - override fun registerSerializersCore(serializers: ISerializers) { - serializers.register(AddPathsParams) - serializers.register(SetInstrumentationParams) - serializers.register(InvokeMethodCommandParams) - serializers.register(InvokeMethodCommandResult) - serializers.register(CollectCoverageParams) - serializers.register(CollectCoverageResult) - serializers.register(ComputeStaticFieldParams) - serializers.register(ComputeStaticFieldResult) - } - - - @JvmStatic - @JvmName("internalCreateModel") - @Deprecated("Use create instead", ReplaceWith("create(lifetime, protocol)")) - internal fun createModel(lifetime: Lifetime, protocol: IProtocol): ChildProcessModel { - @Suppress("DEPRECATION") - return create(lifetime, protocol) - } - - @JvmStatic - @Deprecated("Use protocol.childProcessModel or revise the extension scope instead", ReplaceWith("protocol.childProcessModel")) - fun create(lifetime: Lifetime, protocol: IProtocol): ChildProcessModel { - ChildProcessProtocolRoot.register(protocol.serializers) - - return ChildProcessModel().apply { - identify(protocol.identity, RdId.Null.mix("ChildProcessModel")) - bind(lifetime, protocol, "ChildProcessModel") - } - } - - - const val serializationHash = 3283744426733090208L - - } - override val serializersOwner: ISerializersOwner get() = ChildProcessModel - override val serializationHash: Long get() = ChildProcessModel.serializationHash - - //fields - - /** - * The main process tells where the child process should search for the classes - */ - val addPaths: RdCall get() = _addPaths - - /** - * Load classes from classpath and instrument them - */ - val warmup: RdCall get() = _warmup - - /** - * The main process sends [instrumentation] to the child process - */ - val setInstrumentation: RdCall get() = _setInstrumentation - - /** - * The main process requests the child process to execute a method with the given [signature], - which declaring class's name is [className]. - @property parameters are the parameters needed for an execution, e.g. static environment - */ - val invokeMethodCommand: RdCall get() = _invokeMethodCommand - - /** - * This command tells the child process to stop - */ - val stopProcess: RdCall get() = _stopProcess - - /** - * This command is sent to the child process from the [ConcreteExecutor] if user wants to collect coverage for the - [clazz] - */ - val collectCoverage: RdCall get() = _collectCoverage - - /** - * This command is sent to the child process from the [ConcreteExecutor] if user wants to get value of static field - [fieldId] - */ - val computeStaticField: RdCall get() = _computeStaticField - //methods - //initializer - init { - _addPaths.async = true - _warmup.async = true - _setInstrumentation.async = true - _invokeMethodCommand.async = true - _stopProcess.async = true - _collectCoverage.async = true - _computeStaticField.async = true - } - - init { - bindableChildren.add("addPaths" to _addPaths) - bindableChildren.add("warmup" to _warmup) - bindableChildren.add("setInstrumentation" to _setInstrumentation) - bindableChildren.add("invokeMethodCommand" to _invokeMethodCommand) - bindableChildren.add("stopProcess" to _stopProcess) - bindableChildren.add("collectCoverage" to _collectCoverage) - bindableChildren.add("computeStaticField" to _computeStaticField) - } - - //secondary constructor - private constructor( - ) : this( - RdCall(AddPathsParams, FrameworkMarshallers.Void), - RdCall(FrameworkMarshallers.Void, FrameworkMarshallers.Void), - RdCall(SetInstrumentationParams, FrameworkMarshallers.Void), - RdCall(InvokeMethodCommandParams, InvokeMethodCommandResult), - RdCall(FrameworkMarshallers.Void, FrameworkMarshallers.Void), - RdCall(CollectCoverageParams, CollectCoverageResult), - RdCall(ComputeStaticFieldParams, ComputeStaticFieldResult) - ) - - //equals trait - //hash code trait - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("ChildProcessModel (") - printer.indent { - print("addPaths = "); _addPaths.print(printer); println() - print("warmup = "); _warmup.print(printer); println() - print("setInstrumentation = "); _setInstrumentation.print(printer); println() - print("invokeMethodCommand = "); _invokeMethodCommand.print(printer); println() - print("stopProcess = "); _stopProcess.print(printer); println() - print("collectCoverage = "); _collectCoverage.print(printer); println() - print("computeStaticField = "); _computeStaticField.print(printer); println() - } - printer.print(")") - } - //deepClone - override fun deepClone(): ChildProcessModel { - return ChildProcessModel( - _addPaths.deepClonePolymorphic(), - _warmup.deepClonePolymorphic(), - _setInstrumentation.deepClonePolymorphic(), - _invokeMethodCommand.deepClonePolymorphic(), - _stopProcess.deepClonePolymorphic(), - _collectCoverage.deepClonePolymorphic(), - _computeStaticField.deepClonePolymorphic() - ) - } - //contexts -} -val IProtocol.childProcessModel get() = getOrCreateExtension(ChildProcessModel::class) { @Suppress("DEPRECATION") ChildProcessModel.create(lifetime, this) } - - - -/** - * #### Generated from [ChildProcessModel.kt:8] - */ -data class AddPathsParams ( - val pathsToUserClasses: String, - val pathsToDependencyClasses: String -) : IPrintable { - //companion - - companion object : IMarshaller { - override val _type: KClass = AddPathsParams::class - - @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): AddPathsParams { - val pathsToUserClasses = buffer.readString() - val pathsToDependencyClasses = buffer.readString() - return AddPathsParams(pathsToUserClasses, pathsToDependencyClasses) - } - - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: AddPathsParams) { - buffer.writeString(value.pathsToUserClasses) - buffer.writeString(value.pathsToDependencyClasses) - } - - - } - //fields - //methods - //initializer - //secondary constructor - //equals trait - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || other::class != this::class) return false - - other as AddPathsParams - - if (pathsToUserClasses != other.pathsToUserClasses) return false - if (pathsToDependencyClasses != other.pathsToDependencyClasses) return false - - return true - } - //hash code trait - override fun hashCode(): Int { - var __r = 0 - __r = __r*31 + pathsToUserClasses.hashCode() - __r = __r*31 + pathsToDependencyClasses.hashCode() - return __r - } - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("AddPathsParams (") - printer.indent { - print("pathsToUserClasses = "); pathsToUserClasses.print(printer); println() - print("pathsToDependencyClasses = "); pathsToDependencyClasses.print(printer); println() - } - printer.print(")") - } - //deepClone - //contexts -} - - -/** - * #### Generated from [ChildProcessModel.kt:28] - */ -data class CollectCoverageParams ( - val clazz: ByteArray -) : IPrintable { - //companion - - companion object : IMarshaller { - override val _type: KClass = CollectCoverageParams::class - - @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): CollectCoverageParams { - val clazz = buffer.readByteArray() - return CollectCoverageParams(clazz) - } - - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: CollectCoverageParams) { - buffer.writeByteArray(value.clazz) - } - - - } - //fields - //methods - //initializer - //secondary constructor - //equals trait - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || other::class != this::class) return false - - other as CollectCoverageParams - - if (!(clazz contentEquals other.clazz)) return false - - return true - } - //hash code trait - override fun hashCode(): Int { - var __r = 0 - __r = __r*31 + clazz.contentHashCode() - return __r - } - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("CollectCoverageParams (") - printer.indent { - print("clazz = "); clazz.print(printer); println() - } - printer.print(")") - } - //deepClone - //contexts -} - - -/** - * #### Generated from [ChildProcessModel.kt:32] - */ -data class CollectCoverageResult ( - val coverageInfo: ByteArray -) : IPrintable { - //companion - - companion object : IMarshaller { - override val _type: KClass = CollectCoverageResult::class - - @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): CollectCoverageResult { - val coverageInfo = buffer.readByteArray() - return CollectCoverageResult(coverageInfo) - } - - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: CollectCoverageResult) { - buffer.writeByteArray(value.coverageInfo) - } - - - } - //fields - //methods - //initializer - //secondary constructor - //equals trait - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || other::class != this::class) return false - - other as CollectCoverageResult - - if (!(coverageInfo contentEquals other.coverageInfo)) return false - - return true - } - //hash code trait - override fun hashCode(): Int { - var __r = 0 - __r = __r*31 + coverageInfo.contentHashCode() - return __r - } - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("CollectCoverageResult (") - printer.indent { - print("coverageInfo = "); coverageInfo.print(printer); println() - } - printer.print(")") - } - //deepClone - //contexts -} - - -/** - * #### Generated from [ChildProcessModel.kt:36] - */ -data class ComputeStaticFieldParams ( - val fieldId: ByteArray -) : IPrintable { - //companion - - companion object : IMarshaller { - override val _type: KClass = ComputeStaticFieldParams::class - - @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): ComputeStaticFieldParams { - val fieldId = buffer.readByteArray() - return ComputeStaticFieldParams(fieldId) - } - - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: ComputeStaticFieldParams) { - buffer.writeByteArray(value.fieldId) - } - - - } - //fields - //methods - //initializer - //secondary constructor - //equals trait - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || other::class != this::class) return false - - other as ComputeStaticFieldParams - - if (!(fieldId contentEquals other.fieldId)) return false - - return true - } - //hash code trait - override fun hashCode(): Int { - var __r = 0 - __r = __r*31 + fieldId.contentHashCode() - return __r - } - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("ComputeStaticFieldParams (") - printer.indent { - print("fieldId = "); fieldId.print(printer); println() - } - printer.print(")") - } - //deepClone - //contexts -} - - -/** - * #### Generated from [ChildProcessModel.kt:40] - */ -data class ComputeStaticFieldResult ( - val result: ByteArray -) : IPrintable { - //companion - - companion object : IMarshaller { - override val _type: KClass = ComputeStaticFieldResult::class - - @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): ComputeStaticFieldResult { - val result = buffer.readByteArray() - return ComputeStaticFieldResult(result) - } - - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: ComputeStaticFieldResult) { - buffer.writeByteArray(value.result) - } - - - } - //fields - //methods - //initializer - //secondary constructor - //equals trait - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || other::class != this::class) return false - - other as ComputeStaticFieldResult - - if (!(result contentEquals other.result)) return false - - return true - } - //hash code trait - override fun hashCode(): Int { - var __r = 0 - __r = __r*31 + result.contentHashCode() - return __r - } - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("ComputeStaticFieldResult (") - printer.indent { - print("result = "); result.print(printer); println() - } - printer.print(")") - } - //deepClone - //contexts -} - - -/** - * #### Generated from [ChildProcessModel.kt:17] - */ -data class InvokeMethodCommandParams ( - val classname: String, - val signature: String, - val arguments: ByteArray, - val parameters: ByteArray -) : IPrintable { - //companion - - companion object : IMarshaller { - override val _type: KClass = InvokeMethodCommandParams::class - - @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): InvokeMethodCommandParams { - val classname = buffer.readString() - val signature = buffer.readString() - val arguments = buffer.readByteArray() - val parameters = buffer.readByteArray() - return InvokeMethodCommandParams(classname, signature, arguments, parameters) - } - - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: InvokeMethodCommandParams) { - buffer.writeString(value.classname) - buffer.writeString(value.signature) - buffer.writeByteArray(value.arguments) - buffer.writeByteArray(value.parameters) - } - - - } - //fields - //methods - //initializer - //secondary constructor - //equals trait - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || other::class != this::class) return false - - other as InvokeMethodCommandParams - - if (classname != other.classname) return false - if (signature != other.signature) return false - if (!(arguments contentEquals other.arguments)) return false - if (!(parameters contentEquals other.parameters)) return false - - return true - } - //hash code trait - override fun hashCode(): Int { - var __r = 0 - __r = __r*31 + classname.hashCode() - __r = __r*31 + signature.hashCode() - __r = __r*31 + arguments.contentHashCode() - __r = __r*31 + parameters.contentHashCode() - return __r - } - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("InvokeMethodCommandParams (") - printer.indent { - print("classname = "); classname.print(printer); println() - print("signature = "); signature.print(printer); println() - print("arguments = "); arguments.print(printer); println() - print("parameters = "); parameters.print(printer); println() - } - printer.print(")") - } - //deepClone - //contexts -} - - -/** - * #### Generated from [ChildProcessModel.kt:24] - */ -data class InvokeMethodCommandResult ( - val result: ByteArray -) : IPrintable { - //companion - - companion object : IMarshaller { - override val _type: KClass = InvokeMethodCommandResult::class - - @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): InvokeMethodCommandResult { - val result = buffer.readByteArray() - return InvokeMethodCommandResult(result) - } - - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: InvokeMethodCommandResult) { - buffer.writeByteArray(value.result) - } - - - } - //fields - //methods - //initializer - //secondary constructor - //equals trait - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || other::class != this::class) return false - - other as InvokeMethodCommandResult - - if (!(result contentEquals other.result)) return false - - return true - } - //hash code trait - override fun hashCode(): Int { - var __r = 0 - __r = __r*31 + result.contentHashCode() - return __r - } - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("InvokeMethodCommandResult (") - printer.indent { - print("result = "); result.print(printer); println() - } - printer.print(")") - } - //deepClone - //contexts -} - - -/** - * #### Generated from [ChildProcessModel.kt:13] - */ -data class SetInstrumentationParams ( - val instrumentation: ByteArray -) : IPrintable { - //companion - - companion object : IMarshaller { - override val _type: KClass = SetInstrumentationParams::class - - @Suppress("UNCHECKED_CAST") - override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): SetInstrumentationParams { - val instrumentation = buffer.readByteArray() - return SetInstrumentationParams(instrumentation) - } - - override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: SetInstrumentationParams) { - buffer.writeByteArray(value.instrumentation) - } - - - } - //fields - //methods - //initializer - //secondary constructor - //equals trait - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || other::class != this::class) return false - - other as SetInstrumentationParams - - if (!(instrumentation contentEquals other.instrumentation)) return false - - return true - } - //hash code trait - override fun hashCode(): Int { - var __r = 0 - __r = __r*31 + instrumentation.contentHashCode() - return __r - } - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("SetInstrumentationParams (") - printer.indent { - print("instrumentation = "); instrumentation.print(printer); println() - } - printer.print(")") - } - //deepClone - //contexts -} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ChildProcessProtocolRoot.Generated.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ChildProcessProtocolRoot.Generated.kt deleted file mode 100644 index 7969676ff7..0000000000 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/ChildProcessProtocolRoot.Generated.kt +++ /dev/null @@ -1,58 +0,0 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") -package org.utbot.instrumentation.rd.generated - -import com.jetbrains.rd.framework.* -import com.jetbrains.rd.framework.base.* -import com.jetbrains.rd.framework.impl.* - -import com.jetbrains.rd.util.lifetime.* -import com.jetbrains.rd.util.reactive.* -import com.jetbrains.rd.util.string.* -import com.jetbrains.rd.util.* -import kotlin.reflect.KClass -import kotlin.jvm.JvmStatic - - - -/** - * #### Generated from [ChildProcessModel.kt:5] - */ -class ChildProcessProtocolRoot private constructor( -) : RdExtBase() { - //companion - - companion object : ISerializersOwner { - - override fun registerSerializersCore(serializers: ISerializers) { - ChildProcessProtocolRoot.register(serializers) - ChildProcessModel.register(serializers) - } - - - - - - const val serializationHash = -2158664525887799313L - - } - override val serializersOwner: ISerializersOwner get() = ChildProcessProtocolRoot - override val serializationHash: Long get() = ChildProcessProtocolRoot.serializationHash - - //fields - //methods - //initializer - //secondary constructor - //equals trait - //hash code trait - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("ChildProcessProtocolRoot (") - printer.print(")") - } - //deepClone - override fun deepClone(): ChildProcessProtocolRoot { - return ChildProcessProtocolRoot( - ) - } - //contexts -} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt new file mode 100644 index 0000000000..2fe14c35d9 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt @@ -0,0 +1,840 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.instrumentation.process.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [InstrumentedProcessModel.kt:8] + */ +class InstrumentedProcessModel private constructor( + private val _addPaths: RdCall, + private val _warmup: RdCall, + private val _setInstrumentation: RdCall, + private val _invokeMethodCommand: RdCall, + private val _collectCoverage: RdCall, + private val _computeStaticField: RdCall, + private val _getRelevantSpringRepositories: RdCall, + private val _tryLoadingSpringContext: RdCall +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + serializers.register(AddPathsParams) + serializers.register(SetInstrumentationParams) + serializers.register(InvokeMethodCommandParams) + serializers.register(InvokeMethodCommandResult) + serializers.register(CollectCoverageParams) + serializers.register(CollectCoverageResult) + serializers.register(ComputeStaticFieldParams) + serializers.register(ComputeStaticFieldResult) + serializers.register(GetSpringRepositoriesParams) + serializers.register(GetSpringRepositoriesResult) + serializers.register(TryLoadingSpringContextResult) + } + + + @JvmStatic + @JvmName("internalCreateModel") + @Deprecated("Use create instead", ReplaceWith("create(lifetime, protocol)")) + internal fun createModel(lifetime: Lifetime, protocol: IProtocol): InstrumentedProcessModel { + @Suppress("DEPRECATION") + return create(lifetime, protocol) + } + + @JvmStatic + @Deprecated("Use protocol.instrumentedProcessModel or revise the extension scope instead", ReplaceWith("protocol.instrumentedProcessModel")) + fun create(lifetime: Lifetime, protocol: IProtocol): InstrumentedProcessModel { + InstrumentedProcessRoot.register(protocol.serializers) + + return InstrumentedProcessModel() + } + + + const val serializationHash = 8567129171874407469L + + } + override val serializersOwner: ISerializersOwner get() = InstrumentedProcessModel + override val serializationHash: Long get() = InstrumentedProcessModel.serializationHash + + //fields + + /** + * The main process tells where the instrumented process should search for the classes + */ + val addPaths: RdCall get() = _addPaths + + /** + * Load classes from classpath and instrument them + */ + val warmup: RdCall get() = _warmup + + /** + * The main process sends [instrumentation] to the instrumented process + */ + val setInstrumentation: RdCall get() = _setInstrumentation + + /** + * The main process requests the instrumented process to execute a method with the given [signature], + which declaring class's name is [className]. + @property parameters are the parameters needed for an execution, e.g. static environment + */ + val invokeMethodCommand: RdCall get() = _invokeMethodCommand + + /** + * This command is sent to the instrumented process from the [ConcreteExecutor] if user wants to collect coverage for the + [clazz] + */ + val collectCoverage: RdCall get() = _collectCoverage + + /** + * This command is sent to the instrumented process from the [ConcreteExecutor] if user wants to get value of static field + [fieldId] + */ + val computeStaticField: RdCall get() = _computeStaticField + + /** + * Gets a list of [SpringRepositoryId]s that class specified by the [ClassId] (possibly indirectly) depends on (requires Spring instrumentation) + */ + val getRelevantSpringRepositories: RdCall get() = _getRelevantSpringRepositories + + /** + * This command is sent to the instrumented process from the [ConcreteExecutor] + if the user wants to determine whether or not Spring application context can load + */ + val tryLoadingSpringContext: RdCall get() = _tryLoadingSpringContext + //methods + //initializer + init { + _addPaths.async = true + _warmup.async = true + _setInstrumentation.async = true + _invokeMethodCommand.async = true + _collectCoverage.async = true + _computeStaticField.async = true + _getRelevantSpringRepositories.async = true + _tryLoadingSpringContext.async = true + } + + init { + bindableChildren.add("addPaths" to _addPaths) + bindableChildren.add("warmup" to _warmup) + bindableChildren.add("setInstrumentation" to _setInstrumentation) + bindableChildren.add("invokeMethodCommand" to _invokeMethodCommand) + bindableChildren.add("collectCoverage" to _collectCoverage) + bindableChildren.add("computeStaticField" to _computeStaticField) + bindableChildren.add("getRelevantSpringRepositories" to _getRelevantSpringRepositories) + bindableChildren.add("tryLoadingSpringContext" to _tryLoadingSpringContext) + } + + //secondary constructor + private constructor( + ) : this( + RdCall(AddPathsParams, FrameworkMarshallers.Void), + RdCall(FrameworkMarshallers.Void, FrameworkMarshallers.Void), + RdCall(SetInstrumentationParams, FrameworkMarshallers.Void), + RdCall(InvokeMethodCommandParams, InvokeMethodCommandResult), + RdCall(CollectCoverageParams, CollectCoverageResult), + RdCall(ComputeStaticFieldParams, ComputeStaticFieldResult), + RdCall(GetSpringRepositoriesParams, GetSpringRepositoriesResult), + RdCall(FrameworkMarshallers.Void, TryLoadingSpringContextResult) + ) + + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("InstrumentedProcessModel (") + printer.indent { + print("addPaths = "); _addPaths.print(printer); println() + print("warmup = "); _warmup.print(printer); println() + print("setInstrumentation = "); _setInstrumentation.print(printer); println() + print("invokeMethodCommand = "); _invokeMethodCommand.print(printer); println() + print("collectCoverage = "); _collectCoverage.print(printer); println() + print("computeStaticField = "); _computeStaticField.print(printer); println() + print("getRelevantSpringRepositories = "); _getRelevantSpringRepositories.print(printer); println() + print("tryLoadingSpringContext = "); _tryLoadingSpringContext.print(printer); println() + } + printer.print(")") + } + //deepClone + override fun deepClone(): InstrumentedProcessModel { + return InstrumentedProcessModel( + _addPaths.deepClonePolymorphic(), + _warmup.deepClonePolymorphic(), + _setInstrumentation.deepClonePolymorphic(), + _invokeMethodCommand.deepClonePolymorphic(), + _collectCoverage.deepClonePolymorphic(), + _computeStaticField.deepClonePolymorphic(), + _getRelevantSpringRepositories.deepClonePolymorphic(), + _tryLoadingSpringContext.deepClonePolymorphic() + ) + } + //contexts +} +val IProtocol.instrumentedProcessModel get() = getOrCreateExtension(InstrumentedProcessModel::class) { @Suppress("DEPRECATION") InstrumentedProcessModel.create(lifetime, this) } + + + +/** + * #### Generated from [InstrumentedProcessModel.kt:9] + */ +data class AddPathsParams ( + val pathsToUserClasses: String +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = AddPathsParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): AddPathsParams { + val pathsToUserClasses = buffer.readString() + return AddPathsParams(pathsToUserClasses) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: AddPathsParams) { + buffer.writeString(value.pathsToUserClasses) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as AddPathsParams + + if (pathsToUserClasses != other.pathsToUserClasses) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + pathsToUserClasses.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("AddPathsParams (") + printer.indent { + print("pathsToUserClasses = "); pathsToUserClasses.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:29] + */ +data class CollectCoverageParams ( + val clazz: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = CollectCoverageParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): CollectCoverageParams { + val clazz = buffer.readByteArray() + return CollectCoverageParams(clazz) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: CollectCoverageParams) { + buffer.writeByteArray(value.clazz) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as CollectCoverageParams + + if (!(clazz contentEquals other.clazz)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + clazz.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("CollectCoverageParams (") + printer.indent { + print("clazz = "); clazz.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:33] + */ +data class CollectCoverageResult ( + val coverageInfo: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = CollectCoverageResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): CollectCoverageResult { + val coverageInfo = buffer.readByteArray() + return CollectCoverageResult(coverageInfo) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: CollectCoverageResult) { + buffer.writeByteArray(value.coverageInfo) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as CollectCoverageResult + + if (!(coverageInfo contentEquals other.coverageInfo)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + coverageInfo.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("CollectCoverageResult (") + printer.indent { + print("coverageInfo = "); coverageInfo.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:37] + */ +data class ComputeStaticFieldParams ( + val fieldId: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = ComputeStaticFieldParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): ComputeStaticFieldParams { + val fieldId = buffer.readByteArray() + return ComputeStaticFieldParams(fieldId) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: ComputeStaticFieldParams) { + buffer.writeByteArray(value.fieldId) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as ComputeStaticFieldParams + + if (!(fieldId contentEquals other.fieldId)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + fieldId.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("ComputeStaticFieldParams (") + printer.indent { + print("fieldId = "); fieldId.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:41] + */ +data class ComputeStaticFieldResult ( + val result: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = ComputeStaticFieldResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): ComputeStaticFieldResult { + val result = buffer.readByteArray() + return ComputeStaticFieldResult(result) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: ComputeStaticFieldResult) { + buffer.writeByteArray(value.result) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as ComputeStaticFieldResult + + if (!(result contentEquals other.result)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + result.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("ComputeStaticFieldResult (") + printer.indent { + print("result = "); result.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:45] + */ +data class GetSpringRepositoriesParams ( + val classId: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = GetSpringRepositoriesParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): GetSpringRepositoriesParams { + val classId = buffer.readByteArray() + return GetSpringRepositoriesParams(classId) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: GetSpringRepositoriesParams) { + buffer.writeByteArray(value.classId) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as GetSpringRepositoriesParams + + if (!(classId contentEquals other.classId)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + classId.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("GetSpringRepositoriesParams (") + printer.indent { + print("classId = "); classId.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:49] + */ +data class GetSpringRepositoriesResult ( + val springRepositoryIds: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = GetSpringRepositoriesResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): GetSpringRepositoriesResult { + val springRepositoryIds = buffer.readByteArray() + return GetSpringRepositoriesResult(springRepositoryIds) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: GetSpringRepositoriesResult) { + buffer.writeByteArray(value.springRepositoryIds) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as GetSpringRepositoriesResult + + if (!(springRepositoryIds contentEquals other.springRepositoryIds)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + springRepositoryIds.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("GetSpringRepositoriesResult (") + printer.indent { + print("springRepositoryIds = "); springRepositoryIds.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:18] + */ +data class InvokeMethodCommandParams ( + val classname: String, + val signature: String, + val arguments: ByteArray, + val parameters: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = InvokeMethodCommandParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): InvokeMethodCommandParams { + val classname = buffer.readString() + val signature = buffer.readString() + val arguments = buffer.readByteArray() + val parameters = buffer.readByteArray() + return InvokeMethodCommandParams(classname, signature, arguments, parameters) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: InvokeMethodCommandParams) { + buffer.writeString(value.classname) + buffer.writeString(value.signature) + buffer.writeByteArray(value.arguments) + buffer.writeByteArray(value.parameters) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as InvokeMethodCommandParams + + if (classname != other.classname) return false + if (signature != other.signature) return false + if (!(arguments contentEquals other.arguments)) return false + if (!(parameters contentEquals other.parameters)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + classname.hashCode() + __r = __r*31 + signature.hashCode() + __r = __r*31 + arguments.contentHashCode() + __r = __r*31 + parameters.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("InvokeMethodCommandParams (") + printer.indent { + print("classname = "); classname.print(printer); println() + print("signature = "); signature.print(printer); println() + print("arguments = "); arguments.print(printer); println() + print("parameters = "); parameters.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:25] + */ +data class InvokeMethodCommandResult ( + val result: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = InvokeMethodCommandResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): InvokeMethodCommandResult { + val result = buffer.readByteArray() + return InvokeMethodCommandResult(result) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: InvokeMethodCommandResult) { + buffer.writeByteArray(value.result) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as InvokeMethodCommandResult + + if (!(result contentEquals other.result)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + result.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("InvokeMethodCommandResult (") + printer.indent { + print("result = "); result.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:13] + */ +data class SetInstrumentationParams ( + val instrumentation: ByteArray, + val useBytecodeTransformation: Boolean +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = SetInstrumentationParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): SetInstrumentationParams { + val instrumentation = buffer.readByteArray() + val useBytecodeTransformation = buffer.readBool() + return SetInstrumentationParams(instrumentation, useBytecodeTransformation) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: SetInstrumentationParams) { + buffer.writeByteArray(value.instrumentation) + buffer.writeBool(value.useBytecodeTransformation) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as SetInstrumentationParams + + if (!(instrumentation contentEquals other.instrumentation)) return false + if (useBytecodeTransformation != other.useBytecodeTransformation) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + instrumentation.contentHashCode() + __r = __r*31 + useBytecodeTransformation.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("SetInstrumentationParams (") + printer.indent { + print("instrumentation = "); instrumentation.print(printer); println() + print("useBytecodeTransformation = "); useBytecodeTransformation.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:53] + */ +data class TryLoadingSpringContextResult ( + val springContextLoadingResult: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = TryLoadingSpringContextResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): TryLoadingSpringContextResult { + val springContextLoadingResult = buffer.readByteArray() + return TryLoadingSpringContextResult(springContextLoadingResult) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: TryLoadingSpringContextResult) { + buffer.writeByteArray(value.springContextLoadingResult) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as TryLoadingSpringContextResult + + if (!(springContextLoadingResult contentEquals other.springContextLoadingResult)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + springContextLoadingResult.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("TryLoadingSpringContextResult (") + printer.indent { + print("springContextLoadingResult = "); springContextLoadingResult.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessRoot.Generated.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessRoot.Generated.kt new file mode 100644 index 0000000000..3ef312e0c8 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessRoot.Generated.kt @@ -0,0 +1,59 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.instrumentation.process.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [InstrumentedProcessModel.kt:6] + */ +class InstrumentedProcessRoot private constructor( +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + InstrumentedProcessRoot.register(serializers) + InstrumentedProcessModel.register(serializers) + } + + + + + + const val serializationHash = -7874463878801458679L + + } + override val serializersOwner: ISerializersOwner get() = InstrumentedProcessRoot + override val serializationHash: Long get() = InstrumentedProcessRoot.serializationHash + + //fields + //methods + //initializer + //secondary constructor + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("InstrumentedProcessRoot (") + printer.print(")") + } + //deepClone + override fun deepClone(): InstrumentedProcessRoot { + return InstrumentedProcessRoot( + ) + } + //contexts +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/AccessibleTypesAnalyzer.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/AccessibleTypesAnalyzer.kt new file mode 100644 index 0000000000..5c4fd07e5f --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/AccessibleTypesAnalyzer.kt @@ -0,0 +1,37 @@ +package org.utbot.instrumentation.util + +import org.utbot.common.withAccessibility +import java.util.IdentityHashMap + +class AccessibleTypesAnalyzer { + fun collectAccessibleTypes(bean: Any): Set> { + val accessibleObjects = collectFromObject(bean, analyzedObjects = IdentityHashMap()) + + return accessibleObjects + .map { it::class.java } + .toSet() + } + + private fun collectFromObject(obj: Any, analyzedObjects: IdentityHashMap): Set { + if (analyzedObjects.contains(obj)) { + return emptySet() + } + + analyzedObjects[obj] = Unit + + val clazz = obj::class.java + val objects = mutableSetOf() + + var current: Class<*> = clazz + while (current.superclass != null) { + objects.addAll(current.declaredFields.mapNotNull { + it.withAccessibility { get(obj) } + }.flatMap { + listOf(it) + collectFromObject(it, analyzedObjects) + }) + current = current.superclass + } + + return objects + } +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/InstrumentationException.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/InstrumentationException.kt index 8935985223..8a617f0168 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/InstrumentationException.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/InstrumentationException.kt @@ -1,34 +1,30 @@ -package org.utbot.instrumentation.util - -// TODO: refactor this - -/** - * Base class for instrumentation exceptions. - */ -open class InstrumentationException(msg: String?, cause: Throwable? = null) : Exception(msg, cause) - -class NoProbesArrayException(clazz: Class<*>, arrayName: String) : - InstrumentationException( - "No probes array found\n\t" + - "Clazz: $clazz\n\t" + - "Probes array name: $arrayName\n\t" + - "All fields: ${clazz.fields.joinToString { it.name }}" - ) - -class CastProbesArrayException : - InstrumentationException("Can't cast probes array to Boolean array") - -class ReadingFromKryoException(e: Throwable) : - InstrumentationException("Reading from Kryo exception |> ${e.stackTraceToString()}", e) - -class WritingToKryoException(e: Throwable) : - InstrumentationException("Writing to Kryo exception |> ${e.stackTraceToString()}", e) - -/** - * this exception is thrown only in main process. - * currently it means that {e: Throwable} happened in child process, - * but child process still can operate and not dead. - * on child process death - ConcreteExecutionFailureException is thrown -*/ -class ChildProcessError(e: Throwable) : - InstrumentationException("Error in the child process |> ${e.stackTraceToString()}", e) \ No newline at end of file +package org.utbot.instrumentation.util + +import org.utbot.framework.plugin.api.InstrumentedProcessDeathException + +// TODO: refactor this + +/** + * Base class for instrumentation exceptions. + */ +open class InstrumentationException(msg: String?, cause: Throwable? = null) : Exception(msg, cause) + +class NoProbesArrayException(clazz: Class<*>, arrayName: String) : + InstrumentationException( + "No probes array found\n\t" + + "Clazz: $clazz\n\t" + + "Probes array name: $arrayName\n\t" + + "All fields: ${clazz.fields.joinToString { it.name }}" + ) + +class CastProbesArrayException : + InstrumentationException("Can't cast probes array to Boolean array") + +/** + * this exception is thrown only in main process. + * currently it means that {e: Throwable} happened in instrumented process, + * but instrumented process still can operate and not dead. + * on instrumented process death - [InstrumentedProcessDeathException] is thrown +*/ +class InstrumentedProcessError(e: Throwable) : + InstrumentationException("Error in the instrumented process |> ${e.stackTraceToString()}", e) \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/KryoHelper.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/KryoHelper.kt deleted file mode 100644 index 1b6f904d3c..0000000000 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/util/KryoHelper.kt +++ /dev/null @@ -1,152 +0,0 @@ -package org.utbot.instrumentation.util - -import com.esotericsoftware.kryo.kryo5.Kryo -import com.esotericsoftware.kryo.kryo5.Serializer -import com.esotericsoftware.kryo.kryo5.SerializerFactory -import com.esotericsoftware.kryo.kryo5.io.Input -import com.esotericsoftware.kryo.kryo5.io.Output -import com.esotericsoftware.kryo.kryo5.objenesis.instantiator.ObjectInstantiator -import com.esotericsoftware.kryo.kryo5.objenesis.strategy.InstantiatorStrategy -import com.esotericsoftware.kryo.kryo5.objenesis.strategy.StdInstantiatorStrategy -import com.esotericsoftware.kryo.kryo5.serializers.JavaSerializer -import com.esotericsoftware.kryo.kryo5.util.DefaultInstantiatorStrategy -import com.jetbrains.rd.util.lifetime.Lifetime -import com.jetbrains.rd.util.lifetime.throwIfNotAlive -import org.utbot.framework.plugin.api.TimeoutException -import java.io.ByteArrayOutputStream - -/** - * Helpful class for working with the kryo. - */ -class KryoHelper constructor( - private val lifetime: Lifetime -) { - private val outputBuffer = ByteArrayOutputStream() - private val kryoOutput = Output(outputBuffer) - private val kryoInput= Input() - private val sendKryo: Kryo = TunedKryo() - private val receiveKryo: Kryo = TunedKryo() - - init { - lifetime.onTermination { - kryoInput.close() - kryoOutput.close() - } - } - - fun register(clazz: Class, serializer: Serializer) { - sendKryo.register(clazz, serializer) - receiveKryo.register(clazz, serializer) - } - - private fun addInstantiatorOnKryo(kryo: Kryo, clazz: Class, factory: () -> T) { - val instantiator = kryo.instantiatorStrategy - kryo.instantiatorStrategy = object : InstantiatorStrategy { - override fun newInstantiatorOf(type: Class): ObjectInstantiator { - return if (type === clazz) { - ObjectInstantiator { factory() as R } - } - else - instantiator.newInstantiatorOf(type) - } - } - } - fun addInstantiator(clazz: Class, factory: () -> T) { - addInstantiatorOnKryo(sendKryo, clazz, factory) - addInstantiatorOnKryo(receiveKryo, clazz, factory) - } - - fun setKryoClassLoader(classLoader: ClassLoader) { - sendKryo.classLoader = classLoader - receiveKryo.classLoader = classLoader - } - - /** - * Serializes object to ByteArray - * - * @throws WritingToKryoException wraps all exceptions - */ - fun writeObject(obj: T): ByteArray { - lifetime.throwIfNotAlive() - try { - sendKryo.writeClassAndObject(kryoOutput, obj) - kryoOutput.flush() - - return outputBuffer.toByteArray() - } catch (e: Exception) { - throw WritingToKryoException(e) - } finally { - kryoOutput.reset() - outputBuffer.reset() - } - } - - /** - * Deserializes object form ByteArray - * - * @throws ReadingFromKryoException wraps all exceptions - */ - fun readObject(byteArray: ByteArray): T { - lifetime.throwIfNotAlive() - return try { - kryoInput.buffer = byteArray - receiveKryo.readClassAndObject(kryoInput) as T - } catch (e: Exception) { - throw ReadingFromKryoException(e) - } - } -} - -// This kryo is used to initialize collections properly. -internal class TunedKryo : Kryo() { - init { - this.references = true - this.isRegistrationRequired = false - - this.instantiatorStrategy = object : StdInstantiatorStrategy() { - // workaround for Collections as they cannot be correctly deserialized without calling constructor - val default = DefaultInstantiatorStrategy() - val classesBadlyDeserialized = listOf( - java.util.Queue::class.java, - java.util.HashSet::class.java - ) - - override fun newInstantiatorOf(type: Class): ObjectInstantiator { - return if (classesBadlyDeserialized.any { it.isAssignableFrom(type) }) { - @Suppress("UNCHECKED_CAST") - default.newInstantiatorOf(type) as ObjectInstantiator - } else { - super.newInstantiatorOf(type) - } - } - } - - this.setOptimizedGenerics(false) - register(TimeoutException::class.java, TimeoutExceptionSerializer()) - - // TODO: JIRA:1492 - addDefaultSerializer(java.lang.Throwable::class.java, JavaSerializer()) - - val factory = object : SerializerFactory.FieldSerializerFactory() {} - factory.config.ignoreSyntheticFields = true - factory.config.serializeTransient = false - factory.config.fieldsCanBeNull = true - this.setDefaultSerializer(factory) - } - - /** - * Specific serializer for [TimeoutException] - [JavaSerializer] is not applicable - * because [TimeoutException] is not in class loader. - * - * This serializer is very simple - it just writes [TimeoutException.message] - * because we do not need other components. - */ - private class TimeoutExceptionSerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, value: TimeoutException) { - output.writeString(value.message) - } - - override fun read(kryo: Kryo?, input: Input, type: Class?): TimeoutException = - TimeoutException(input.readString()) - } -} \ No newline at end of file diff --git a/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/MockHelper.kt b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/MockHelper.kt index e7b10fc982..b97879559b 100644 --- a/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/MockHelper.kt +++ b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/examples/mock/MockHelper.kt @@ -1,14 +1,14 @@ package org.utbot.instrumentation.examples.mock -import org.utbot.common.withAccessibility -import org.utbot.framework.plugin.api.util.signature -import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter -import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor -import org.utbot.instrumentation.instrumentation.mock.MockConfig import java.lang.reflect.Method import java.util.IdentityHashMap import kotlin.reflect.jvm.javaMethod import org.objectweb.asm.Type +import org.utbot.common.withAccessibility +import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter +import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor +import org.utbot.instrumentation.instrumentation.mock.MockConfig +import org.utbot.instrumentation.instrumentation.mock.computeKeyForMethod /** * Helper for generating tests with methods mocks. @@ -52,8 +52,8 @@ class MockHelper( error("Can't mock function returning void!") } - val sign = method.signature - val methodId = mockClassVisitor.signatureToId[sign] + val computedSignature = computeKeyForMethod(method) + val methodId = mockClassVisitor.signatureToId[computedSignature] val isMockField = instrumentedClazz.getDeclaredField(MockConfig.IS_MOCK_FIELD + methodId) MockGetter.updateMocks(instance, method, mockedValues) @@ -129,7 +129,7 @@ class MockHelper( } fun updateMocks(obj: Any?, method: Method, values: List<*>) { - updateMocks(obj, method.signature, values) + updateMocks(obj, computeKeyForMethod(method), values) } } } \ No newline at end of file diff --git a/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/BaseConstructorTest.kt b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/BaseConstructorTest.kt new file mode 100644 index 0000000000..3410ae8cab --- /dev/null +++ b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/BaseConstructorTest.kt @@ -0,0 +1,42 @@ +package org.utbot.instrumentation.instrumentation.execution.constructors + +import org.utbot.framework.plugin.api.util.constructor.ValueConstructor +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.id +import java.util.IdentityHashMap +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.jClass + +abstract class BaseConstructorTest { + private lateinit var cookie: AutoCloseable + + @BeforeEach + fun setup() { + cookie = UtContext.setUtContext(UtContext(ClassLoader.getSystemClassLoader())) + } + + @AfterEach + fun tearDown() { + cookie.close() + } + + protected fun computeReconstructed(value: T): T { + val model = UtModelConstructor( + objectToModelCache = IdentityHashMap(), + idGenerator = StateBeforeAwareIdGenerator(allPreExistingModels = emptySet()), + utModelWithCompositeOriginConstructorFinder = ::findUtCustomModelConstructor + ).construct(value, value::class.java.id) + + Assertions.assertTrue(model is UtAssembleModel) + + @Suppress("UNCHECKED_CAST") + return ValueConstructor().construct(listOf(model)).single().value as T + } + + protected open fun findUtCustomModelConstructor(classId: ClassId): UtModelWithCompositeOriginConstructor? = + javaStdLibModelWithCompositeOriginConstructors[classId.jClass]?.invoke() +} \ No newline at end of file diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/ListConstructorTest.kt b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/ListConstructorTest.kt similarity index 93% rename from utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/ListConstructorTest.kt rename to utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/ListConstructorTest.kt index e3ef59049e..2b19fe60cf 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/ListConstructorTest.kt +++ b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/ListConstructorTest.kt @@ -1,4 +1,4 @@ -package org.utbot.framework.concrete.constructors +package org.utbot.instrumentation.instrumentation.execution.constructors import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/MapConstructorTest.kt b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/MapConstructorTest.kt similarity index 92% rename from utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/MapConstructorTest.kt rename to utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/MapConstructorTest.kt index e756e02942..919bdb5a71 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/MapConstructorTest.kt +++ b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/MapConstructorTest.kt @@ -1,4 +1,4 @@ -package org.utbot.framework.concrete.constructors +package org.utbot.instrumentation.instrumentation.execution.constructors import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/OptionalConstructorTest.kt b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/OptionalConstructorTest.kt similarity index 95% rename from utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/OptionalConstructorTest.kt rename to utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/OptionalConstructorTest.kt index 7c515f9d55..b2af99a68c 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/OptionalConstructorTest.kt +++ b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/OptionalConstructorTest.kt @@ -1,4 +1,4 @@ -package org.utbot.framework.concrete.constructors +package org.utbot.instrumentation.instrumentation.execution.constructors import java.util.Optional import java.util.OptionalDouble diff --git a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/SetConstructorTest.kt b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/SetConstructorTest.kt similarity index 91% rename from utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/SetConstructorTest.kt rename to utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/SetConstructorTest.kt index 56cfe25ff9..1081f21542 100644 --- a/utbot-framework-test/src/test/kotlin/org/utbot/framework/concrete/constructors/SetConstructorTest.kt +++ b/utbot-instrumentation/src/test/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/SetConstructorTest.kt @@ -1,4 +1,4 @@ -package org.utbot.framework.concrete.constructors +package org.utbot.instrumentation.instrumentation.execution.constructors import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test diff --git a/utbot-intellij-go/build.gradle.kts b/utbot-intellij-go/build.gradle.kts new file mode 100644 index 0000000000..fff30caba1 --- /dev/null +++ b/utbot-intellij-go/build.gradle.kts @@ -0,0 +1,125 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val kotlinPluginVersion: String by rootProject + +// === IDE settings === +val projectType: String by rootProject +val communityEdition: String by rootProject +val ultimateEdition: String by rootProject + +val ideType: String by rootProject +val androidStudioPath: String? by rootProject + +val ideaVersion: String? by rootProject +val pycharmVersion: String? by rootProject +val golandVersion: String? by rootProject + +val javaIde: String? by rootProject +val pythonIde: String? by rootProject +val jsIde: String? by rootProject +val goIde: String? by rootProject + +val ideVersion = when(ideType) { + "PC", "PY" -> pycharmVersion + "GO" -> golandVersion + else -> ideaVersion +} + +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject +val goPluginVersion: String? by rootProject + +// https://plugins.jetbrains.com/docs/intellij/android-studio.html#configuring-the-plugin-pluginxml-file +val ideTypeOrAndroidStudio = if (androidStudioPath == null) ideType else "IC" + +project.tasks.asMap["runIde"]?.enabled = false +// === IDE settings === + +plugins { + id("org.jetbrains.intellij") version "1.13.1" +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation(project(":utbot-ui-commons")) + + //Family + implementation(project(":utbot-go")) +} + +intellij { + + val androidPlugins = listOf("org.jetbrains.android") + + val jvmPlugins = mutableListOf( + "java" + ) + + val kotlinPlugins = listOf( + "org.jetbrains.kotlin" + ) + + androidStudioPath?.let { jvmPlugins += androidPlugins } + + val pythonCommunityPlugins = listOf( + "PythonCore:${pythonCommunityPluginVersion}" + ) + + val pythonUltimatePlugins = listOf( + "Pythonid:${pythonUltimatePluginVersion}" + ) + + val jsPlugins = listOf( + "JavaScript" + ) + + val goPlugins = listOf( + "org.jetbrains.plugins.go:${goPluginVersion}" + ) + + val mavenUtilsPlugins = listOf( + "org.jetbrains.idea.maven" + ) + + val basePluginSet = jvmPlugins + kotlinPlugins + mavenUtilsPlugins + androidPlugins + + plugins.set( + when (projectType) { + communityEdition -> basePluginSet + pythonCommunityPlugins + ultimateEdition -> when (ideType) { + "IC" -> basePluginSet + pythonCommunityPlugins + "IU" -> basePluginSet + pythonUltimatePlugins + jsPlugins + goPlugins + "PC" -> pythonCommunityPlugins + "PY" -> pythonUltimatePlugins + jsPlugins + "GO" -> goPlugins + else -> basePluginSet + } + else -> basePluginSet + } + ) + + version.set(ideVersion) + type.set(ideType) +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/generator/GoUtTestsCodeFileWriter.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/generator/GoUtTestsCodeFileWriter.kt new file mode 100644 index 0000000000..19b81b6421 --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/generator/GoUtTestsCodeFileWriter.kt @@ -0,0 +1,61 @@ +package org.utbot.intellij.plugin.go.generator + +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.PsiManager +import com.intellij.util.IncorrectOperationException +import org.utbot.go.api.GoUtFile +import org.utbot.intellij.plugin.go.language.GoLanguageAssistant +import org.utbot.intellij.plugin.go.models.GenerateGoTestsModel +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import java.nio.file.Paths + +// This class is highly inspired by CodeGenerationController. +object GoUtTestsCodeFileWriter { + + fun createTestsFileWithGeneratedCode( + model: GenerateGoTestsModel, + sourceFile: GoUtFile, + generatedTestsFileCode: String + ) { + val testsFileName = createTestsFileName(sourceFile) + try { + runWriteAction { + val sourcePsiFile = findPsiFile(model, sourceFile) + val sourceFileDir = sourcePsiFile.containingDirectory + + val testsFileNameWithExtension = "$testsFileName.go" + val testPsiFile = PsiFileFactory.getInstance(model.project) + .createFileFromText( + testsFileNameWithExtension, GoLanguageAssistant.language, generatedTestsFileCode + ) + sourceFileDir.findFile(testsFileNameWithExtension)?.delete() + sourceFileDir.add(testPsiFile) + + val testFile = sourceFileDir.findFile(testsFileNameWithExtension)!! + OpenFileDescriptor(model.project, testFile.virtualFile).navigate(true) + } + } catch (e: IncorrectOperationException) { + showCreatingFileError(model.project, testsFileName) + } + } + + private fun findPsiFile(model: GenerateGoTestsModel, sourceFile: GoUtFile): PsiFile { + val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(Paths.get(sourceFile.absolutePath))!! + return PsiManager.getInstance(model.project).findFile(virtualFile)!! + } + + private fun createTestsFileName(sourceFile: GoUtFile) = sourceFile.fileNameWithoutExtension + "_go_ut_test" + + private fun showCreatingFileError(project: Project, testFileName: String) { + showErrorDialogLater( + project, + message = "Cannot Create File '$testFileName'", + title = "Failed to Create File" + ) + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/generator/GoUtTestsDialogProcessor.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/generator/GoUtTestsDialogProcessor.kt new file mode 100644 index 0000000000..b5c1fcdd9d --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/generator/GoUtTestsDialogProcessor.kt @@ -0,0 +1,138 @@ +package org.utbot.intellij.plugin.go.generator + +import com.goide.project.DefaultGoRootsProvider +import com.goide.psi.GoFunctionOrMethodDeclaration +import com.goide.sdk.GoSdk +import com.goide.sdk.GoSdkService +import com.goide.sdk.GoSdkVersion +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.module.Module +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import org.utbot.go.gocodeanalyzer.GoParsingSourceCodeAnalysisResultException +import org.utbot.go.logic.GoUtTestsGenerationConfig +import org.utbot.go.worker.GoWorkerFailedException +import org.utbot.intellij.plugin.go.models.GenerateGoTestsModel +import org.utbot.intellij.plugin.go.ui.GenerateGoTestsDialogWindow +import org.utbot.intellij.plugin.go.ui.utils.resolveGoExecutablePath +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import java.nio.file.Paths + +object GoUtTestsDialogProcessor { + + private const val helpMessage: String = + "Please try running \"go mod tidy\" in one of the project directories or fix any errors in the code." + + fun createDialogAndGenerateTests( + project: Project, + module: Module, + targetFunctions: Set, + focusedTargetFunctions: Set, + ) { + createDialog(project, module, targetFunctions, focusedTargetFunctions)?.let { + if (it.showAndGet()) createTests(it.model) + } + } + + private fun createDialog( + project: Project, + module: Module, + targetFunctions: Set, + focusedTargetFunctions: Set, + ): GenerateGoTestsDialogWindow? { + val goSdk = GoSdkService.getInstance(project).getSdk(module) + if (goSdk == GoSdk.NULL) { + val result = Messages.showOkCancelDialog( + project, + "GOROOT is not defined. Select it?", + "Unsupported Go SDK", + "Select", + "Cancel", + Messages.getErrorIcon() + ) + if (result == Messages.OK) { + ShowSettingsUtil.getInstance().showSettingsDialog(project, "GOROOT") + } + return null + } else if (!goSdk.isValid || GoSdkVersion.fromText(goSdk.version).isLessThan(GoSdkVersion.GO_1_18)) { + val result = Messages.showOkCancelDialog( + project, + "Go SDK isn't valid or version less than 1.18. Select another SDK?", + "Unsupported Go SDK", + "Select", + "Cancel", + Messages.getErrorIcon() + ) + if (result == Messages.OK) { + ShowSettingsUtil.getInstance().showSettingsDialog(project, "GOROOT") + } + return null + } + + val goPath = DefaultGoRootsProvider().getGoPathRoots(project, module).first().path + return GenerateGoTestsDialogWindow( + GenerateGoTestsModel( + project, + goExecutableAbsolutePath = Paths.get(goSdk.resolveGoExecutablePath()!!).toAbsolutePath(), + gopathAbsolutePath = Paths.get(goPath).toAbsolutePath(), + targetFunctions, + focusedTargetFunctions, + ) + ) + } + + private fun buildErrorMessage(exception: Exception): String = + if (exception.message == null) { + helpMessage + } else { + buildString { + appendLine(exception.message) + appendLine(helpMessage) + } + } + + private fun createTests(model: GenerateGoTestsModel) { + ProgressManager.getInstance().run(object : Task.Backgroundable(model.project, "Generate Go tests") { + override fun run(indicator: ProgressIndicator) { + // readAction is required to read PSI-tree or else "Read access" exception occurs. + val (selectedFunctionNamesBySourceFiles, selectedMethodNamesBySourceFiles) = runReadAction { + model.selectedFunctions.groupBy({ Paths.get(it.containingFile.virtualFile.path) }) { it.name!! } to + model.selectedMethods.groupBy({ Paths.get(it.containingFile.virtualFile.path) }) { it.name!! } + } + val testsGenerationConfig = GoUtTestsGenerationConfig( + model.goExecutableAbsolutePath, + model.gopathAbsolutePath, + model.numberOfFuzzingProcess, + model.mode, + model.eachFunctionExecutionTimeoutMillis, + model.allFunctionExecutionTimeoutMillis + ) + + try { + IntellijGoUtTestsGenerationController(model, indicator).generateTests( + selectedFunctionNamesBySourceFiles, + selectedMethodNamesBySourceFiles, + testsGenerationConfig + ) { indicator.isCanceled } + } catch (e: GoParsingSourceCodeAnalysisResultException) { + val errorMessage = buildErrorMessage(e) + showErrorDialogLater( + model.project, + errorMessage, + title = "Unit tests generation is cancelled" + ) + } catch (e: GoWorkerFailedException) { + showErrorDialogLater( + model.project, + helpMessage, + title = "Unit tests generation is cancelled" + ) + } + } + }) + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/generator/IntellijGoUtTestsGenerationController.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/generator/IntellijGoUtTestsGenerationController.kt new file mode 100644 index 0000000000..b5a89b4c17 --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/generator/IntellijGoUtTestsGenerationController.kt @@ -0,0 +1,153 @@ +package org.utbot.intellij.plugin.go.generator + +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.progress.ProgressIndicator +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.gocodeanalyzer.GoSourceCodeAnalyzer +import org.utbot.go.logic.AbstractGoUtTestsGenerationController +import org.utbot.intellij.plugin.go.models.GenerateGoTestsModel +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.intellij.plugin.ui.utils.showWarningDialogLater +import java.nio.file.Path + +class IntellijGoUtTestsGenerationController( + private val model: GenerateGoTestsModel, + private val indicator: ProgressIndicator +) : AbstractGoUtTestsGenerationController() { + + private object ProgressIndicatorConstants { + const val START_FRACTION = 0.05 // is needed to prevent infinite indicator that appears for 0.0 + const val SOURCE_CODE_ANALYSIS_FRACTION = 0.25 + const val TEST_CASES_CODE_GENERATION_FRACTION = 0.1 + const val TEST_CASES_GENERATION_FRACTION = + 1.0 - SOURCE_CODE_ANALYSIS_FRACTION - TEST_CASES_CODE_GENERATION_FRACTION + } + + private lateinit var testCasesGenerationCounter: ProcessedFilesCounter + private lateinit var testCasesCodeGenerationCounter: ProcessedFilesCounter + + private data class ProcessedFilesCounter( + private val toProcessTotalFilesNumber: Int, + private var processedFiles: Int = 0 + ) { + val processedFilesRatio: Double get() = processedFiles.toDouble() / toProcessTotalFilesNumber + fun addProcessedFile() { + processedFiles++ + } + } + + override fun onSourceCodeAnalysisStart( + targetFunctionNamesBySourceFiles: Map>, + targetMethodNamesBySourceFiles: Map> + ): Boolean { + indicator.isIndeterminate = false + indicator.text = "Analyze source files" + indicator.fraction = ProgressIndicatorConstants.START_FRACTION + return true + } + + override fun onSourceCodeAnalysisFinished( + analysisResults: Map + ): Boolean { + indicator.fraction = indicator.fraction.coerceAtLeast( + ProgressIndicatorConstants.START_FRACTION + ProgressIndicatorConstants.SOURCE_CODE_ANALYSIS_FRACTION + ) + if (!handleMissingSelectedFunctions(analysisResults)) return false + + val filesToProcessTotalNumber = + analysisResults.count { (_, analysisResult) -> analysisResult.functions.isNotEmpty() } + testCasesGenerationCounter = ProcessedFilesCounter(filesToProcessTotalNumber) + testCasesCodeGenerationCounter = ProcessedFilesCounter(filesToProcessTotalNumber) + return true + } + + override fun onPackageInstrumentationStart(): Boolean { + return true + } + + override fun onPackageInstrumentationFinished(): Boolean { + return true + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsStart( + sourceFile: GoUtFile, + functions: List + ): Boolean { + indicator.text = "Generate test cases for ${sourceFile.fileName}" + indicator.fraction = indicator.fraction.coerceAtLeast( + ProgressIndicatorConstants.START_FRACTION + + ProgressIndicatorConstants.SOURCE_CODE_ANALYSIS_FRACTION + + ProgressIndicatorConstants.TEST_CASES_GENERATION_FRACTION + * testCasesGenerationCounter.processedFilesRatio + ) + indicator.checkCanceled() // allow user to cancel possibly slow unit test generation + return true + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsFinished( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + testCasesGenerationCounter.addProcessedFile() + return true + } + + override fun onTestCasesFileCodeGenerationStart( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + indicator.text = "Generate tests code for ${sourceFile.fileName}" + indicator.fraction = indicator.fraction.coerceAtLeast( + ProgressIndicatorConstants.START_FRACTION + + ProgressIndicatorConstants.SOURCE_CODE_ANALYSIS_FRACTION + + ProgressIndicatorConstants.TEST_CASES_GENERATION_FRACTION + + ProgressIndicatorConstants.TEST_CASES_CODE_GENERATION_FRACTION + * testCasesCodeGenerationCounter.processedFilesRatio + ) + return true + } + + override fun onTestCasesFileCodeGenerationFinished( + sourceFile: GoUtFile, + generatedTestsFileCode: String + ): Boolean { + invokeLater { + GoUtTestsCodeFileWriter.createTestsFileWithGeneratedCode(model, sourceFile, generatedTestsFileCode) + } + testCasesCodeGenerationCounter.addProcessedFile() + return true + } + + private fun handleMissingSelectedFunctions( + analysisResults: Map + ): Boolean { + val missingSelectedFunctionsListMessage = generateMissingSelectedFunctionsListMessage(analysisResults) + val okSelectedFunctionsArePresent = + analysisResults.any { (_, analysisResult) -> analysisResult.functions.isNotEmpty() } + + if (missingSelectedFunctionsListMessage == null) { + return okSelectedFunctionsArePresent + } + + val errorMessageSb = StringBuilder() + .append("Some selected functions were skipped during source code analysis.") + .append(missingSelectedFunctionsListMessage) + if (okSelectedFunctionsArePresent) { + showWarningDialogLater( + model.project, + errorMessageSb.append("Unit test generation for other selected functions will be performed as usual.") + .toString(), + title = "Skipped some functions for unit tests generation" + ) + } else { + showErrorDialogLater( + model.project, + errorMessageSb.append("Unit test generation is cancelled: no other selected functions.").toString(), + title = "Unit tests generation is cancelled" + ) + } + return okSelectedFunctionsArePresent + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/language/GoLanguageAssistant.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/language/GoLanguageAssistant.kt new file mode 100644 index 0000000000..f88c243ab8 --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/language/GoLanguageAssistant.kt @@ -0,0 +1,104 @@ +package org.utbot.intellij.plugin.go.language + +import com.goide.psi.* +import com.intellij.lang.Language +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import org.utbot.intellij.plugin.go.generator.GoUtTestsDialogProcessor + +@Suppress("unused") // is used in org.utbot.intellij.plugin.language.agnostic.LanguageAssistant via reflection +object GoLanguageAssistant : LanguageAssistant() { + + private const val goId = "go" + val language: Language = Language.findLanguageByID(goId) ?: error("Go language wasn't found") + + private data class PsiTargets( + val targetFunctions: Set, + val focusedTargetFunctions: Set, + ) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val file = e.getData(CommonDataKeys.PSI_FILE) as? GoFile ?: return + val module = ModuleUtilCore.findModuleForFile(file) ?: return + val (targetFunctions, focusedTargetFunctions) = getPsiTargets(e) ?: return + GoUtTestsDialogProcessor.createDialogAndGenerateTests( + project, + module, + targetFunctions, + focusedTargetFunctions + ) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): PsiTargets? { + e.project ?: return null + + val editor = e.getData(CommonDataKeys.EDITOR) + val file = e.getData(CommonDataKeys.PSI_FILE) as? GoFile ?: return null + val element = if (editor != null) { + findPsiElement(file, editor) ?: return null + } else { + e.getData(CommonDataKeys.PSI_ELEMENT) ?: return null + } + + val targetFunctions = extractTargetFunctionsOrMethods(file) + val containingFunctionOrMethod = getContainingFunctionOrMethod(element) + val containingStruct = getContainingStruct(element) + val focusedTargetFunctions = if (containingFunctionOrMethod != null) { + setOf(containingFunctionOrMethod) + } else { + if (containingStruct != null) { + targetFunctions.filterIsInstance() + .filter { containingStruct == getMethodReceiverStruct(it) }.toSet() + } else { + emptySet() + } + } + + return PsiTargets(targetFunctions, focusedTargetFunctions) + } + + private fun extractTargetFunctionsOrMethods(file: GoFile): Set { + return file.functions.toSet() union file.methods.toSet() + } + + private fun getContainingFunctionOrMethod(element: PsiElement): GoFunctionOrMethodDeclaration? { + if (element is GoFunctionOrMethodDeclaration) + return element + + val parent = element.parent ?: return null + return getContainingFunctionOrMethod(parent) + } + + private fun getContainingStruct(element: PsiElement): GoStructType? = + PsiTreeUtil.getParentOfType(element, GoStructType::class.java, false) + + private fun getMethodReceiverStruct(method: GoMethodDeclaration): GoStructType? { + val receiverType = method.receiverType?.contextlessUnderlyingType ?: return null + if (receiverType is GoPointerType) { + return receiverType.type?.contextlessUnderlyingType as? GoStructType + } + return receiverType as? GoStructType + } + + // This method is cloned from GenerateTestsActions.kt. + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + + return element + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/models/GenerateGoTestsModel.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/models/GenerateGoTestsModel.kt new file mode 100644 index 0000000000..0b62e84649 --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/models/GenerateGoTestsModel.kt @@ -0,0 +1,33 @@ +package org.utbot.intellij.plugin.go.models + +import com.goide.psi.GoFunctionDeclaration +import com.goide.psi.GoFunctionOrMethodDeclaration +import com.goide.psi.GoMethodDeclaration +import com.intellij.openapi.project.Project +import org.utbot.go.logic.GoUtTestsGenerationConfig +import org.utbot.go.logic.TestsGenerationMode +import java.nio.file.Path + +/** + * Contains information about Go tests generation task required for intellij plugin logic. + * + * targetFunctions: all possible functions to generate tests for; + * focusedTargetFunctions: such target functions that user is focused on while plugin execution; + * selectedFunctions: finally selected functions to generate tests for; + * goExecutableAbsolutePath: self-explanatory; + * eachFunctionExecutionTimeoutMillis: timeout in milliseconds for each fuzzed function execution. + */ +data class GenerateGoTestsModel( + val project: Project, + val goExecutableAbsolutePath: Path, + val gopathAbsolutePath: Path, + val targetFunctions: Set, + val focusedTargetFunctions: Set, +) { + lateinit var selectedFunctions: Set + lateinit var selectedMethods: Set + var numberOfFuzzingProcess: Int = GoUtTestsGenerationConfig.DEFAULT_NUMBER_OF_FUZZING_PROCESSES + var mode: TestsGenerationMode = TestsGenerationMode.DEFAULT + var eachFunctionExecutionTimeoutMillis: Long = GoUtTestsGenerationConfig.DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS + var allFunctionExecutionTimeoutMillis: Long = GoUtTestsGenerationConfig.DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/ui/GenerateGoTestsDialogWindow.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/ui/GenerateGoTestsDialogWindow.kt new file mode 100644 index 0000000000..b24febe97e --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/ui/GenerateGoTestsDialogWindow.kt @@ -0,0 +1,147 @@ +package org.utbot.intellij.plugin.go.ui + +import com.goide.psi.GoFunctionDeclaration +import com.goide.psi.GoFunctionOrMethodDeclaration +import com.goide.psi.GoMethodDeclaration +import com.goide.refactor.ui.GoDeclarationInfo +import com.intellij.openapi.components.service +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.JBIntSpinner +import com.intellij.ui.components.JBLabel +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import org.utbot.go.logic.GoUtTestsGenerationConfig +import org.utbot.go.logic.TestsGenerationMode +import org.utbot.intellij.plugin.go.models.GenerateGoTestsModel +import org.utbot.intellij.plugin.settings.Settings +import java.text.ParseException +import java.util.concurrent.TimeUnit +import javax.swing.JCheckBox +import javax.swing.JComponent + +private const val MINIMUM_ALL_EXECUTION_TIMEOUT_SECONDS = 1 +private const val ALL_EXECUTION_TIMEOUT_SECONDS_SPINNER_STEP = 10 + +private const val MINIMUM_NUMBER_OF_FUZZING_PROCESSES = 1 +private const val NUMBER_OF_FUZZING_PROCESSES_STEP = 1 + +// This class is highly inspired by GenerateTestsDialogWindow. +class GenerateGoTestsDialogWindow(val model: GenerateGoTestsModel) : DialogWrapper(model.project) { + + private val targetInfos = model.targetFunctions.toInfos() + private val targetFunctionsTable = GoFunctionsSelectionTable(targetInfos).apply { + val height = this.rowHeight * (targetInfos.size.coerceAtMost(12) + 1) + this.preferredScrollableViewportSize = JBUI.size(-1, height) + } + private val numberOfFuzzingProcessesSpinner: JBIntSpinner = JBIntSpinner( + GoUtTestsGenerationConfig.DEFAULT_NUMBER_OF_FUZZING_PROCESSES, + MINIMUM_NUMBER_OF_FUZZING_PROCESSES, + Int.MAX_VALUE, + NUMBER_OF_FUZZING_PROCESSES_STEP + ) + private val fuzzingMode = JCheckBox("Fuzzing mode") + + private val allFunctionExecutionTimeoutSecondsSpinner = + JBIntSpinner( + TimeUnit.MILLISECONDS.toSeconds(GoUtTestsGenerationConfig.DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS).toInt(), + MINIMUM_ALL_EXECUTION_TIMEOUT_SECONDS, + Int.MAX_VALUE, + ALL_EXECUTION_TIMEOUT_SECONDS_SPINNER_STEP + ) + + private lateinit var panel: DialogPanel + + init { + title = "Generate Tests with UnitTestBot" + isResizable = false + init() + } + + override fun createCenterPanel(): JComponent { + panel = panel { + row("Timeout for all functions:") { + cell(allFunctionExecutionTimeoutSecondsSpinner) + cell(JBLabel("seconds")) + } + row("Number of fuzzing processes:") { + cell(numberOfFuzzingProcessesSpinner) + } + row { + cell(fuzzingMode) + contextHelp("Stop test generation when a panic or error occurs (only one test will be generated for one of these cases)") + } + row("Generate test methods for:") {} + row { + scrollCell(targetFunctionsTable).align(Align.FILL) + } + } + updateFunctionsOrMethodsTable() + return panel + } + + override fun doOKAction() { + model.selectedFunctions = + targetFunctionsTable.selectedMemberInfos.fromInfos().filterIsInstance().toSet() + model.selectedMethods = + targetFunctionsTable.selectedMemberInfos.fromInfos().filterIsInstance().toSet() + try { + numberOfFuzzingProcessesSpinner.commitEdit() + allFunctionExecutionTimeoutSecondsSpinner.commitEdit() + } catch (_: ParseException) { + } + model.numberOfFuzzingProcess = numberOfFuzzingProcessesSpinner.number + model.mode = if (fuzzingMode.isSelected) { + TestsGenerationMode.FUZZING_MODE + } else { + TestsGenerationMode.DEFAULT + } + val settings = model.project.service() + with(settings) { + model.eachFunctionExecutionTimeoutMillis = hangingTestsTimeout.timeoutMs + } + model.allFunctionExecutionTimeoutMillis = + TimeUnit.SECONDS.toMillis(allFunctionExecutionTimeoutSecondsSpinner.number.toLong()) + super.doOKAction() + } + + private fun updateFunctionsOrMethodsTable() { + val focusedTargetFunctionsNames = model.focusedTargetFunctions.map { it.name }.toSet() + val selectedInfos = targetInfos.filter { + it.declaration.name in focusedTargetFunctionsNames + } + if (selectedInfos.isEmpty()) { + checkInfos(targetInfos) + } else { + checkInfos(selectedInfos) + } + targetFunctionsTable.setMemberInfos(targetInfos) + } + + private fun checkInfos(infos: Collection) { + infos.forEach { it.isChecked = true } + } + + private fun Collection.toInfos(): Set = + this.map { GoDeclarationInfo(it) }.toSet() + + private fun Collection.fromInfos(): Set = + this.map { it.declaration as GoFunctionOrMethodDeclaration }.toSet() + + @Suppress("DuplicatedCode") // This method is highly inspired by GenerateTestsDialogWindow.doValidate(). + override fun doValidate(): ValidationInfo? { + targetFunctionsTable.tableHeader?.background = UIUtil.getTableBackground() + targetFunctionsTable.background = UIUtil.getTableBackground() + if (targetFunctionsTable.selectedMemberInfos.isEmpty()) { + targetFunctionsTable.tableHeader?.background = JBUI.CurrentTheme.Validator.errorBackgroundColor() + targetFunctionsTable.background = JBUI.CurrentTheme.Validator.errorBackgroundColor() + return ValidationInfo( + "Tick any methods to generate tests for", targetFunctionsTable.componentPopupMenu + ) + } + return null + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/ui/GoFunctionsSelectionTable.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/ui/GoFunctionsSelectionTable.kt new file mode 100644 index 0000000000..ee6203c875 --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/ui/GoFunctionsSelectionTable.kt @@ -0,0 +1,32 @@ +package org.utbot.intellij.plugin.go.ui + +import com.goide.psi.GoNamedElement +import com.goide.refactor.ui.GoDeclarationInfo +import com.intellij.refactoring.ui.AbstractMemberSelectionTable +import com.intellij.ui.RowIcon +import com.intellij.util.PlatformIcons +import javax.swing.Icon + +class GoFunctionsSelectionTable(infos: Set) : + AbstractMemberSelectionTable(infos, null, null) { + + override fun getAbstractColumnValue(info: GoDeclarationInfo): Boolean { + return info.isToAbstract + } + + override fun isAbstractColumnEditable(rowIndex: Int): Boolean { + return myMemberInfoModel.isAbstractEnabled(myMemberInfos[rowIndex] as GoDeclarationInfo) + } + + override fun getOverrideIcon(memberInfo: GoDeclarationInfo): Icon? = null + + override fun setVisibilityIcon(memberInfo: GoDeclarationInfo, icon_: com.intellij.ui.icons.RowIcon?) { + val icon = icon_ as RowIcon + val iconToSet = if (memberInfo.declaration.isPublic) { + PlatformIcons.PUBLIC_ICON + } else { + PlatformIcons.PRIVATE_ICON + } + icon.setIcon(iconToSet, VISIBILITY_ICON_POSITION) + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/ui/utils/GoSdkUtils.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/ui/utils/GoSdkUtils.kt new file mode 100644 index 0000000000..fd385cc82c --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/go/ui/utils/GoSdkUtils.kt @@ -0,0 +1,9 @@ +package org.utbot.intellij.plugin.go.ui.utils + +import com.goide.sdk.GoSdk +import java.nio.file.Paths + +fun GoSdk.resolveGoExecutablePath(): String? { + val canonicalGoSdkPath = this.executable?.canonicalPath ?: return null + return Paths.get(canonicalGoSdkPath).toAbsolutePath().toString() +} \ No newline at end of file diff --git a/utbot-intellij-js/build.gradle.kts b/utbot-intellij-js/build.gradle.kts new file mode 100644 index 0000000000..916c540eed --- /dev/null +++ b/utbot-intellij-js/build.gradle.kts @@ -0,0 +1,125 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject + +// === IDE settings === +val projectType: String by rootProject +val communityEdition: String by rootProject +val ultimateEdition: String by rootProject + +val ideType: String by rootProject +val androidStudioPath: String? by rootProject + +val ideaVersion: String? by rootProject +val pycharmVersion: String? by rootProject +val golandVersion: String? by rootProject + +val javaIde: String? by rootProject +val pythonIde: String? by rootProject +val jsIde: String? by rootProject +val goIde: String? by rootProject + +val ideVersion = when(ideType) { + "PC", "PY" -> pycharmVersion + "GO" -> golandVersion + else -> ideaVersion +} + +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject +val goPluginVersion: String? by rootProject + +// https://plugins.jetbrains.com/docs/intellij/android-studio.html#configuring-the-plugin-pluginxml-file +val ideTypeOrAndroidStudio = if (androidStudioPath == null) ideType else "IC" + +project.tasks.asMap["runIde"]?.enabled = false +// === IDE settings === + +plugins { + id("org.jetbrains.intellij") version "1.13.1" +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation(project(":utbot-ui-commons")) + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + + //Family + implementation(project(":utbot-js")) +} + +intellij { + + val androidPlugins = listOf("org.jetbrains.android") + + val jvmPlugins = mutableListOf( + "java" + ) + + val kotlinPlugins = listOf( + "org.jetbrains.kotlin" + ) + + androidStudioPath?.let { jvmPlugins += androidPlugins } + + val pythonCommunityPlugins = listOf( + "PythonCore:${pythonCommunityPluginVersion}" + ) + + val pythonUltimatePlugins = listOf( + "Pythonid:${pythonUltimatePluginVersion}" + ) + + val jsPlugins = listOf( + "JavaScript" + ) + + val goPlugins = listOf( + "org.jetbrains.plugins.go:${goPluginVersion}" + ) + + val mavenUtilsPlugins = listOf( + "org.jetbrains.idea.maven" + ) + + val basePluginSet = jvmPlugins + kotlinPlugins + mavenUtilsPlugins + androidPlugins + + plugins.set( + when (projectType) { + communityEdition -> basePluginSet + pythonCommunityPlugins + ultimateEdition -> when (ideType) { + "IC" -> basePluginSet + pythonCommunityPlugins + "IU" -> basePluginSet + pythonUltimatePlugins + jsPlugins + goPlugins + "PC" -> pythonCommunityPlugins + "PY" -> pythonUltimatePlugins + jsPlugins + "GO" -> goPlugins + else -> basePluginSet + } + else -> basePluginSet + } + ) + + version.set(ideVersion) + type.set(ideType) +} \ No newline at end of file diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/CoverageModeButtons.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/CoverageModeButtons.kt new file mode 100644 index 0000000000..c2f8c2f1e6 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/CoverageModeButtons.kt @@ -0,0 +1,33 @@ +package org.utbot.intellij.plugin.js + +import javax.swing.ButtonGroup +import javax.swing.JRadioButton +import service.coverage.CoverageMode + +object CoverageModeButtons { + + var mode = CoverageMode.FAST + + val fastButton = JRadioButton("Fast") + val baseButton = JRadioButton("Basic") + + + init { + val buttonGroup = ButtonGroup() + fastButton.isSelected = true + val baseButtonModel = baseButton.model + baseButtonModel.addChangeListener { + if (baseButtonModel.isPressed) { + mode = CoverageMode.BASIC + } + } + val fastButtonModel = fastButton.model + fastButtonModel.addChangeListener { + if (baseButtonModel.isPressed) { + mode = CoverageMode.FAST + } + } + buttonGroup.add(fastButton) + buttonGroup.add(baseButton) + } +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/JsDialogProcessor.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/JsDialogProcessor.kt new file mode 100644 index 0000000000..164c2b0929 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/JsDialogProcessor.kt @@ -0,0 +1,312 @@ +package org.utbot.intellij.plugin.js + +import api.JsTestGenerator +import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreterManager +import com.intellij.lang.ecmascript6.psi.ES6Class +import com.intellij.lang.javascript.psi.JSFile +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.impl.file.PsiDirectoryFactory +import com.intellij.util.concurrency.AppExecutorUtil +import mu.KotlinLogging +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.intellij.plugin.js.language.JsLanguageAssistant +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import settings.JsDynamicSettings +import settings.JsExportsSettings.endComment +import settings.JsExportsSettings.startComment +import settings.JsTestGenerationSettings.dummyClassName +import settings.PackageDataService +import settings.jsPackagesList +import utils.JsCmdExec +import utils.OsProvider +import java.io.File +import java.io.IOException + +private val logger = KotlinLogging.logger {} + +object JsDialogProcessor { + + fun createDialogAndGenerateTests( + project: Project, + srcModule: Module, + fileMethods: Set, + focusedMethod: JSMemberInfo?, + containingFilePath: String, + editor: Editor?, + file: JSFile + ) { + val model = + createJsTestModel(project, srcModule, fileMethods, focusedMethod, containingFilePath, file) ?: return + (object : Task.Backgroundable( + project, + "Check the requirements" + ) { + override fun run(indicator: ProgressIndicator) { + invokeLater { + if (!PackageDataService( + model.containingFilePath, model.project.basePath!!, model.pathToNPM + ).checkAndInstallRequirements(project) + ) return@invokeLater + createDialog(model)?.let { dialogWindow -> + if (!dialogWindow.showAndGet()) return@invokeLater + // Since Tern.js accesses containing file, sync with file system required before test generation. + editor?.let { + runWriteAction { + with(FileDocumentManager.getInstance()) { + saveDocument(editor.document) + } + } + } + createTests( + dialogWindow.model, + containingFilePath, + editor, + dialogWindow.model.file.getContent() + ) + } + } + } + }).queue() + } + + private fun findNodeAndNPM(): Pair? = try { + val pathToNode = + NodeJsLocalInterpreterManager.getInstance().interpreters.first().interpreterSystemIndependentPath + val (_, errorText) = JsCmdExec.runCommand( + shouldWait = true, cmd = arrayOf("\"${pathToNode}\"", "-v") + ) + if (errorText.isNotEmpty()) throw NoSuchElementException() + val pathToNPM = + pathToNode.substringBeforeLast("/") + "/" + "npm" + OsProvider.getProviderByOs().npmPackagePostfix + pathToNode to pathToNPM + } catch (e: NoSuchElementException) { + Messages.showErrorDialog( + "Node.js interpreter is not found in IDEA settings.\n" + "Please set it in Settings > Languages & Frameworks > Node.js", + "Requirement Error" + ) + logger.error { "Node.js interpreter was not found in IDEA settings." } + null + } catch (e: IOException) { + Messages.showErrorDialog( + "Node.js interpreter path is corrupted in IDEA settings.\n" + "Please check Settings > Languages & Frameworks > Node.js", + "Requirement Error" + ) + logger.error { "Node.js interpreter path is corrupted in IDEA settings." } + null + } + + private fun createJsTestModel( + project: Project, + srcModule: Module, + fileMethods: Set, + focusedMethod: JSMemberInfo?, + filePath: String, + file: JSFile + ): JsTestsModel? { + val testModules = srcModule.testModules() + + if (testModules.isEmpty()) { + val errorMessage = """ + No test source roots found in the project.
    + Please, create or configure at least one test source root. + """.trimIndent() + showErrorDialogLater(project, errorMessage, "Test source roots not found") + return null + } + val (pathToNode, pathToNPM) = findNodeAndNPM() ?: return null + return JsTestsModel( + project = project, + potentialTestModules = testModules, + file = file, + fileMethods = fileMethods, + selectedMethods = if (focusedMethod != null) setOf(focusedMethod) else emptySet(), + ).apply { + containingFilePath = filePath + this.pathToNode = pathToNode + this.pathToNPM = pathToNPM + } + } + + private fun createDialog(jsTestsModel: JsTestsModel?) = jsTestsModel?.let { JsDialogWindow(it) } + + private fun unblockDocument(project: Project, document: Document) { + PsiDocumentManager.getInstance(project).apply { + commitDocument(document) + doPostponedOperationsAndUnblockDocument(document) + } + } + + private fun createTests(model: JsTestsModel, containingFilePath: String, editor: Editor?, contents: String) { + val normalizedContainingFilePath = containingFilePath.replace(File.separator, "/") + (object : Task.Backgroundable(model.project, "Generate tests") { + override fun run(indicator: ProgressIndicator) { + indicator.isIndeterminate = false + indicator.text = "Generate tests: read classes" + val testDir = PsiDirectoryFactory.getInstance(project).createDirectory( + model.testSourceRoot!! + ) + val testFileName = normalizedContainingFilePath.substringAfterLast("/").replace(Regex(".js"), "Test.js") + currentFileText = model.file.getContent() + val testGenerator = JsTestGenerator( + fileText = contents, + sourceFilePath = normalizedContainingFilePath, + projectPath = model.project.basePath?.replace(File.separator, "/") + ?: throw IllegalStateException("Can't access project path."), + selectedMethods = runReadAction { + model.selectedMethods.map { + it.member.name!! + } + }, + parentClassName = runReadAction { + val name = (model.selectedMethods.first().member.parent as ES6Class).name + if (name == dummyClassName) null else name + }, + outputFilePath = "${testDir.virtualFile.path}/$testFileName".replace(File.separator, "/"), + exportsManager = partialApplication( + JsDialogProcessor::manageExports, editor, project, model + ), + settings = JsDynamicSettings( + pathToNode = model.pathToNode, + pathToNYC = model.pathToNYC, + pathToNPM = model.pathToNPM, + timeout = model.timeout, + coverageMode = model.coverageMode + ), + isCancelled = { indicator.isCanceled }) + + indicator.fraction = indicator.fraction.coerceAtLeast(0.9) + indicator.text = "Generate code for tests" + + val generatedCode = testGenerator.run() + invokeLater { + runWriteAction { + val testPsiFile = testDir.findFile(testFileName) ?: run { + val temp = PsiFileFactory.getInstance(project) + .createFileFromText(testFileName, JsLanguageAssistant.jsLanguage, generatedCode) + testDir.add(temp) + testDir.findFile(testFileName)!! + } + OpenFileDescriptor(project, testPsiFile.virtualFile).navigate(true) + } + } + } + }).queue() + } + + private fun partialApplication(f: (A, B, C, D) -> Unit, a: A, b: B, c: C): (D) -> Unit { + return { d: D -> f(a, b, c, d) } + } + + private fun JSFile.getContent(): String = this.viewProvider.contents.toString() + + private fun Project.setNewText(editor: Editor?, filePath: String, text: String) { + editor?.let { + runWriteAction { + with(editor.document) { + unblockDocument(this@setNewText, this@with) + setText(text) + unblockDocument(this@setNewText, this@with) + } + with(FileDocumentManager.getInstance()) { + saveDocument(editor.document) + } + } + } ?: run { + File(filePath).writeText(text) + } + currentFileText = text + } + + // Needed for continuous exports managing + private var currentFileText = "" + + private fun manageExports( + editor: Editor?, + project: Project, + model: JsTestsModel, + swappedText: (String?, String) -> String + ) { + AppExecutorUtil.getAppExecutorService().submit { + invokeLater { + when { + currentFileText.contains(startComment) -> { + val regex = Regex("$startComment((\\r\\n|\\n|\\r|.)*)$endComment") + regex.find(currentFileText)?.groups?.get(1)?.value?.let { existingSection -> + val newText = swappedText(existingSection, currentFileText) + project.setNewText(editor, model.containingFilePath, newText) + } + } + + else -> { + val line = buildString { + append("\n") + appendLine(swappedText(null, currentFileText)) + } + project.setNewText(editor, model.containingFilePath, currentFileText + line) + } + } + } + } + } +} + +private fun Module.testModules() = listOf(this) + +private fun PackageDataService.checkAndInstallRequirements(project: Project): Boolean { + val missingPackages = jsPackagesList.filterNot { this.findPackage(it) } + if (missingPackages.isEmpty()) return true + val message = """ + Requirements are not installed: + ${missingPackages.joinToString { it.packageName }} + Install them? + """.trimIndent() + val result = Messages.showOkCancelDialog( + project, message, "Requirements Missmatch Error", "Install", "Cancel", null + ) + + if (result == Messages.CANCEL) + return false + + try { + val (_, errorText) = this.installMissingPackages(missingPackages) + if (errorText.isNotEmpty()) { + showErrorDialogLater( + project, + "Requirements installing failed with some reason:\n${errorText}", + "Failed to install requirements" + ) + return false + } + return true + } catch (_: TimeoutException) { + showErrorDialogLater( + project, + """ + Requirements installing failed due to the exceeded waiting time for the installation, check your internet connection. + + Try to install missing npm packages manually: + ${ + missingPackages.joinToString(separator = "\n") { + "> npm install ${it.npmListFlag} ${it.packageName}" + } + } + """.trimIndent(), + "Failed to install requirements" + ) + return false + } +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/JsDialogWindow.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/JsDialogWindow.kt new file mode 100644 index 0000000000..14b71160c7 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/JsDialogWindow.kt @@ -0,0 +1,170 @@ +package org.utbot.intellij.plugin.js + +import com.intellij.lang.javascript.refactoring.ui.JSMemberSelectionTable +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +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.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.ui.ContextHelpLabel +import com.intellij.ui.JBIntSpinner +import com.intellij.ui.components.Panel +import com.intellij.ui.layout.Cell +import com.intellij.ui.layout.panel +import com.intellij.util.ui.JBUI +import framework.codegen.Mocha +import org.utbot.framework.plugin.api.CodeGenerationSettingItem +import org.utbot.intellij.plugin.ui.components.TestSourceDirectoryChooser +import settings.JsTestGenerationSettings.defaultTimeout +import java.awt.BorderLayout +import java.io.File +import java.nio.file.Paths +import javax.swing.DefaultComboBoxModel +import javax.swing.JComboBox +import javax.swing.JComponent + +class JsDialogWindow(val model: JsTestsModel) : DialogWrapper(model.project) { + + private val items = model.fileMethods + + private val functionsTable = JSMemberSelectionTable(items, null, null).apply { + val height = this.rowHeight * (items.size.coerceAtMost(12) + 1) + this.preferredScrollableViewportSize = JBUI.size(-1, height) + } + + private val testSourceFolderField = TestSourceDirectoryChooser(model, model.file.virtualFile) + private val testFrameworks = ComboBox(DefaultComboBoxModel(arrayOf(Mocha))) + private val nycSourceFileChooserField = NycSourceFileChooser(model) + private val coverageMode = CoverageModeButtons + + private lateinit var panel: DialogPanel + + private val timeoutSpinner = + JBIntSpinner( + defaultTimeout.toInt(), + MINIMUM_TIMEOUT_VALUE_IN_SECONDS, + Int.MAX_VALUE, + MINIMUM_TIMEOUT_VALUE_IN_SECONDS + ) + + init { + title = "Generate Tests with UtBot" + super.setOKButtonText("Generate Tests") + isResizable = false + init() + } + + + @Suppress("UNCHECKED_CAST") + override fun createCenterPanel(): JComponent { + panel = panel { + row("Test source root:") { + component(testSourceFolderField) + } + row("Test framework:") { + component( + Panel().apply { + add(testFrameworks as ComboBox, BorderLayout.LINE_START) + } + ) + } + row("Nyc source path:") { + component(nycSourceFileChooserField) + } + row("Coverage mode:") { + cell { + panelWithHelpTooltip("Fast mode does not guarantee proper handling of user timeouts") { + coverageMode.fastButton() + coverageMode.baseButton() + } + } + } + row("Timeout for Node.js (in seconds):") { + panelWithHelpTooltip("The execution timeout for each generated test") { + component(timeoutSpinner) + } + } + row("Generate test methods for:") {} + row { + scrollPane(functionsTable) + } + } + updateMembersTable() + setListeners() + return panel + } + + + private inline fun Cell.panelWithHelpTooltip(tooltipText: String?, crossinline init: Cell.() -> Unit): Cell { + init() + tooltipText?.let { component(ContextHelpLabel.create(it)) } + return this + } + + + override fun doOKAction() { + val selected = functionsTable.selectedMemberInfos.toSet() + model.selectedMethods = if (selected.any()) selected else emptySet() + model.testFramework = testFrameworks.item + model.timeout = timeoutSpinner.number.toLong() + model.pathToNYC = nycSourceFileChooserField.text + model.coverageMode = CoverageModeButtons.mode + File(testSourceFolderField.text).mkdir() + model.testSourceRoot = + VirtualFileManager.getInstance().refreshAndFindFileByNioPath(Paths.get(testSourceFolderField.text)) + super.doOKAction() + } + + override fun doValidate(): ValidationInfo? { + return testSourceFolderField.validatePath() ?: nycSourceFileChooserField.validateNyc() + } + + private fun updateMembersTable() { + if (items.isEmpty()) isOKActionEnabled = false + val focusedNames = model.selectedMethods.map { it.member.name } + val selectedMethods = items.filter { + focusedNames.contains(it.member.name) + } + if (selectedMethods.isEmpty()) { + checkMembers(items) + } else { + checkMembers(selectedMethods) + } + } + + @Suppress("unused") + private fun configureTestFrameworkIfRequired() { +// initTestFrameworkPresenceThread.join() + val frameworkNotInstalled = !testFrameworks.item.isInstalled + if (frameworkNotInstalled) { + Messages.showErrorDialog( + "Test framework ${testFrameworks.item.displayName} is not installed. " + + "Run \"npm i -g ${testFrameworks.item.displayName}\".", + "Missing Framework" + ) + } + } + + + private fun setListeners() { + + testSourceFolderField.childComponent.addActionListener { event -> + with((event.source as JComboBox<*>).selectedItem) { + if (this is VirtualFile) { + model.setSourceRootAndFindTestModule(this@with) + } else { + model.setSourceRootAndFindTestModule(null) + } + } + } + } + + private fun checkMembers(members: Collection) = members.forEach { it.isChecked = true } + + +} + +private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 1 diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/JsTestsModel.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/JsTestsModel.kt new file mode 100644 index 0000000000..de18043117 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/JsTestsModel.kt @@ -0,0 +1,49 @@ +package org.utbot.intellij.plugin.js + +import com.intellij.lang.javascript.psi.JSFile +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.intellij.plugin.models.BaseTestsModel +import service.coverage.CoverageMode +import settings.JsTestGenerationSettings.defaultTimeout + +class JsTestsModel( + project: Project, + val potentialTestModules: List, + val file: JSFile, + val fileMethods: Set, + var selectedMethods: Set, +) : BaseTestsModel( + project +) { + var testModule: Module = potentialTestModules.firstOrNull() ?: error("Empty list of test modules in model") + + var timeout = defaultTimeout + + lateinit var testFramework: TestFramework + lateinit var containingFilePath: String + var pathToNode: String = "node" + var pathToNYC: String = "nyc" + var pathToNPM: String = "npm" + var coverageMode: CoverageMode = CoverageMode.FAST + + fun setSourceRootAndFindTestModule(newTestSourceRoot: VirtualFile?) { + requireNotNull(newTestSourceRoot) + testSourceRoot = newTestSourceRoot + var target = newTestSourceRoot + while (target != null && target is FakeVirtualFile) { + target = target.parent + } + if (target == null) { + error("Could not find module for $newTestSourceRoot") + } + + testModule = ModuleUtil.findModuleForFile(target, project) + ?: error("Could not find module for $newTestSourceRoot") + } +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/NycSourceFileChooser.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/NycSourceFileChooser.kt new file mode 100644 index 0000000000..e3683d5a6d --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/NycSourceFileChooser.kt @@ -0,0 +1,36 @@ +package org.utbot.intellij.plugin.js + +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.ui.TextBrowseFolderListener +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.ui.ValidationInfo +import org.utbot.common.PathUtil.replaceSeparator +import settings.PackageDataService +import utils.OsProvider + + +class NycSourceFileChooser(val model: JsTestsModel) : TextFieldWithBrowseButton() { + + + init { + val descriptor = FileChooserDescriptor( + true, + false, + false, + false, + false, + false, + ) + addBrowseFolderListener( + TextBrowseFolderListener(descriptor, model.project) + ) + text = PackageDataService.nycPath + } + + fun validateNyc(): ValidationInfo? { + return if (replaceSeparator(text).endsWith("nyc" + OsProvider.getProviderByOs().npmPackagePostfix)) + null + else + ValidationInfo("Nyc executable file was not found in the specified directory", this) + } +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/language/JsLanguageAssistant.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/language/JsLanguageAssistant.kt new file mode 100644 index 0000000000..ec84561b6f --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/js/language/JsLanguageAssistant.kt @@ -0,0 +1,167 @@ +package org.utbot.intellij.plugin.js.language + +import com.intellij.lang.Language +import com.intellij.lang.ecmascript6.psi.ES6Class +import com.intellij.lang.javascript.psi.JSFile +import com.intellij.lang.javascript.psi.JSFunction +import com.intellij.lang.javascript.psi.ecmal4.JSClass +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.util.PsiTreeUtil +import org.utbot.intellij.plugin.js.JsDialogProcessor +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import settings.JsTestGenerationSettings.dummyClassName + +@Suppress("unused") // is used in org.utbot.intellij.plugin.language.agnostic.LanguageAssistant via reflection +object JsLanguageAssistant : LanguageAssistant() { + + private const val jsId = "ECMAScript 6" + val jsLanguage: Language = Language.findLanguageByID(jsId) ?: error("JavaScript language wasn't found") + + private data class PsiTargets( + val methods: Set, + val focusedMethod: JSMemberInfo?, + val module: Module, + val containingFilePath: String, + val editor: Editor?, + val file: JSFile + ) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val (methods, focusedMethod, module, containingFilePath, editor, file) = getPsiTargets(e) ?: return + JsDialogProcessor.createDialogAndGenerateTests( + project = project, + srcModule = module, + fileMethods = methods, + focusedMethod = focusedMethod, + containingFilePath = containingFilePath, + editor = editor, + file = file, + ) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): PsiTargets? { + e.project ?: return null + val editor = e.getData(CommonDataKeys.EDITOR) + val file = e.getData(CommonDataKeys.PSI_FILE) as? JSFile ?: return null + val element = if (editor != null) { + findPsiElement(file, editor) ?: return null + } else { + e.getData(CommonDataKeys.PSI_ELEMENT) ?: return null + } + val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return null + val virtualFile = (e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return null).path + val focusedMethod = getContainingMethod(element) + containingClass(element)?.let { + val methods = it.functions + val memberInfos = generateMemberInfo(e.project!!, methods.toList(), it) + val focusedMethodMI = memberInfos.find { member -> + member.member?.name == focusedMethod?.name + } + return PsiTargets( + methods = memberInfos, + focusedMethod = focusedMethodMI, + module = module, + containingFilePath = virtualFile, + editor = editor, + file = file, + ) + } + var memberInfos = generateMemberInfo(e.project!!, file.statements.filterIsInstance()) + var focusedMethodMI = memberInfos.find { member -> + member.member?.name == focusedMethod?.name + } + // TODO: generate tests for all classes, not only the first one + // (currently not possible since breaks JsTestGenerator routine) + if (memberInfos.isEmpty()) { + val classes = file.statements.filterIsInstance() + if (classes.isEmpty()) return null + + memberInfos = generateMemberInfo( + e.project!!, + emptyList(), + classes.first() + ) + if (memberInfos.isEmpty()) return null + + focusedMethodMI = memberInfos.first() + } + return PsiTargets( + methods = memberInfos, + focusedMethod = focusedMethodMI, + module = module, + containingFilePath = virtualFile, + editor = editor, + file = file, + ) + } + + private fun getContainingMethod(element: PsiElement): JSFunction? { + if (element is JSFunction) + return element + + val parent = element.parent ?: return null + return getContainingMethod(parent) + } + + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + return element + } + + private fun containingClass(element: PsiElement) = + PsiTreeUtil.getParentOfType(element, ES6Class::class.java, false) + + private fun buildClassStringFromMethods(methods: List): String { + var strBuilder = "\n" + val filteredMethods = methods.filterNot { method -> method.name == "constructor" } + filteredMethods.forEach { + strBuilder += it.text.replace("function ", "") + } + // Creating a class with a random name. It won't affect user's code since it is created in abstract PsiFile. + return "class $dummyClassName {$strBuilder}" + } + + /* + Small hack: generating a string source code of an "impossible" class in order to + generate a PsiFile with it, then extract ES6Class from it, then extract MemberInfos. + Created for top-level functions that don't have a parent class. + */ + private fun generateMemberInfo( + project: Project, + methods: List, + jsClass: JSClass? = null + ): Set { + jsClass?.let { + val res = mutableListOf() + JSMemberInfo.extractClassMembers(it, res) { member -> + member is JSFunction + } + return res.toSet() + } + val strClazz = buildClassStringFromMethods(methods) + val abstractPsiFile = PsiFileFactory.getInstance(project) + .createFileFromText(jsLanguage, strClazz) + val clazz = PsiTreeUtil.getChildOfType(abstractPsiFile, JSClass::class.java) + val res = mutableListOf() + JSMemberInfo.extractClassMembers(clazz!!, res) { true } + return res.toSet() + } +} diff --git a/utbot-intellij-main/build.gradle.kts b/utbot-intellij-main/build.gradle.kts new file mode 100644 index 0000000000..30e728a74d --- /dev/null +++ b/utbot-intellij-main/build.gradle.kts @@ -0,0 +1,157 @@ +val semVer: String? by rootProject +val junit5Version: String by rootProject +val junit4PlatformVersion: String by rootProject + +// === IDE settings === +val projectType: String by rootProject +val communityEdition: String by rootProject +val ultimateEdition: String by rootProject + +val ideType: String by rootProject +val androidStudioPath: String? by rootProject + +val ideaVersion: String? by rootProject +val pycharmVersion: String? by rootProject +val golandVersion: String? by rootProject + +val javaIde: String? by rootProject +val pythonIde: String? by rootProject +val jsIde: String? by rootProject +val goIde: String? by rootProject + +val ideVersion = when(ideType) { + "PC", "PY" -> pycharmVersion + "GO" -> golandVersion + else -> ideaVersion +} + +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject +val goPluginVersion: String? by rootProject + +// https://plugins.jetbrains.com/docs/intellij/android-studio.html#configuring-the-plugin-pluginxml-file +val ideTypeOrAndroidStudio = if (androidStudioPath == null) ideType else "IC" + +project.tasks.asMap["runIde"]?.enabled = false +// === IDE settings === + +plugins { + id("org.jetbrains.intellij") version "1.13.1" +} + +intellij { + + val androidPlugins = listOf("org.jetbrains.android") + + val jvmPlugins = mutableListOf( + "java" + ) + + val kotlinPlugins = listOf( + "org.jetbrains.kotlin" + ) + + androidStudioPath?.let { jvmPlugins += androidPlugins } + + val pythonCommunityPlugins = listOf( + "PythonCore:${pythonCommunityPluginVersion}" + ) + + val pythonUltimatePlugins = listOf( + "Pythonid:${pythonUltimatePluginVersion}" + ) + + val jsPlugins = listOf( + "JavaScript" + ) + + val goPlugins = listOf( + "org.jetbrains.plugins.go:${goPluginVersion}" + ) + + val mavenUtilsPlugins = listOf( + "org.jetbrains.idea.maven" + ) + + val basePluginSet = jvmPlugins + kotlinPlugins + mavenUtilsPlugins + androidPlugins + + plugins.set( + when (projectType) { + communityEdition -> basePluginSet + pythonCommunityPlugins + ultimateEdition -> when (ideType) { + "IC" -> basePluginSet + pythonCommunityPlugins + "IU" -> basePluginSet + pythonUltimatePlugins + jsPlugins + goPlugins + "PC" -> pythonCommunityPlugins + "PY" -> pythonUltimatePlugins + jsPlugins + "GO" -> goPlugins + else -> basePluginSet + } + else -> basePluginSet + } + ) + + version.set(ideVersion) + type.set(ideTypeOrAndroidStudio) + SettingsTemplateHelper.proceed(project) +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + runIde { + jvmArgs("-Xmx2048m") + jvmArgs("--add-exports", "java.desktop/sun.awt.windows=ALL-UNNAMED") + androidStudioPath?.let { ideDir.set(file(it)) } + } + + patchPluginXml { + sinceBuild.set("223") + untilBuild.set("232.*") + version.set(semVer) + } +} + +dependencies { + implementation(project(":utbot-ui-commons")) + + //Family + + if (javaIde?.split(',')?.contains(ideType) == true) { + implementation(project(":utbot-intellij")) + } + + if (pythonIde?.split(',')?.contains(ideType) == true) { + implementation(project(":utbot-python")) + implementation(project(":utbot-intellij-python")) + } + + if (projectType == ultimateEdition) { + if (jsIde?.split(',')?.contains(ideType) == true) { + implementation(project(":utbot-js")) + implementation(project(":utbot-intellij-js")) + } + + if (goIde?.split(',')?.contains(ideType) == true) { + implementation(project(":utbot-go")) + implementation(project(":utbot-intellij-go")) + } + } + + implementation(project(":utbot-android-studio")) + + testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:$junit4PlatformVersion") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junit5Version") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:$junit5Version") +} diff --git a/utbot-intellij-main/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt b/utbot-intellij-main/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt new file mode 100644 index 0000000000..a0cdc117e5 --- /dev/null +++ b/utbot-intellij-main/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt @@ -0,0 +1,31 @@ +package org.utbot.intellij.plugin.ui.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import org.utbot.intellij.plugin.settings.Settings + +class GenerateTestsAction : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + LanguageAssistant.get(e)?.actionPerformed(e) + } + + override fun update(e: AnActionEvent) { + val languageAssistant = LanguageAssistant.get(e) + if (languageAssistant == null || !accessByProjectSettings(e)) { + e.presentation.isEnabled = false + } else { + languageAssistant.update(e) + } + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + private fun accessByProjectSettings(e: AnActionEvent): Boolean { + val experimentalLanguageSetting = e.project?.service()?.experimentalLanguagesSupport + val languagePackageName = LanguageAssistant.get(e)?.toString() + return experimentalLanguageSetting == true || languagePackageName?.contains("JvmLanguageAssistant") == true + } +} diff --git a/utbot-intellij-main/src/main/resources/META-INF/plugin.xml b/utbot-intellij-main/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000000..89f84572f4 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,86 @@ + + + + org.utbot.intellij.plugin.id + UnitTestBot + utbot.org + com.intellij.modules.platform + + com.intellij.modules.java + org.jetbrains.kotlin + com.intellij.modules.python + org.jetbrains.plugins.go + org.jetbrains.android + org.jetbrains.idea.maven + + + + + + + + + + + + + + + + + + + +
    + Discover UnitTestBot key features in our latest release: +
      +
    • generating ready-to-use test cases — with valid inputs, method bodies, assertions, and comments
    • +
    • maximizing branch coverage in regression suite while keeping the number of tests minimized
    • +
    • finding deeply hidden code defects and expressing them as tests
    • +
    • fine-tuned mocking, including mocking static methods
    • +
    • representing all the test descriptions in a human-readable format
    • +
    • generating SARIF reports
    • +
    • innovative symbolic execution engine combined with a smart fuzzing platform
    • +
    + Try UnitTestBot online demo to see how it generates tests for your code in real time. +
    + Contribute to UnitTestBot via GitHub. +
    + Found a bug? File an issue. +
    + Have an idea? Start a discussion. + ]]> +
    + + +
  • It automatically detects if you use the Spring framework and provides you with necessary options right in the dialog window.
  • +
  • You can choose from the three approaches to Spring test generation:
  • +
      +
    • standard unit tests that mock environmental interactions,
    • +
    • Spring-specific unit tests that use information about the Spring application context,
    • +
    • and integration tests that validate interactions between Spring components.
    • +
    + + Find more improvements and bug fixes: +
      +
    • Support for IntelliJ IDEA 2023.2
    • +
    • Taint analysis feature (experimental)
    • +
    • Improved mocking in symbolic execution engine
    • +
    • Enhanced fuzzing mechanism: improved domain-specific API and mutation processes; support for generic fields and resolving generic parameter types; single branch detection, and ability to use all public methods of a class under test
    • +
    • Improved UIs for standard Java, Spring, and Python test generation
    • +
    • Fixed bugs for symbolic execution engine, fuzzing, code generation and instrumented process, summaries, SARIF reports, and more
    • +
    • Multiple improvements for Python support related to rendering constructors; mastering exceptions, timed out tests, and regular expressions; fixes for coverage and shutting down behavior
    • +
    • Enhanced Go test generation: support for maps and user-defined types
    • +
    + ]]> +
    +
    diff --git a/utbot-intellij-main/src/main/resources/META-INF/pluginIcon.svg b/utbot-intellij-main/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000000..d24574d6dd --- /dev/null +++ b/utbot-intellij-main/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/withAndroid.xml b/utbot-intellij-main/src/main/resources/META-INF/withAndroid.xml similarity index 100% rename from utbot-intellij/src/main/resources/META-INF/withAndroid.xml rename to utbot-intellij-main/src/main/resources/META-INF/withAndroid.xml diff --git a/utbot-intellij-main/src/main/resources/META-INF/withGo.xml b/utbot-intellij-main/src/main/resources/META-INF/withGo.xml new file mode 100644 index 0000000000..65c848f900 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/META-INF/withGo.xml @@ -0,0 +1,3 @@ + + + diff --git a/utbot-intellij-main/src/main/resources/META-INF/withIdeaMaven.xml b/utbot-intellij-main/src/main/resources/META-INF/withIdeaMaven.xml new file mode 100644 index 0000000000..5c2f872b51 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/META-INF/withIdeaMaven.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/utbot-intellij-main/src/main/resources/META-INF/withJS.xml b/utbot-intellij-main/src/main/resources/META-INF/withJS.xml new file mode 100644 index 0000000000..d04570b311 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/META-INF/withJS.xml @@ -0,0 +1,4 @@ + + + JavaScript + \ No newline at end of file diff --git a/utbot-intellij-main/src/main/resources/META-INF/withJava.xml b/utbot-intellij-main/src/main/resources/META-INF/withJava.xml new file mode 100644 index 0000000000..eafe833bc7 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/META-INF/withJava.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-intellij-main/src/main/resources/META-INF/withKotlin.xml b/utbot-intellij-main/src/main/resources/META-INF/withKotlin.xml new file mode 100644 index 0000000000..07e0e420c3 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/META-INF/withKotlin.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/utbot-intellij-main/src/main/resources/META-INF/withLang.xml b/utbot-intellij-main/src/main/resources/META-INF/withLang.xml new file mode 100644 index 0000000000..ed33e791e3 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/META-INF/withLang.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/utbot-intellij-main/src/main/resources/META-INF/withPython.xml b/utbot-intellij-main/src/main/resources/META-INF/withPython.xml new file mode 100644 index 0000000000..f272fd7601 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/META-INF/withPython.xml @@ -0,0 +1,4 @@ + + + com.intellij.modules.python + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/application.properties b/utbot-intellij-main/src/main/resources/application.properties similarity index 100% rename from utbot-intellij/src/main/resources/application.properties rename to utbot-intellij-main/src/main/resources/application.properties diff --git a/utbot-intellij-main/src/main/resources/bundles/UtbotBundle.properties b/utbot-intellij-main/src/main/resources/bundles/UtbotBundle.properties new file mode 100644 index 0000000000..4c13064c79 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/bundles/UtbotBundle.properties @@ -0,0 +1,10 @@ +# {0} - Test report url prefix, {1] - suffix +test.report.force.mock.warning=Warning: Some test cases were ignored, because no mocking framework is installed in the project.
    \ +Better results could be achieved by installing mocking framework. +test.report.force.static.mock.warning=Warning: Some test cases were ignored, because mockito-inline is not installed in the project.
    \ +Better results could be achieved by configuring mockito-inline. +test.report.test.framework.warning=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. \ No newline at end of file diff --git a/utbot-intellij-main/src/main/resources/inspectionDescriptions/UnitTestBotInspectionTool.html b/utbot-intellij-main/src/main/resources/inspectionDescriptions/UnitTestBotInspectionTool.html new file mode 100644 index 0000000000..a01f16e673 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/inspectionDescriptions/UnitTestBotInspectionTool.html @@ -0,0 +1,11 @@ + + +

    Reports unchecked exceptions detected by UnitTestBot.

    +

    Example:

    +
    +void foo(int a) {
    +    return 1 / a; // throws ArithmeticException when `a == 0`
    +}
    +
    + + \ No newline at end of file diff --git a/utbot-intellij-main/src/main/resources/log4j2.xml b/utbot-intellij-main/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..6a9ae540c8 --- /dev/null +++ b/utbot-intellij-main/src/main/resources/log4j2.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-intellij-main/src/main/resources/settings.properties b/utbot-intellij-main/src/main/resources/settings.properties new file mode 100644 index 0000000000..84bc852a2e --- /dev/null +++ b/utbot-intellij-main/src/main/resources/settings.properties @@ -0,0 +1,621 @@ +# Copyright (c) 2024 utbot.org +# +# 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. + +# +# Setting to disable coroutines debug explicitly. +# Set it to false if debug info is required. +# +# Default value is [true] +#disableCoroutinesDebug=true + +# +# Make `true` for interactive mode (like Intellij plugin). If `false` UTBot can apply certain optimizations. +# +# Default value is [true] +#classfilesCanChange=true + +# +# Timeout for Z3 solver.check calls. +# Set it to 0 to disable timeout. +# +# Default value is [1000] +#checkSolverTimeoutMillis=1000 + +# +# Timeout for symbolic execution +# +# Default value is [60000] +#utBotGenerationTimeoutInMillis=60000 + +# +# Random seed in path selector. +# Set null to disable random. +# +# Default value is [42] +#seedInPathSelector=42 + +# +# Type of path selector. +# +# COVERED_NEW_SELECTOR: [CoveredNewSelector] +# INHERITORS_SELECTOR: [InheritorsSelector] +# BFS_SELECTOR: [BFSSelector] +# SUBPATH_GUIDED_SELECTOR: [SubpathGuidedSelector] +# CPI_SELECTOR: [CPInstSelector] +# FORK_DEPTH_SELECTOR: [ForkDepthSelector] +# ML_SELECTOR: [MLSelector] +# TORCH_SELECTOR: [TorchSelector] +# RANDOM_SELECTOR: [RandomSelector] +# RANDOM_PATH_SELECTOR: [RandomPathSelector] +# +# Default value is [INHERITORS_SELECTOR] +#pathSelectorType=INHERITORS_SELECTOR + +# +# Type of MLSelector recalculation. +# +# WITH_RECALCULATION: [MLSelectorWithRecalculation] +# WITHOUT_RECALCULATION: [MLSelectorWithoutRecalculation] +# +# Default value is [WITHOUT_RECALCULATION] +#mlSelectorRecalculationType=WITHOUT_RECALCULATION + +# +# Type of [MLPredictor]. +# +# MLP: [MultilayerPerceptronPredictor] +# LINREG: [LinearRegressionPredictor] +# +# Default value is [MLP] +#mlPredictorType=MLP + +# +# Steps limit for path selector. +# +# Default value is [3500] +#pathSelectorStepsLimit=3500 + +# +# Determines whether path selector should save remaining states for concrete execution after stopping by strategy. +# False for all framework tests by default. +#saveRemainingStatesForConcreteExecution=true + +# +# Use debug visualization. +# Set it to true if debug visualization is needed. +# +# Default value is [false] +#useDebugVisualization=false + +# +# Set the value to true to show library classes' graphs in visualization. +# +# Default value is [false] +#showLibraryClassesInVisualization=false + +# +# Use simplification of UtExpressions. +# Set it to false to disable expression simplification. +# +# Default value is [true] +#useExpressionSimplification=true + +# +# Enable the Summarization module to generate summaries for methods under test. +# Note: if it is [SummariesGenerationType.NONE], +# all the execution for a particular method will be stored at the same nameless region. +# +# FULL: All possible analysis actions are taken +# LIGHT: Analysis actions based on sources are NOT taken +# NONE: No summaries are generated +# +# Default value is [FULL] +#summaryGenerationType=FULL + +# +# If True test comments will be generated. +# +# Default value is [true] +#enableJavaDocGeneration=true + +# +# If True cluster comments will be generated. +# +# Default value is [true] +#enableClusterCommentsGeneration=true + +# +# If True names for tests will be generated. +# +# Default value is [true] +#enableTestNamesGeneration=true + +# +# If True display names for tests will be generated. +# +# Default value is [true] +#enableDisplayNameGeneration=true + +# +# If True display name in from -> to style will be generated. +# +# Default value is [true] +#useDisplayNameArrowStyle=true + +# +# Generate summaries using plugin's custom JavaDoc tags. +# +# Default value is [true] +#useCustomJavaDocTags=true + +# +# This option regulates which [NullPointerException] check should be performed for nested methods. +# Set an option in true if you want to perform NPE check in the corresponding situations, otherwise set false. +# +# Default value is [true] +#checkNpeInNestedMethods=true + +# +# This option regulates which [NullPointerException] check should be performed for nested not private methods. +# Set an option in true if you want to perform NPE check in the corresponding situations, otherwise set false. +# +# Default value is [false] +#checkNpeInNestedNotPrivateMethods=false + +# +# This option determines whether we should generate [NullPointerException] checks for final or non-public fields +# in non-application classes. Set by true, this option highly decreases test's readability in some cases +# because of using reflection API for setting final/non-public fields in non-application classes. +# NOTE: With false value loses some executions with NPE in system classes, but often most of these executions +# are not expected by user. +# +# Default value is [false] +#maximizeCoverageUsingReflection=false + +# +# Activate or deactivate substituting static fields values set in static initializer +# with symbolic variable to try to set them another value than in initializer. +# +# Default value is [true] +#substituteStaticsWithSymbolicVariable=true + +# +# Use concrete execution. +# +# Default value is [true] +#useConcreteExecution=true + +# +# Enable code generation tests with every possible configuration +# for every method in samples. +# Important: is enabled generation requires enormous amount of time. +# +# Default value is [false] +#checkAllCombinationsForEveryTestInSamples=false + +# +# Enable transformation UtCompositeModels into UtAssembleModels using AssembleModelGenerator. +# Note: false doesn't mean that there will be no assemble models, it means that the generator will be turned off. +# Assemble models will present for lists, sets, etc. +# +# Default value is [true] +#useAssembleModelGenerator=true + +# +# Test related files from the temp directory that are older than [daysLimitForTempFiles] +# will be removed at the beginning of the test run. +# +# Default value is [3] +#daysLimitForTempFiles=3 + +# +# Enables soft constraints in the engine. +# +# Default value is [true] +#preferredCexOption=true + +# +# Type of test minimization strategy. +# +# DO_NOT_MINIMIZE_STRATEGY: Always adds new test +# COVERAGE_STRATEGY: Adds new test only if it increases coverage +# +# Default value is [COVERAGE_STRATEGY] +#testMinimizationStrategyType=COVERAGE_STRATEGY + +# +# Set to true to start fuzzing if symbolic execution haven't return anything +# +# Default value is [true] +#useFuzzing=true + +# +# Set the total attempts to improve coverage by fuzzer. +# +# Default value is [2147483647] +#fuzzingMaxAttempts=2147483647 + +# +# Fuzzer tries to generate and run tests during this time. +# +# Default value is [3000] +#fuzzingTimeoutInMillis=3000 + +# +# Find implementations of interfaces and abstract classes to fuzz. +# +# Default value is [true] +#fuzzingImplementationOfAbstractClasses=true + +# +# Use methods to mutate fields of classes different from class under test or not. +# +# Default value is [false] +#tryMutateOtherClassesFieldsWithMethods=false + +# +# Generate tests that treat possible overflows in arithmetic operations as errors +# that throw Arithmetic Exception. +# +# Default value is [false] +#treatOverflowAsError=false + +# +# Generate tests that treat assertions as error suits. +# +# Default value is [true] +#treatAssertAsErrorSuite=true + +# +# Instrument all classes before start +# +# Default value is [false] +#warmupConcreteExecution=false + +# +# Ignore string literals during the code analysis to make possible to analyze antlr. +# It is a hack and must be removed after the competition. +# +# Default value is [false] +#ignoreStringLiterals=false + +# +# Timeout for specific concrete execution (in milliseconds). +# +# Default value is [1000] +#concreteExecutionDefaultTimeoutInInstrumentedProcessMillis=1000 + +# +# Enable taint analysis or not. +# +# Default value is [false] +#useTaintAnalysis=false + +# +# Path to custom log4j2 configuration file for EngineProcess. +# By default utbot-intellij/src/main/resources/log4j2.xml is used. +# Also default value is used if provided value is not a file. +#engineProcessLogConfigFile="" + +# +# The property is useful only for the IntelliJ IDEs. +# If the property is set in true the engine process opens a debug port. +# @see runInstrumentedProcessWithDebug +# @see org.utbot.intellij.plugin.process.EngineProcess +# +# Default value is [false] +#runEngineProcessWithDebug=false + +# +# The engine process JDWP agent's port of the engine process. +# A debugger attaches to the port in order to debug the process. +# +# Default value is [5005] +#engineProcessDebugPort=5005 + +# +# Value of the suspend mode for the JDWP agent of the engine process. +# If the value is true, the engine process will suspend until a debugger attaches to it. +# +# Default value is [true] +#suspendEngineProcessExecutionInDebugMode=true + +# +# The property is useful only for the IntelliJ IDEs. +# If the property is set in true the spring analyzer process opens a debug port. +# @see runInstrumentedProcessWithDebug +# @see org.utbot.spring.process.SpringAnalyzerProcess +# +# Default value is [false] +#runSpringAnalyzerProcessWithDebug=false + +# +# The spring analyzer process JDWP agent's port. +# A debugger attaches to the port in order to debug the process. +# +# Default value is [5007] +#springAnalyzerProcessDebugPort=5007 + +# +# Value of the suspend mode for the JDWP agent of the spring analyzer process. +# If the value is true, the spring analyzer process will suspend until a debugger attaches to it. +# +# Default value is [true] +#suspendSpringAnalyzerProcessExecutionInDebugMode=true + +# +# The instrumented process JDWP agent's port of the instrumented process. +# A debugger attaches to the port in order to debug the process. +# +# Default value is [5006] +#instrumentedProcessDebugPort=5006 + +# +# Value of the suspend mode for the JDWP agent of the instrumented process. +# If the value is true, the instrumented process will suspend until a debugger attaches to it. +# +# Default value is [true] +#suspendInstrumentedProcessExecutionInDebugMode=true + +# +# If true, runs the instrumented process with the ability to attach a debugger. +# To debug the instrumented process, set the breakpoint in the +# [org.utbot.instrumentation.rd.InstrumentedProcess.Companion.invoke] +# and in the instrumented process's main function and run the main process. +# Then run the remote JVM debug configuration in IDEA. +# If you see the message in console about successful connection, then +# the debugger is attached successfully. +# Now you can put the breakpoints in the instrumented process and debug +# both processes simultaneously. +# @see [org.utbot.instrumentation.rd.InstrumentedProcess.Companion.invoke] +# +# Default value is [false] +#runInstrumentedProcessWithDebug=false + +# +# Number of branch instructions using for clustering executions in the test minimization phase. +# +# Default value is [4] +#numberOfBranchInstructionsForClustering=4 + +# +# Determines should we choose only one crash execution with "minimal" model or keep all. +# +# Default value is [true] +#minimizeCrashExecutions=true + +# +# Determines maximum number of executions with unknown coverage per method per result type. +# In [ContestUsvm] it is useful if concrete fails, so we use symbolic execution result without trace. +# +# Default value is [10] +#maxUnknownCoverageExecutionsPerMethodPerResultType=10 + +# +# Enable it to calculate unsat cores for hard constraints as well. +# It may be usefull during debug. +# Note: it might highly impact performance, so do not enable it in release mode. +# +# Default value is [false] +#enableUnsatCoreCalculationForHardConstraints=false + +# +# Enable it to process states with unknown solver status +# from the queue to concrete execution. +# +# Default value is [true] +#processUnknownStatesDuringConcreteExecution=true + +# +# 2^{this} will be the length of observed subpath. +# See [SubpathGuidedSelector] +# +# Default value is [1] +#subpathGuidedSelectorIndex=1 + +# +# Flag that indicates whether feature processing for execution states enabled or not +# +# Default value is [false] +#enableFeatureProcess=false + +# +# Path to deserialized ML models +# +# Default value is [../models/0] +#modelPath=../models/0 + +# +# Full class name of the class containing the configuration for the ML models to solve path selection task. +# +# Default value is [org.utbot.AnalyticsConfiguration] +#analyticsConfigurationClassPath=org.utbot.AnalyticsConfiguration + +# +# Full class name of the class containing the configuration for the ML models exported from the PyTorch to solve path selection task. +# +# Default value is [org.utbot.AnalyticsTorchConfiguration] +#analyticsTorchConfigurationClassPath=org.utbot.AnalyticsTorchConfiguration + +# +# Number of model iterations that will be used during ContestEstimator +# +# Default value is [1] +#iterations=1 + +# +# Path for state features dir +# +# Default value is [eval/secondFeatures/antlr/INHERITORS_SELECTOR] +#featurePath=eval/secondFeatures/antlr/INHERITORS_SELECTOR + +# +# Counter for tests during testGeneration for one project in ContestEstimator +# +# Default value is [0] +#testCounter=0 + +# +# Flag that indicates whether tests for synthetic (see [Executable.isSynthetic]) and implicitly declared methods (like values, valueOf in enums) should be generated, or not +# +# Default value is [true] +#skipTestGenerationForSyntheticAndImplicitlyDeclaredMethods=true + +# +# Flag that indicates whether should we branch on and set static fields from trusted libraries or not. +# @see [org.utbot.common.WorkaroundReason.IGNORE_STATICS_FROM_TRUSTED_LIBRARIES] +# +# Default value is [true] +#ignoreStaticsFromTrustedLibraries=true + +# +# Use the sandbox in the instrumented process. +# If true, the sandbox will prevent potentially dangerous calls, e.g., file access, reading +# or modifying the environment, calls to `Unsafe` methods etc. +# If false, all these operations will be enabled and may lead to data loss during code analysis +# and test generation. +# +# Default value is [true] +#useSandbox=true + +# +# Transform bytecode in the instrumented process. +# If true, bytecode transformation will help fuzzing to find interesting input data, but the size of bytecode can increase. +# If false, bytecode won`t be changed. +# +# Default value is [false] +#useBytecodeTransformation=false + +# +# Limit for number of generated tests per method (in each region) +# +# Default value is [50] +#maxTestsPerMethodInRegion=50 + +# +# Max file length for generated test file +# +# Default value is [1000000] +#maxTestFileSize=1000000 + +# +# If this options set in true, all soot classes will be removed from a Soot Scene, +# therefore, you will be unable to test soot classes. +# +# Default value is [true] +#removeSootClassesFromHierarchy=true + +# +# If this options set in true, all UtBot classes will be removed from a Soot Scene, +# therefore, you will be unable to test UtBot classes. +# +# Default value is [true] +#removeUtBotClassesFromHierarchy=true + +# +# Use this option to enable calculation and logging of MD5 for dropped states by statistics. +# Example of such logging: +# Dropping state (lastStatus=UNDEFINED) by the distance statistics. MD5: 5d0bccc242e87d53578ca0ef64aa5864 +# +# Default value is [false] +#enableLoggingForDroppedStates=false + +# +# If this option set in true, depending on the number of possible types for +# a particular object will be used either type system based on conjunction +# or on bit vectors. +# @see useBitVecBasedTypeSystem +# +# Default value is [true] +#useBitVecBasedTypeSystem=true + +# +# The number of types on which the choice of the type system depends. +# +# Default value is [64] +#maxTypeNumberForEnumeration=64 + +# +# The threshold for numbers of types for which they will be encoded into solver. +# It is used to do not encode big type storages due to significand performance degradation. +# +# Default value is [512] +#maxNumberOfTypesToEncode=512 + +# +# The behaviour of further analysis if tests generation cancellation is requested. +# +# NONE: Do not react on cancellation +# CANCEL_EVERYTHING: Clear all generated test classes +# SAVE_PROCESSED_RESULTS: Show already processed test classes +# +# Default value is [SAVE_PROCESSED_RESULTS] +#cancellationStrategyType=SAVE_PROCESSED_RESULTS + +# +# Depending on this option, sections might be analyzed or not. +# Note that some clinit sections still will be initialized using runtime information. +# +# Default value is [true] +#enableClinitSectionsAnalysis=true + +# +# Process all clinit sections concretely. +# If [enableClinitSectionsAnalysis] is false, it disables effect of this option as well. +# Note that values processed concretely won't be replaced with unbounded symbolic variables. +# +# Default value is [false] +#processAllClinitSectionsConcretely=false + +# +# In cases where we don't have a body for a method, we can either throw an exception +# or treat this a method as a source of an unbounded symbolic variable returned as a result. +# If this option is set in true, instead of analysis we will return an unbounded symbolic +# variable with a corresponding type. Otherwise, an exception will be thrown. +# Default value is false since it is not a common situation when you cannot retrieve a body +# from a regular method. Setting this option in true might be suitable in situations when +# it is more important not to fall at all rather than work precisely. +#treatAbsentMethodsAsUnboundedValue=false + +# +# A maximum size for any array in the program. Note that input arrays might be less than this value +# due to the symbolic engine limitation, see `org.utbot.engine.Traverser.softMaxArraySize`. +# +# Default value is [1024] +#maxArraySize=1024 + +# +# A maximum size for any array in the program. Note that input arrays might be less than this value +# due to the symbolic engine limitation, see `org.utbot.engine.Traverser.softMaxArraySize`. +# +# Default value is [false] +#disableUnsatChecking=false + +# +# When generating integration tests we only partially reset context in between executions to save time. +# For example, entity id generators do not get reset. It may lead to non-reproduceable results if +# IDs leak to the output of the method under test. +# To cope with that, we rerun executions that are left after minimization, fully resetting Spring context +# between executions. However, full context reset is slow, so we use this setting to limit number of +# tests per method that are rerun with full context reset in case minimization outputs too many tests. +# +# Default value is [25] +#maxSpringContextResetsPerMethod=25 + +# +# Add "test method start marker" and "test method end marker" around each test, can be used to +# detect uncompilable tests and remove them. +# +# Default value is [false] +#addTestMethodMarkers=false diff --git a/utbot-intellij/src/test/resources/application.properties b/utbot-intellij-main/src/test/resources/application.properties similarity index 100% rename from utbot-intellij/src/test/resources/application.properties rename to utbot-intellij-main/src/test/resources/application.properties diff --git a/utbot-intellij-main/src/test/resources/junit-platform.properties b/utbot-intellij-main/src/test/resources/junit-platform.properties new file mode 100644 index 0000000000..6011214f1f --- /dev/null +++ b/utbot-intellij-main/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testclass.order.default = org.junit.jupiter.api.ClassOrderer$OrderAnnotation \ No newline at end of file diff --git a/utbot-intellij/src/test/resources/log4j.properties b/utbot-intellij-main/src/test/resources/log4j.properties similarity index 100% rename from utbot-intellij/src/test/resources/log4j.properties rename to utbot-intellij-main/src/test/resources/log4j.properties diff --git a/utbot-intellij-main/src/test/resources/log4j2.xml b/utbot-intellij-main/src/test/resources/log4j2.xml new file mode 100644 index 0000000000..faa66a318f --- /dev/null +++ b/utbot-intellij-main/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-intellij-python/build.gradle.kts b/utbot-intellij-python/build.gradle.kts new file mode 100644 index 0000000000..222842d165 --- /dev/null +++ b/utbot-intellij-python/build.gradle.kts @@ -0,0 +1,128 @@ +val semVer: String? by rootProject +val kotlinLoggingVersion: String? by rootProject + +// === IDE settings === +val projectType: String by rootProject +val communityEdition: String by rootProject +val ultimateEdition: String by rootProject + +val ideType: String by rootProject +val androidStudioPath: String? by rootProject + +val ideaVersion: String? by rootProject +val pycharmVersion: String? by rootProject +val golandVersion: String? by rootProject + +val javaIde: String? by rootProject +val pythonIde: String? by rootProject +val jsIde: String? by rootProject +val goIde: String? by rootProject + +val ideVersion = when(ideType) { + "PC", "PY" -> pycharmVersion + "GO" -> golandVersion + else -> ideaVersion +} + +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject +val goPluginVersion: String? by rootProject + +// https://plugins.jetbrains.com/docs/intellij/android-studio.html#configuring-the-plugin-pluginxml-file +val ideTypeOrAndroidStudio = if (androidStudioPath == null) ideType else "IC" + +project.tasks.asMap["runIde"]?.enabled = false +// === IDE settings === + +plugins { + id("org.jetbrains.intellij") version "1.13.1" +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + patchPluginXml { + sinceBuild.set("223") + untilBuild.set("232.*") + version.set(semVer) + } +} + +val pythonTypesAPIHash: String by rootProject + +dependencies { + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation(project(":utbot-ui-commons")) + + //Family + implementation(project(":utbot-python")) + implementation("com.github.UnitTestBot:PythonTypesAPI:$pythonTypesAPIHash") +} + +intellij { + + val androidPlugins = listOf("org.jetbrains.android") + + val jvmPlugins = mutableListOf( + "java" + ) + + val kotlinPlugins = listOf( + "org.jetbrains.kotlin" + ) + + androidStudioPath?.let { jvmPlugins += androidPlugins } + + val pythonCommunityPlugins = listOf( + "PythonCore:${pythonCommunityPluginVersion}" + ) + + val pythonUltimatePlugins = listOf( + "Pythonid:${pythonUltimatePluginVersion}" + ) + + val jsPlugins = listOf( + "JavaScript" + ) + + val goPlugins = listOf( + "org.jetbrains.plugins.go:${goPluginVersion}" + ) + + val mavenUtilsPlugins = listOf( + "org.jetbrains.idea.maven" + ) + + val basePluginSet = jvmPlugins + kotlinPlugins + mavenUtilsPlugins + androidPlugins + + plugins.set( + when (projectType) { + communityEdition -> basePluginSet + pythonCommunityPlugins + ultimateEdition -> when (ideType) { + "IC" -> basePluginSet + pythonCommunityPlugins + "IU" -> basePluginSet + pythonUltimatePlugins + jsPlugins + goPlugins + "PC" -> pythonCommunityPlugins + "PY" -> pythonUltimatePlugins + jsPlugins + "GO" -> goPlugins + else -> basePluginSet + } + else -> basePluginSet + } + ) + + version.set(ideVersion) + type.set(ideType) +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyClassItem.java b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyClassItem.java new file mode 100644 index 0000000000..f49036a20a --- /dev/null +++ b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyClassItem.java @@ -0,0 +1,42 @@ +package org.utbot.intellij.plugin.python.table; + +import com.intellij.icons.AllIcons; +import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.PyElement; + +import javax.swing.*; + +public class UtPyClassItem implements UtPyTableItem { + private final PyClass pyClass; + private boolean isChecked; + + public UtPyClassItem(PyClass clazz) { + pyClass = clazz; + isChecked = false; + } + + @Override + public PyElement getContent() { + return pyClass; + } + + @Override + public String getIdName() { + return pyClass.getQualifiedName(); + } + + @Override + public Icon getIcon() { + return AllIcons.Nodes.Class; + } + + @Override + public boolean isChecked() { + return isChecked; + } + + @Override + public void setChecked(boolean valueToBeSet) { + isChecked = valueToBeSet; + } +} diff --git a/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyFunctionItem.java b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyFunctionItem.java new file mode 100644 index 0000000000..b5564512e7 --- /dev/null +++ b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyFunctionItem.java @@ -0,0 +1,42 @@ +package org.utbot.intellij.plugin.python.table; + +import com.intellij.icons.AllIcons; +import com.jetbrains.python.psi.PyElement; +import com.jetbrains.python.psi.PyFunction; + +import javax.swing.*; + +public class UtPyFunctionItem implements UtPyTableItem { + private final PyFunction pyFunction; + private boolean isChecked; + + public UtPyFunctionItem(PyFunction function) { + pyFunction = function; + isChecked = false; + } + + @Override + public PyElement getContent() { + return pyFunction; + } + + @Override + public String getIdName() { + return pyFunction.getQualifiedName(); + } + + @Override + public Icon getIcon() { + return AllIcons.Nodes.Function; + } + + @Override + public boolean isChecked() { + return isChecked; + } + + @Override + public void setChecked(boolean valueToBeSet) { + isChecked = valueToBeSet; + } +} diff --git a/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyMemberSelectionTable.java b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyMemberSelectionTable.java new file mode 100644 index 0000000000..26233b9dfe --- /dev/null +++ b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyMemberSelectionTable.java @@ -0,0 +1,237 @@ +package org.utbot.intellij.plugin.python.table; + +import com.intellij.openapi.actionSystem.BackgroundableDataProvider; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.DataProvider; +import com.intellij.refactoring.ui.EnableDisableAction; +import com.intellij.ui.*; +import com.intellij.ui.icons.RowIcon; +import com.intellij.ui.table.JBTable; +import com.intellij.util.ui.JBUI; +import com.jetbrains.python.psi.PyElement; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import java.awt.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class UtPyMemberSelectionTable extends JBTable implements BackgroundableDataProvider { + protected static final int CHECKED_COLUMN = 0; + protected static final int DISPLAY_NAME_COLUMN = 1; + protected static final int ICON_POSITION = 0; + + protected List myItems; + protected MyTableModel myTableModel; + private DataProvider dataProvider; + + public UtPyMemberSelectionTable(Collection items) { + myItems = new ArrayList<>(items); + myTableModel = new MyTableModel<>(this); + setModel(myTableModel); + + TableColumnModel model = getColumnModel(); + model.getColumn(DISPLAY_NAME_COLUMN).setCellRenderer(new MyTableRenderer<>(this)); + TableColumn checkBoxColumn = model.getColumn(CHECKED_COLUMN); + TableUtil.setupCheckboxColumn(checkBoxColumn); + checkBoxColumn.setCellRenderer(new MyBooleanRenderer<>(this)); + setPreferredScrollableViewportSize(JBUI.size(400, -1)); + setVisibleRowCount(12); + getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + setShowGrid(false); + setIntercellSpacing(new Dimension(0, 0)); + new MyEnableDisableAction().register(); + } + + public void setItems(Collection items) { + myItems = new ArrayList<>(items); + } + + @Override + public @Nullable DataProvider createBackgroundDataProvider() { + if (dataProvider == null) { + dataProvider = new DataProvider() { + @Override + public @Nullable Object getData(@NotNull @NonNls String dataId) { + if (CommonDataKeys.PSI_ELEMENT.is(dataId)) { + for (UtPyTableItem item : getSelectedMemberInfos()) { + PyElement pyElement = item.getContent(); + if (pyElement != null) return pyElement; + } + } + return null; + } + }; + } + return dataProvider; + } + + public Collection getSelectedMemberInfos() { + ArrayList list = new ArrayList<>(myItems.size()); + for (T info : myItems) { + if (info.isChecked()) { + list.add(info); + } + } + return list; + } + + private class MyEnableDisableAction extends EnableDisableAction { + + @Override + protected JTable getTable() { + return UtPyMemberSelectionTable.this; + } + + @Override + protected void applyValue(int[] rows, boolean valueToBeSet) { + for (int row : rows) { + final T memberInfo = myItems.get(row); + memberInfo.setChecked(valueToBeSet); + } + final int[] selectedRows = getSelectedRows(); + final ListSelectionModel selectionModel = getSelectionModel(); + for (int selectedRow : selectedRows) { + selectionModel.addSelectionInterval(selectedRow, selectedRow); + } + } + + @Override + protected boolean isRowChecked(final int row) { + return myItems.get(row).isChecked(); + } + } + + private static class MyBooleanRenderer extends BooleanTableCellRenderer { + private final UtPyMemberSelectionTable myTable; + + MyBooleanRenderer(UtPyMemberSelectionTable table) { + myTable = table; + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (component instanceof JCheckBox) { + int modelColumn = myTable.convertColumnIndexToModel(column); + T itemInfo = myTable.myItems.get(row); + component.setEnabled(modelColumn == CHECKED_COLUMN || itemInfo.isChecked()); + } + return component; + } + } + + private static class MyTableRenderer extends ColoredTableCellRenderer { + private final UtPyMemberSelectionTable myTable; + + MyTableRenderer(UtPyMemberSelectionTable table) { + myTable = table; + } + + @Override + public void customizeCellRenderer(@NotNull JTable table, final Object value, + boolean isSelected, boolean hasFocus, final int row, final int column) { + + final int modelColumn = myTable.convertColumnIndexToModel(column); + final T item = myTable.myItems.get(row); + if (modelColumn == DISPLAY_NAME_COLUMN) { + Icon itemIcon = item.getIcon(); + RowIcon icon = IconManager.getInstance().createRowIcon(3); + icon.setIcon(itemIcon, ICON_POSITION); + setIcon(icon); + } + else { + setIcon(null); + } + setIconOpaque(false); + setOpaque(false); + + if (value == null) return; + append((String)value); + } + + } + + protected static class MyTableModel extends AbstractTableModel { + private final UtPyMemberSelectionTable myTable; + private Boolean removePrefix; + + public MyTableModel(UtPyMemberSelectionTable table) { + myTable = table; + } + + private void initRemovePrefix() { + List names = new ArrayList<>(); + for (UtPyTableItem item: myTable.myItems) { + names.add(item.getIdName()); + } + removePrefix = Utils.haveCommonPrefix(names); + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public int getRowCount() { + return myTable.myItems.size(); + } + + @Override + public Class getColumnClass(int columnIndex) { + if (columnIndex == CHECKED_COLUMN) { + return Boolean.class; + } + return super.getColumnClass(columnIndex); + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (removePrefix == null) { + initRemovePrefix(); + } + final T itemInfo = myTable.myItems.get(rowIndex); + if (columnIndex == CHECKED_COLUMN) { + return itemInfo.isChecked(); + } else if (columnIndex == DISPLAY_NAME_COLUMN) { + if (removePrefix) { + return Utils.getSuffix(itemInfo.getIdName()); + } + return itemInfo.getIdName(); + } else { + throw new RuntimeException("Incorrect column index"); + } + } + + @Override + public String getColumnName(int column) { + if (column == CHECKED_COLUMN) { + return " "; + } else if (column == DISPLAY_NAME_COLUMN) { + return "Members"; + } else { + throw new RuntimeException("Incorrect column index"); + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return columnIndex == CHECKED_COLUMN; + } + + + @Override + public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex) { + if (columnIndex == CHECKED_COLUMN) { + myTable.myItems.get(rowIndex).setChecked((Boolean) aValue); + } + } + } +} diff --git a/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyTableItem.java b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyTableItem.java new file mode 100644 index 0000000000..c1e413016a --- /dev/null +++ b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/UtPyTableItem.java @@ -0,0 +1,18 @@ +package org.utbot.intellij.plugin.python.table; + +import com.jetbrains.python.psi.PyElement; + +import javax.swing.*; + +public interface UtPyTableItem { + + public PyElement getContent(); + + public String getIdName(); + + public Icon getIcon(); + + boolean isChecked(); + + void setChecked(boolean valueToBeSet); +} diff --git a/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/Utils.java b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/Utils.java new file mode 100644 index 0000000000..0f23b1ba4c --- /dev/null +++ b/utbot-intellij-python/src/main/java/org/utbot/intellij/plugin/python/table/Utils.java @@ -0,0 +1,27 @@ +package org.utbot.intellij.plugin.python.table; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Utils { + public static Boolean haveCommonPrefix(List strings) { + Set prefixes = new HashSet<>(); + for (String str: strings) { + prefixes.add(getPrefix(str)); + } + return prefixes.size() <= 1; + } + + public static String getPrefix(String str) { + String suffix = getSuffix(str); + int len = str.length(); + return str.substring(0, len-suffix.length()-1); + } + + public static String getSuffix(String str) { + String[] parts = str.split("\\."); + int len = parts.length; + return parts[len-1]; + } +} diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/IntellijRequirementsInstaller.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/IntellijRequirementsInstaller.kt new file mode 100644 index 0000000000..08aa8110db --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/IntellijRequirementsInstaller.kt @@ -0,0 +1,80 @@ +package org.utbot.intellij.plugin.python + +import com.intellij.notification.NotificationType +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.dsl.builder.panel +import org.utbot.intellij.plugin.ui.Notifier +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.python.utils.RequirementsInstaller +import org.utbot.python.utils.RequirementsUtils +import javax.swing.JComponent +import org.jetbrains.concurrency.runAsync + + +class IntellijRequirementsInstaller( + val project: Project, +): RequirementsInstaller { + override fun checkRequirements(pythonPath: String, requirements: List): Boolean { + return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements) + } + + override fun installRequirements(pythonPath: String, requirements: List) { + invokeLater { + if (InstallRequirementsDialog(requirements).showAndGet()) { + runAsync { + val installResult = RequirementsUtils.installRequirements(pythonPath, requirements) + invokeLater { + if (installResult.exitValue != 0) { + showErrorDialogLater( + project, + "Requirements installing failed.
    " + + "${installResult.stderr}

    " + + "Try to install with pip:
    " + + " ${requirements.joinToString("
    ")}", + "Requirements error" + ) + } else { + invokeLater { + runReadAction { + PythonNotifier.notify("Requirements installation is complete") + } + } + } + } + } + } + } + } +} + + +class InstallRequirementsDialog(private val requirements: List) : DialogWrapper(true) { + init { + title = "Python Requirements Installation" + init() + } + + private lateinit var panel: DialogPanel + + override fun createCenterPanel(): JComponent { + panel = panel { + row("Some requirements are not installed.") { } + row("Requirements:") { } + indent { + requirements.map { row {text(it)} } + } + row("Install them?") { } + } + return panel + } +} + +object PythonNotifier : Notifier() { + override val notificationType: NotificationType = NotificationType.INFORMATION + + override val displayId: String = "Python notification" +} diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonDialogProcessor.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonDialogProcessor.kt new file mode 100644 index 0000000000..e0a250ac1f --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonDialogProcessor.kt @@ -0,0 +1,420 @@ +package org.utbot.intellij.plugin.python + +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task.Backgroundable +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDirectory +import com.intellij.util.concurrency.AppExecutorUtil +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyElement +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import com.jetbrains.python.sdk.pythonSdk +import mu.KotlinLogging +import org.utbot.common.PathUtil.toPath +import org.utbot.framework.plugin.api.util.LockFile +import org.utbot.intellij.plugin.settings.Settings +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.python.PythonMethodHeader +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.TestFileInformation +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.codegen.PythonCgLanguageAssistant +import org.utbot.python.newtyping.mypy.dropInitFile +import org.utbot.python.utils.RequirementsInstaller +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import kotlin.io.path.Path + +object PythonDialogProcessor { + private val logger = KotlinLogging.logger {} + + enum class ProgressRange(val from : Double, val to: Double) { + ANALYZE(from = 0.0, to = 0.1), + SOLVING(from = 0.1, to = 0.95), + CODEGEN(from = 0.95, to = 1.0), + } + + private fun updateIndicator( + indicator: ProgressIndicator, + range: ProgressRange, + text: String, + fraction: Double, + stepCount: Int, + stepNumber: Int = 0, + ) { + assert(stepCount > stepNumber) + val maxValue = 1.0 / stepCount + val shift = stepNumber.toDouble() + invokeLater { + if (indicator.isCanceled) return@invokeLater + text.let { indicator.text = it } + indicator.fraction = indicator.fraction + .coerceAtLeast((shift + range.from + (range.to - range.from) * fraction.coerceIn(0.0, 1.0)) * maxValue) + logger.debug("Phase ${indicator.text} with progress ${String.format("%.2f",indicator.fraction)}") + } + } + + private fun runIndicatorWithTimeHandler(indicator: ProgressIndicator, range: ProgressRange, text: String, globalCount: Int, globalShift: Int, timeout: Long): ScheduledFuture<*> { + val startTime = System.currentTimeMillis() + return AppExecutorUtil.getAppScheduledExecutorService().scheduleWithFixedDelay({ + val innerTimeoutRatio = + ((System.currentTimeMillis() - startTime).toDouble() / timeout) + .coerceIn(0.0, 1.0) + updateIndicator( + indicator, + range, + text, + innerTimeoutRatio, + globalCount, + globalShift, + ) + }, 0, 100, TimeUnit.MILLISECONDS) + } + + private fun updateIndicatorTemplate( + indicator: ProgressIndicator, + stepCount: Int, + stepNumber: Int + ): (ProgressRange, String, Double) -> Unit { + return { range: ProgressRange, text: String, fraction: Double -> + updateIndicator( + indicator, + range, + text, + fraction, + stepCount, + stepNumber + ) + } + } + + fun createDialogAndGenerateTests( + project: Project, + elementsToShow: Set, + focusedElement: PyElement?, + editor: Editor? = null, + ) { + editor?.let{ + runWriteAction { + with(FileDocumentManager.getInstance()) { + saveDocument(it.document) + } + } + } + val pythonPath = getPythonPath(project) + if (pythonPath == null) { + showErrorDialogLater( + project, + message = "Couldn't find Python interpreter", + title = "Python test generation error" + ) + } else { + val dialog = createDialog( + project, + elementsToShow, + focusedElement, + pythonPath, + ) + if (!dialog.showAndGet()) { + return + } + createTests(project, dialog.model) + } + } + + private fun createDialog( + project: Project, + elementsToShow: Set, + focusedElement: PyElement?, + pythonPath: String, + ): PythonDialogWindow { + val focusedElements = focusedElement + ?.let { setOf(focusedElement.toUtPyTableItem()).filterNotNull() } + ?.toSet() + + return PythonDialogWindow( + PythonTestsModel( + project, + elementsToShow, + focusedElements, + project.service().generationTimeoutInMillis, + project.service().hangingTestsTimeout.timeoutMs, + cgLanguageAssistant = PythonCgLanguageAssistant, + pythonPath = pythonPath, + names = elementsToShow.associateBy { Pair(it.fileName()!!, it.name!!) }, + ) + ) + } + + private fun findSelectedPythonMethods(model: PythonTestLocalModel): List { + return ReadAction.nonBlocking> { + model.selectedElements + .filter { model.selectedElements.contains(it) } + .flatMap { + when (it) { + is PyFunction -> listOf(it) + is PyClass -> it.methods.toList() + else -> emptyList() + } + } + .filter { fineFunction(it) } + .mapNotNull { + val functionName = it.name ?: return@mapNotNull null + val moduleFilename = it.containingFile.virtualFile?.canonicalPath ?: "" + val containingClassId = it.containingClass?.qualifiedName?.let{ cls -> PythonClassId(cls) } + PythonMethodHeader( + functionName, + moduleFilename, + containingClassId, + ) + } + .toSet() + .toList() + }.executeSynchronously() ?: emptyList() + } + + private fun groupPyElementsByModule(model: PythonTestsModel): Set { + return ReadAction.nonBlocking> { + model.selectedElements + .groupBy { it.containingFile } + .flatMap { fileGroup -> + fileGroup.value + .groupBy { it is PyClass }.values + } + .flatMap { fileGroup -> + val classes = fileGroup.filterIsInstance() + val functions = fileGroup.filterIsInstance() + val groups: List> = classes.map { listOf(it) } + listOf(functions) + groups + } + .filter { it.isNotEmpty() } + .map { + val realElements = it.map { member -> model.names[Pair(member.fileName(), member.name)]!! } + val file = realElements.first().containingFile as PyFile + val srcModule = getSrcModule(realElements.first()) + + val (directoriesForSysPath, moduleToImport) = getDirectoriesForSysPath(srcModule, file) + PythonTestLocalModel( + model.project, + model.timeout, + model.timeoutForRun, + model.cgLanguageAssistant, + model.pythonPath, + model.testSourceRootPath, + model.testFramework, + realElements.toSet(), + model.runtimeExceptionTestsBehaviour, + directoriesForSysPath, + moduleToImport.dropInitFile(), + file, + realElements.first().let { pyElement -> + if (pyElement is PyFunction) { + pyElement.containingClass + } else { + null + } + } + ) + } + .toSet() + }.executeSynchronously() ?: emptySet() + } + + private fun createTests(project: Project, baseModel: PythonTestsModel) { + ProgressManager.getInstance().run(object : Backgroundable(project, "Generate python tests") { + override fun run(indicator: ProgressIndicator) { + if (!LockFile.lock()) { + return + } + try { + indicator.text = "Checking requirements..." + indicator.isIndeterminate = false + + val installer = IntellijRequirementsInstaller(project) + + val requirementsAreInstalled = RequirementsInstaller.checkRequirements( + installer, + baseModel.pythonPath, + if (baseModel.testFramework.isInstalled) emptyList() else listOf(baseModel.testFramework.mainPackage) + ) + if (!requirementsAreInstalled) { + return + } + + val modelGroups = groupPyElementsByModule(baseModel) + val totalModules = modelGroups.size + + modelGroups.forEachIndexed { index, model -> + val localUpdateIndicator = updateIndicatorTemplate(indicator, totalModules, index) + localUpdateIndicator(ProgressRange.ANALYZE, "Analyze code: read files", 0.1) + + val methods = findSelectedPythonMethods(model) + val content = getContentFromPyFile(model.file) + + val config = PythonTestGenerationConfig( + pythonPath = model.pythonPath, + testFileInformation = TestFileInformation(model.file.virtualFile.path, content, model.currentPythonModule), + sysPathDirectories = model.directoriesForSysPath, + testedMethods = methods, + timeout = model.timeout, + timeoutForRun = model.timeoutForRun, + testFramework = model.testFramework, + testSourceRootPath = Path(model.testSourceRootPath), + withMinimization = true, + isCanceled = { indicator.isCanceled }, + runtimeExceptionTestsBehaviour = model.runtimeExceptionTestsBehaviour + ) + val processor = PythonIntellijProcessor( + config, + project, + model + ) + + localUpdateIndicator(ProgressRange.ANALYZE, "Analyze module ${model.currentPythonModule}", 0.5) + + val mypyConfig = processor.sourceCodeAnalyze() + + localUpdateIndicator(ProgressRange.ANALYZE, "Analyze module ${model.currentPythonModule}", 1.0) + + val timerHandler = runIndicatorWithTimeHandler( + indicator, + ProgressRange.SOLVING, + "Generate test cases for module ${model.currentPythonModule}", + totalModules, + index, + model.timeout, + ) + try { + val testSets = processor.testGenerate(mypyConfig) + timerHandler.cancel(true) + if (testSets.isEmpty()) return@forEachIndexed + + localUpdateIndicator(ProgressRange.CODEGEN, "Generate tests code for module ${model.currentPythonModule}", 0.0) + val testCode = processor.testCodeGenerate(testSets) + + localUpdateIndicator(ProgressRange.CODEGEN, "Saving tests module ${model.currentPythonModule}", 0.9) + processor.saveTests(testCode) + + logger.info( + "Finished test generation for the following functions: ${ + testSets.map { it.method.name }.toSet().joinToString() + }" + ) + } finally { + timerHandler.cancel(true) + } + } + } finally { + LockFile.unlock() + } + } + }) + } +} + +fun getPythonPath(project: Project): String? { + return project.pythonSdk?.homePath +} + +fun getSrcModule(element: PyElement): Module { + return ModuleUtilCore.findModuleForPsiElement(element) ?: error("Module for source class or function not found") +} + +fun getContentFromPyFile(file: PyFile) = + ReadAction.nonBlocking { + file.viewProvider.contents.toString() + }.executeSynchronously() ?: error("Cannot read file $file") + +/* + * Returns set of sys paths and tested file import path + */ +fun getDirectoriesForSysPath( + srcModule: Module, + file: PyFile +): Pair, String> { + return ReadAction.nonBlocking, String>> { + val sources = ModuleRootManager.getInstance(srcModule).getSourceRoots(false).toMutableList() + val ancestor = ProjectFileIndex.getInstance(file.project).getContentRootForFile(file.virtualFile) + if (ancestor != null) + sources.add(ancestor) + + // Collect sys.path directories with imported modules + val importedPaths = emptyList().toMutableList() + + // 1. import + file.importTargets.forEach { importTarget -> + importTarget.multiResolve().forEach { + val element = it.element + if (element != null) { + val directory = element.parent + if (directory is PsiDirectory) { + // If we have `import a.b.c` we need to add syspath to module `a` only + val additionalLevel = importTarget.importedQName?.componentCount?.dec() ?: 0 + directory.topParent(additionalLevel)?.let { dir -> + importedPaths.add(dir.virtualFile) + } + } + } + } + } + + // 2. from import ... + file.fromImports.forEach { importTarget -> + importTarget.resolveImportSourceCandidates().forEach { + val directory = it.parent + val isRelativeImport = + importTarget.relativeLevel > 0 // If we have `from . import a` we don't need to add syspath + if (directory is PsiDirectory && !isRelativeImport) { + // If we have `from a.b.c import d` we need to add syspath to module `a` only + val additionalLevel = importTarget.importSourceQName?.componentCount?.dec() ?: 0 + directory.topParent(additionalLevel)?.let { dir -> + importedPaths.add(dir.virtualFile) + } + } + } + } + + // Select modules only from this project but not from installation directory + importedPaths.forEach { + val path = it.toNioPath() + val hasSitePackages = + (0 until (path.nameCount)).any { i -> path.subpath(i, i + 1).toString() == "site-packages" } + if (it.isProjectSubmodule(ancestor) && !hasSitePackages) { + sources.add(it) + } + } + + val fileName = file.name.removeSuffix(".py") + val importPath = ancestor?.let { + VfsUtil.getParentDir( + VfsUtilCore.getRelativeLocation(file.virtualFile, it) + ) + } ?: "" + val importStringPath = listOf( + importPath.toPath().joinToString("."), + fileName + ) + .filterNot { it.isEmpty() } + .joinToString(".") + + Pair( + sources.map { it.path }.toSet(), + importStringPath + ) + }.executeSynchronously() ?: error("Cannot collect sys path directories") +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonDialogWindow.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonDialogWindow.kt new file mode 100644 index 0000000000..550ab70748 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonDialogWindow.kt @@ -0,0 +1,163 @@ +package org.utbot.intellij.plugin.python + +import com.intellij.openapi.components.service +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.JBIntSpinner +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.components.BorderLayoutPanel +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyFunction +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.intellij.plugin.python.settings.PythonTestFrameworkMapper +import org.utbot.intellij.plugin.python.settings.loadStateFromModel +import org.utbot.intellij.plugin.python.table.UtPyClassItem +import org.utbot.intellij.plugin.python.table.UtPyFunctionItem +import org.utbot.intellij.plugin.python.table.UtPyMemberSelectionTable +import org.utbot.intellij.plugin.python.table.UtPyTableItem +import org.utbot.intellij.plugin.settings.Settings +import org.utbot.intellij.plugin.ui.components.TestSourceDirectoryChooser +import org.utbot.intellij.plugin.ui.utils.createTestFrameworksRenderer +import java.util.concurrent.TimeUnit +import javax.swing.DefaultComboBoxModel +import javax.swing.JComponent + +private const val WILL_BE_INSTALLED_LABEL = " (will be installed)" +private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 5 +private const val STEP_TIMEOUT_VALUE_IN_SECONDS = 5 +private const val ACTION_GENERATE = "Generate Tests" + +class PythonDialogWindow(val model: PythonTestsModel) : DialogWrapper(model.project) { + + private val pyElementsTable = UtPyMemberSelectionTable(emptyList()) + private val testSourceFolderField = TestSourceDirectoryChooser(model) + private val timeoutSpinnerForTotalTimeout = + JBIntSpinner( + TimeUnit.MILLISECONDS.toSeconds(model.timeout).toInt(), + MINIMUM_TIMEOUT_VALUE_IN_SECONDS, + Int.MAX_VALUE, + STEP_TIMEOUT_VALUE_IN_SECONDS + ) + private val testFrameworks = + ComboBox(DefaultComboBoxModel(model.cgLanguageAssistant.getLanguageTestFrameworkManager().testFrameworks.toTypedArray())) + + private lateinit var panel: DialogPanel + private lateinit var currentFrameworkItem: TestFramework + + init { + title = "Generate Tests with UnitTestBot" + isResizable = false + + model.cgLanguageAssistant.getLanguageTestFrameworkManager().testFrameworks.forEach { + it.isInstalled = it.isInstalled || checkModuleIsInstalled(model.pythonPath, it.mainPackage) + } + + init() + setOKButtonText(ACTION_GENERATE) + } + + override fun createCenterPanel(): JComponent { + panel = panel { + row("Test sources root:") { + cell(testSourceFolderField).align(Align.FILL) + } + row("Testing framework:") { + cell(testFrameworks) + } + row("Test generation timeout:") { + cell(BorderLayoutPanel().apply { + addToLeft(timeoutSpinnerForTotalTimeout) + addToRight(JBLabel("seconds per group")) + }) + contextHelp("Set the timeout for all test generation processes per class or top level functions in one module to complete.") + } + row("Generate tests for:") {} + row { + cell(JBScrollPane(pyElementsTable)).align(Align.FILL) + } + } + + initDefaultValues() + updatePyElementsTable() + return panel + } + + private fun initDefaultValues() { + val settings = model.project.service() + + val installedTestFramework = PythonTestFrameworkMapper.allItems.singleOrNull { it.isInstalled } + val testFramework = PythonTestFrameworkMapper.handleUnknown(settings.testFramework) + currentFrameworkItem = installedTestFramework ?: testFramework + + updateTestFrameworksList() + } + + private fun updateTestFrameworksList() { + testFrameworks.item = currentFrameworkItem + testFrameworks.renderer = createTestFrameworksRenderer(WILL_BE_INSTALLED_LABEL) + } + + private fun updatePyElementsTable() { + val functions = model.elementsToDisplay.filterIsInstance() + val classes = model.elementsToDisplay.filterIsInstance() + val functionItems = functions + .groupBy { it.containingClass } + .flatMap { (_, pyFuncs) -> + pyFuncs.map { UtPyFunctionItem(it) } + } + val classItems = classes.map { + UtPyClassItem(it) + } + val items = classItems + functionItems + updateMethodsTable(items) + val height = pyElementsTable.rowHeight * (items.size.coerceAtMost(12) + 1) + pyElementsTable.preferredScrollableViewportSize = JBUI.size(-1, height) + } + + private fun updateMethodsTable(allMethods: Collection) { + val focusedNames = model.focusedElements?.map { it.idName } + val selectedMethods = allMethods.filter { + focusedNames?.contains(it.idName) ?: false + } + + if (selectedMethods.isEmpty()) { + checkMembers(allMethods) + } else { + checkMembers(selectedMethods) + } + + pyElementsTable.setItems(allMethods) + } + + private fun checkMembers(members: Collection) = members.forEach { it.isChecked = true } + + override fun doOKAction() { + val selectedMembers = pyElementsTable.selectedMemberInfos + model.selectedElements = selectedMembers.mapNotNull { it.content }.toSet() + model.testFramework = testFrameworks.item + model.timeout = TimeUnit.SECONDS.toMillis(timeoutSpinnerForTotalTimeout.number.toLong()) + model.testSourceRootPath = testSourceFolderField.text + model.projectType = ProjectType.Python + + val settings = model.project.service() + with(settings) { + model.timeoutForRun = hangingTestsTimeout.timeoutMs + model.runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour + } + + loadStateFromModel(settings, model) + + super.doOKAction() + } + + override fun doValidate(): ValidationInfo? { + return testSourceFolderField.validatePath() + } +} diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonIntellijProcessor.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonIntellijProcessor.kt new file mode 100644 index 0000000000..bb1408958f --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonIntellijProcessor.kt @@ -0,0 +1,79 @@ +package org.utbot.intellij.plugin.python + +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiFileFactory +import com.jetbrains.python.psi.PyClass +import org.utbot.intellij.plugin.python.language.PythonLanguageAssistant +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.PythonTestSet +import org.utbot.python.utils.camelToSnakeCase +import java.nio.file.Path +import java.nio.file.Paths + +class PythonIntellijProcessor( + override val configuration: PythonTestGenerationConfig, + val project: Project, + val model: PythonTestLocalModel, +) : PythonTestGenerationProcessor() { + override fun saveTests(testsCode: String) { + invokeLater { + runWriteAction { + val testDir = createPsiDirectoryForTestSourceRoot(model) + val testFileName = getOutputFileName(model) + val testPsiFile = PsiFileFactory.getInstance(model.project) + .createFileFromText(testFileName, PythonLanguageAssistant.language, testsCode) + testDir.findFile(testPsiFile.name)?.delete() + testDir.add(testPsiFile) + val file = testDir.findFile(testPsiFile.name)!! + OpenFileDescriptor(project, file.virtualFile).navigate(true) + } + } + } + + private fun getDirectoriesFromRoot(root: Path, path: Path): List { + if (path == root || path.parent == null) + return emptyList() + return getDirectoriesFromRoot(root, path.parent) + listOf(path.fileName.toString()) + } + + private fun createPsiDirectoryForTestSourceRoot(model: PythonTestLocalModel): PsiDirectory { + val root = getContentRoot(model.project, model.file.virtualFile) + val paths = getDirectoriesFromRoot( + Paths.get(root.path), + Paths.get(model.testSourceRootPath) + ) + val rootPSI = getContainingElement(model.file) { it.virtualFile == root }!! + return paths.fold(rootPSI) { acc, folderName -> + acc.findSubdirectory(folderName) ?: acc.createSubdirectory(folderName) + } + } + + private fun getOutputFileName(model: PythonTestLocalModel): String { + val moduleName = model.currentPythonModule.camelToSnakeCase().replace('.', '_') + return if (model.selectedElements.size == 1 && model.selectedElements.first() is PyClass) { + val className = model.selectedElements.first().name?.camelToSnakeCase()?.replace('.', '_') + "test_${moduleName}_$className.py" + } else if (model.containingClass == null) { + "test_$moduleName.py" + } else { + val className = model.containingClass.name?.camelToSnakeCase()?.replace('.', '_') + "test_${moduleName}_$className.py" + } + } + + override fun notGeneratedTestsAction(testedFunctions: List) { + showErrorDialogLater( + project, + message = "Cannot create tests for the following functions: " + testedFunctions.joinToString(), + title = "Python test generation error" + ) + } + + override fun processCoverageInfo(testSets: List) { } +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonTestsModel.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonTestsModel.kt new file mode 100644 index 0000000000..a0ce0e4984 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/PythonTestsModel.kt @@ -0,0 +1,45 @@ +package org.utbot.intellij.plugin.python + +import com.intellij.openapi.project.Project +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyElement +import com.jetbrains.python.psi.PyFile +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.services.language.CgLanguageAssistant +import org.utbot.intellij.plugin.python.table.UtPyTableItem +import org.utbot.intellij.plugin.models.BaseTestsModel + +class PythonTestsModel( + project: Project, + val elementsToDisplay: Set, + val focusedElements: Set?, + var timeout: Long, + var timeoutForRun: Long, + val cgLanguageAssistant: CgLanguageAssistant, + val pythonPath: String, + val names: Map, PyElement>, +) : BaseTestsModel( + project, +) { + lateinit var testSourceRootPath: String + lateinit var testFramework: TestFramework + var selectedElements: Set = emptySet() + lateinit var runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour +} + +data class PythonTestLocalModel( + val project: Project, + val timeout: Long, + val timeoutForRun: Long, + val cgLanguageAssistant: CgLanguageAssistant, + val pythonPath: String, + val testSourceRootPath: String, + val testFramework: TestFramework, + val selectedElements: Set, + val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour, + val directoriesForSysPath: Set, + val currentPythonModule: String, + val file: PyFile, + val containingClass: PyClass?, +) \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/Utils.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/Utils.kt new file mode 100644 index 0000000000..73aab6b635 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/Utils.kt @@ -0,0 +1,74 @@ +package org.utbot.intellij.plugin.python + +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyElement +import com.jetbrains.python.psi.PyFunction +import org.utbot.intellij.plugin.python.table.UtPyClassItem +import org.utbot.intellij.plugin.python.table.UtPyFunctionItem +import org.utbot.intellij.plugin.python.table.UtPyTableItem +import org.utbot.python.utils.RequirementsUtils + +inline fun getContainingElement( + element: PsiElement, + predicate: (T) -> Boolean = { true } +): T? { + var result = element + while ((result !is T || !predicate(result)) && (result.parent != null)) { + result = result.parent + } + return result as? T +} + +fun getAncestors(element: PsiElement): List = + if (element.parent == null) + listOf(element) + else + getAncestors(element.parent) + element + +fun getContentRoot(project: Project, file: VirtualFile): VirtualFile { + return ProjectFileIndex.getInstance(project) + .getContentRootForFile(file) ?: error("Source file lies outside of a module") +} + +fun VirtualFile.isProjectSubmodule(ancestor: VirtualFile?): Boolean { + return VfsUtil.isUnder(this, setOf(ancestor).toMutableSet()) +} + +fun checkModuleIsInstalled(pythonPath: String, moduleName: String): Boolean { + return RequirementsUtils.requirementsAreInstalled(pythonPath, listOf(moduleName)) +} + +fun fineFunction(function: PyFunction): Boolean { + val hasNotConstructorName = !listOf("__init__", "__new__").contains(function.name) + val decoratorNames = function.decoratorList?.decorators?.mapNotNull { it?.qualifiedName } + val knownDecorators = decoratorNames?.all { it.toString() in listOf("staticmethod") } ?: true + return hasNotConstructorName && knownDecorators +} + +fun fineClass(pyClass: PyClass): Boolean = + getAncestors(pyClass).dropLast(1).all { it !is PyClass && it !is PyFunction } && + pyClass.methods.any { fineFunction(it) } + +fun PsiDirectory.topParent(level: Int): PsiDirectory? { + var directory: PsiDirectory? = this + repeat(level) { + directory = directory?.parent + } + return directory +} + +fun PyElement.fileName(): String? = this.containingFile.virtualFile.canonicalPath + +fun PyElement.toUtPyTableItem(): UtPyTableItem? { + return when (this) { + is PyClass -> UtPyClassItem(this) + is PyFunction -> UtPyFunctionItem(this) + else -> null + } +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/language/PythonLanguageAssistant.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/language/PythonLanguageAssistant.kt new file mode 100644 index 0000000000..8c8a9a3a82 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/language/PythonLanguageAssistant.kt @@ -0,0 +1,184 @@ +package org.utbot.intellij.plugin.python.language + +import com.intellij.lang.Language +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import org.utbot.framework.plugin.api.util.LockFile +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import org.utbot.intellij.plugin.python.* + +@Suppress("unused") // is used in org.utbot.intellij.plugin.language.agnostic.LanguageAssistant via reflection +object PythonLanguageAssistant : LanguageAssistant() { + + private const val pythonID = "Python" + val language: Language = Language.findLanguageByID(pythonID) ?: error("Language wasn't found") + + data class Targets( + val pyClasses: Set, + val pyFunctions: Set, + val focusedClass: PyClass?, + val focusedFunction: PyFunction?, + val editor: Editor? + ) { + override fun toString(): String { + return "Targets($pyClasses, $pyFunctions)" + } + } + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val targets = getPsiTargets(e) ?: return + + PythonDialogProcessor.createDialogAndGenerateTests( + project, + targets.pyClasses + targets.pyFunctions, + targets.focusedFunction ?: targets.focusedClass, + targets.editor, + ) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = !LockFile.isLocked() && getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): Targets? { + val project = e.project ?: return null + val editor = e.getData(CommonDataKeys.EDITOR) + + val resultFunctions = mutableSetOf() + val resultClasses = mutableSetOf() + val focusedFunction: PyFunction? + var focusedClass: PyClass? = null + + if (editor != null) { + val file = e.getData(CommonDataKeys.PSI_FILE) as? PyFile ?: return null + val element = findPsiElement(file, editor) ?: return null + + val rootFunctions = file.topLevelFunctions.filter { fineFunction(it) } + val rootClasses = file.topLevelClasses.filter { fineClass(it) } + + val containingClass = getContainingElement(element) { fineClass(it) } + val containingFunction: PyFunction? = + if (containingClass == null) + getContainingElement(element) { it.parent is PsiFile && fineFunction(it) } + else + getContainingElement(element) { func -> + val ancestors = getAncestors(func) + ancestors.dropLast(1).all { it !is PyFunction } && + ancestors.count { it is PyClass } == 1 && fineFunction(func) + } + + if (rootClasses.isEmpty()) { + return if (rootFunctions.isEmpty()) { + null + } else { + resultFunctions.addAll(rootFunctions) + focusedFunction = containingFunction + Targets(resultClasses, resultFunctions, null, focusedFunction, editor) + } + } else { + if (containingClass == null) { + resultClasses.addAll(rootClasses) + resultFunctions.addAll(rootFunctions) + focusedFunction = containingFunction + } else { + resultFunctions.addAll(containingClass.methods.filter { fineFunction(it) }) + focusedClass = containingClass + focusedFunction = containingFunction + } + return Targets(resultClasses, resultFunctions, focusedClass, focusedFunction, editor) + } + } else { + val element = e.getData(CommonDataKeys.PSI_ELEMENT) + if (element is PsiFileSystemItem) { + e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { + val (classes, functions) = getAllElements(project, it.toList()) + resultFunctions.addAll(functions) + resultClasses.addAll(classes) + } + } else { + val someSelection = e.getData(PlatformDataKeys.PSI_ELEMENT_ARRAY)?: return null + someSelection.forEach { + when(it) { + is PsiFileSystemItem -> { + val (classes, functions) = getAllElements(project, listOf(it.virtualFile)) + resultFunctions += functions + resultClasses += classes + } + } + } + } + if (resultClasses.isNotEmpty() || resultFunctions.isNotEmpty()) { + return Targets(resultClasses, resultFunctions, null, null, null) + } + } + return null + } + + // this method is copy-paste from GenerateTestsActions.kt + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + + return element + } + + private fun getAllElements(project: Project, virtualFiles: Collection): Pair, Set> { + val psiFiles = virtualFiles.mapNotNull { + PsiManager.getInstance(project).findFile(it) + } + val psiDirectories = virtualFiles.mapNotNull { + PsiManager.getInstance(project).findDirectory(it) + } + + val classes = psiFiles.flatMap { getClassesFromFile(it) }.toMutableSet() + val functions = psiFiles.flatMap { getFunctionsFromFile(it) }.toMutableSet() + + psiDirectories.forEach { + classes.addAll(getAllClasses(it)) + functions.addAll(getAllFunctions(it)) + } + + return classes to functions + } + + private fun getAllFunctions(directory: PsiDirectory): Set { + val allFunctions = directory.files.flatMap { getFunctionsFromFile(it) }.toMutableSet() + directory.subdirectories.forEach { + allFunctions.addAll(getAllFunctions(it)) + } + return allFunctions + } + + private fun getAllClasses(directory: PsiDirectory): Set { + val allClasses = directory.files.flatMap { getClassesFromFile(it) }.toMutableSet() + directory.subdirectories.forEach { + allClasses.addAll(getAllClasses(it)) + } + return allClasses + } + + private fun getFunctionsFromFile(psiFile: PsiFile): List { + return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, PyFunction::class.java) + .map { it as PyFunction } + .filter { fineFunction(it) } + } + + private fun getClassesFromFile(psiFile: PsiFile): List { + return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, PyClass::class.java) + .map { it as PyClass } + .filter { fineClass(it) } + } +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/settings/PythonTestFrameworkMapper.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/settings/PythonTestFrameworkMapper.kt new file mode 100644 index 0000000000..0e2b60ac75 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/settings/PythonTestFrameworkMapper.kt @@ -0,0 +1,31 @@ +package org.utbot.intellij.plugin.python.settings + +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.intellij.plugin.settings.TestFrameworkMapper +import org.utbot.python.framework.codegen.PythonTestFrameworkManager +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest + +object PythonTestFrameworkMapper: TestFrameworkMapper { + override fun toString(value: TestFramework): String = value.id + + override fun fromString(value: String): TestFramework = when (value) { + Unittest.id -> Unittest + Pytest.id -> Pytest + else -> error("Unknown TestFramework $value") + } + + override fun handleUnknown(testFramework: TestFramework): TestFramework { + if (allItems.contains(testFramework)) { + return testFramework + } + return try { + fromString(testFramework.id) + } catch (ex: IllegalStateException) { + defaultItem + } + } + + val defaultItem: TestFramework get() = PythonTestFrameworkManager().defaultTestFramework + val allItems: List get() = PythonTestFrameworkManager().testFrameworks +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/settings/Settings.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/settings/Settings.kt new file mode 100644 index 0000000000..a61dfcb5f7 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/python/settings/Settings.kt @@ -0,0 +1,20 @@ +package org.utbot.intellij.plugin.python.settings + +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.intellij.plugin.python.PythonTestsModel +import org.utbot.intellij.plugin.settings.Settings + +fun loadStateFromModel(settings: Settings, model: PythonTestsModel) { + settings.loadState(fromGenerateTestsModel(model)) +} + +private fun fromGenerateTestsModel(model: PythonTestsModel): Settings.State { + return Settings.State( + sourceRootHistory = model.sourceRootHistory, + testFramework = model.testFramework, + generationTimeoutInMillis = model.timeout, + enableExperimentalLanguagesSupport = true, + hangingTestsTimeout = HangingTestsTimeout(model.timeoutForRun), + runtimeExceptionTestsBehaviour = model.runtimeExceptionTestsBehaviour, + ) +} diff --git a/utbot-intellij/build.gradle.kts b/utbot-intellij/build.gradle.kts index 0b0f35d7b2..8f0520b4a9 100644 --- a/utbot-intellij/build.gradle.kts +++ b/utbot-intellij/build.gradle.kts @@ -2,19 +2,50 @@ val intellijPluginVersion: String? by rootProject val kotlinLoggingVersion: String? by rootProject val apacheCommonsTextVersion: String? by rootProject val jacksonVersion: String? by rootProject -val ideType: String? by rootProject -val pythonCommunityPluginVersion: String? by rootProject -val pythonUltimatePluginVersion: String? by rootProject -val sootCommitHash: String? by rootProject + +val sootVersion: String? by rootProject val kryoVersion: String? by rootProject +val rdVersion: String? by rootProject val semVer: String? by rootProject + +val junit5Version: String by rootProject +val junit4PlatformVersion: String by rootProject + +// === IDE settings === +val projectType: String by rootProject +val communityEdition: String by rootProject +val ultimateEdition: String by rootProject + +val ideType: String by rootProject val androidStudioPath: String? by rootProject +val ideaVersion: String? by rootProject +val pycharmVersion: String? by rootProject +val golandVersion: String? by rootProject + +val javaIde: String? by rootProject +val pythonIde: String? by rootProject +val jsIde: String? by rootProject +val goIde: String? by rootProject + +val ideVersion = when(ideType) { + "PC", "PY" -> pycharmVersion + "GO" -> golandVersion + else -> ideaVersion +} + +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject +val goPluginVersion: String? by rootProject + // https://plugins.jetbrains.com/docs/intellij/android-studio.html#configuring-the-plugin-pluginxml-file val ideTypeOrAndroidStudio = if (androidStudioPath == null) ideType else "IC" +project.tasks.asMap["runIde"]?.enabled = false +// === IDE settings === + plugins { - id("org.jetbrains.intellij") version "1.7.0" + id("org.jetbrains.intellij") version "1.13.1" } intellij { @@ -22,8 +53,11 @@ intellij { val androidPlugins = listOf("org.jetbrains.android") val jvmPlugins = mutableListOf( - "java", - "org.jetbrains.kotlin:222-1.7.20-release-201-IJ4167.29" + "java" + ) + + val kotlinPlugins = listOf( + "org.jetbrains.kotlin" ) androidStudioPath?.let { jvmPlugins += androidPlugins } @@ -37,53 +71,100 @@ intellij { ) val jsPlugins = listOf( - "JavaScriptLanguage" + "JavaScript" + ) + + val goPlugins = listOf( + "org.jetbrains.plugins.go:${goPluginVersion}" + ) + + val mavenUtilsPlugins = listOf( + "org.jetbrains.idea.maven" ) + val basePluginSet = jvmPlugins + kotlinPlugins + mavenUtilsPlugins + androidPlugins + plugins.set( - when (ideType) { - "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins - "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins - "PC" -> pythonCommunityPlugins - "PU" -> pythonUltimatePlugins // something else, JS? - else -> jvmPlugins + when (projectType) { + communityEdition -> basePluginSet + pythonCommunityPlugins + ultimateEdition -> when (ideType) { + "IC" -> basePluginSet + pythonCommunityPlugins + "IU" -> basePluginSet + pythonUltimatePlugins + jsPlugins + goPlugins + "PC" -> pythonCommunityPlugins + "PY" -> pythonUltimatePlugins + jsPlugins + "GO" -> goPlugins + else -> basePluginSet + } + else -> basePluginSet } ) - version.set("222.4167.29") + version.set(ideVersion) type.set(ideTypeOrAndroidStudio) } +val remoteRobotVersion = "0.11.16" + tasks { compileKotlin { kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") allWarningsAsErrors = false } } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } runIde { jvmArgs("-Xmx2048m") + jvmArgs("--add-exports", "java.desktop/sun.awt.windows=ALL-UNNAMED") androidStudioPath?.let { ideDir.set(file(it)) } } patchPluginXml { - sinceBuild.set("212") - untilBuild.set("222.*") + sinceBuild.set("223") + untilBuild.set("232.*") version.set(semVer) } + + runIdeForUiTests { + jvmArgs("-Xmx2048m", "-Didea.is.internal=true", "-Didea.ui.debug.mode=true") + + systemProperty("robot-server.port", "8082") // default port 8580 + systemProperty("ide.mac.message.dialogs.as.sheets", "false") + systemProperty("jb.privacy.policy.text", "") + systemProperty("jb.consents.confirmation.enabled", "false") + systemProperty("idea.trust.all.projects", "true") + systemProperty("ide.mac.file.chooser.native", "false") + systemProperty("jbScreenMenuBar.enabled", "false") + systemProperty("apple.laf.useScreenMenuBar", "false") + systemProperty("ide.show.tips.on.startup.default.value", "false") + } + + downloadRobotServerPlugin { + version.set(remoteRobotVersion) + } + + test { + description = "Runs UI integration tests." + useJUnitPlatform { + exclude("/org/utbot/**") //Comment this line to run the tests locally + } + } +} + +repositories { + maven("https://jitpack.io") + maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") } dependencies { -// implementation("com.github.UnitTestBot:soot:${sootCommitHash}") - implementation(group ="com.jetbrains.rd", name = "rd-framework", version = "2022.3.1") - implementation(group ="com.jetbrains.rd", name = "rd-core", version = "2022.3.1") + implementation(group ="com.jetbrains.rd", name = "rd-framework", version = rdVersion) + implementation(group ="com.jetbrains.rd", name = "rd-core", version = rdVersion) implementation(group ="com.esotericsoftware.kryo", name = "kryo5", version = kryoVersion) implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) implementation(group = "org.apache.commons", name = "commons-text", version = apacheCommonsTextVersion) @@ -91,8 +172,28 @@ dependencies { implementation(group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version = jacksonVersion) implementation(project(":utbot-framework")) { exclude(group = "org.slf4j", module = "slf4j-api") } - implementation(project(":utbot-fuzzers")) + implementation(project(":utbot-spring-framework")) { exclude(group = "org.slf4j", module = "slf4j-api") } + implementation(project(":utbot-java-fuzzing")) //api(project(":utbot-analytics")) testImplementation("org.mock-server:mockserver-netty:5.4.1") testApi(project(":utbot-framework")) -} \ No newline at end of file + + implementation(project(":utbot-ui-commons")) + implementation(project(":utbot-android-studio")) + + testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion") + testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion") + + testImplementation("org.assertj:assertj-core:3.11.1") + + // Logging Network Calls + testImplementation("com.squareup.okhttp3:logging-interceptor:4.10.0") + + // Video Recording + implementation("com.automation-remarks:video-recorder-junit5:2.0") + + testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:$junit4PlatformVersion") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junit5Version") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:$junit5Version") +} diff --git a/utbot-intellij/readme.md b/utbot-intellij/readme.md index 9172f0f5b7..55b6a3c3ab 100644 --- a/utbot-intellij/readme.md +++ b/utbot-intellij/readme.md @@ -1,5 +1,5 @@ -UT Bot Intellij Plugin -====================== +UnitTestBot Intellij Plugin +=========================== To run/debug plugin in IDEA: @@ -10,4 +10,20 @@ To run/debug plugin in IDEA: To compile plugin: * run `gradle buildPlugin` -* find zipped plugin in build/distributions \ No newline at end of file +* find zipped plugin in build/distributions + +## UnitTestBot Intellij Plugin UI Tests + +* comment `exclude("/org/utbot/**")` in utbot-intellij/build.gradle.kts +* correct DEFAULT_PROJECT_DIRECTORY in RunInfo.kt if needed (it is your local directory in which test projects will be created locally) +* run IDEA in sandbox with IntelliJ Robot server plugin installed: `gradle runIdeForUiTests` +* wait till debug IDEA is started +* check it is above other windows and maximized +* check keyboard language is EN +* do NOT lock screen +* run **All** the tests in utbot-intellij/src/test/kotlin/org/utbot/tests + +Note: projects are created first and only on new projects tests are executed. +That is done for independency of each autotest run. + + diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/UtbotBundle.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/UtbotBundle.kt new file mode 100644 index 0000000000..a13bd6424c --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/UtbotBundle.kt @@ -0,0 +1,29 @@ +package org.utbot.intellij.plugin + +import com.intellij.DynamicBundle +import org.jetbrains.annotations.NonNls +import org.jetbrains.annotations.PropertyKey + +class UtbotBundle : DynamicBundle(BUNDLE) { + companion object { + @NonNls + private const val BUNDLE = "bundles.UtbotBundle" + private val INSTANCE: UtbotBundle = UtbotBundle() + + fun message( + @PropertyKey(resourceBundle = BUNDLE) key: String, + vararg params: Any + ): String { + return INSTANCE.getMessage(key, *params) + } + + fun takeIf( + @PropertyKey(resourceBundle = BUNDLE) key: String, + vararg params: Any, + condition: () -> Boolean): String? { + if (condition()) + return message(key, params) + return null + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt index d22c335d51..550c366084 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt @@ -1,7 +1,9 @@ package org.utbot.intellij.plugin.generator +import com.intellij.analysis.AnalysisScope import com.intellij.codeInsight.CodeInsightUtil import com.intellij.codeInsight.FileModificationService +import com.intellij.codeInsight.actions.OptimizeImportsProcessor import com.intellij.ide.fileTemplates.FileTemplateManager import com.intellij.ide.fileTemplates.FileTemplateUtil import com.intellij.ide.fileTemplates.JavaTemplateUtil @@ -16,16 +18,30 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileTypes.FileType import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.util.Computable import com.intellij.openapi.wm.ToolWindowManager -import com.intellij.psi.* +import com.intellij.psi.JavaDirectoryService +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassOwner +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiMethod +import com.intellij.psi.SmartPointerManager +import com.intellij.psi.SmartPsiElementPointer import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.codeStyle.JavaCodeStyleManager.DO_NOT_ADD_IMPORTS import com.intellij.psi.search.GlobalSearchScopesCore import com.intellij.testIntegration.TestIntegrationUtils -import com.intellij.util.IncorrectOperationException import com.siyeh.ig.psiutils.ImportUtils import mu.KotlinLogging import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass @@ -41,34 +57,50 @@ import org.jetbrains.kotlin.psi.KtPsiFactory import org.jetbrains.kotlin.psi.psiUtil.endOffset import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.utbot.common.FileUtil import org.utbot.common.HTML_LINE_SEPARATOR import org.utbot.common.PathUtil.toHtmlLinkTag -import org.utbot.framework.codegen.Import -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.RegularImport -import org.utbot.framework.codegen.StaticImport -import org.utbot.framework.codegen.model.UtilClassKind -import org.utbot.framework.codegen.model.UtilClassKind.Companion.UT_UTILS_CLASS_NAME +import org.utbot.framework.CancellationStrategyType.CANCEL_EVERYTHING +import org.utbot.framework.CancellationStrategyType.NONE +import org.utbot.framework.CancellationStrategyType.SAVE_PROCESSED_RESULTS +import org.utbot.framework.UtSettings +import org.utbot.framework.codegen.domain.Import +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.RegularImport +import org.utbot.framework.codegen.domain.StaticImport +import org.utbot.framework.codegen.tree.ututils.UtilClassKind +import org.utbot.framework.codegen.tree.ututils.UtilClassKind.Companion.UT_UTILS_INSTANCE_NAME import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.intellij.plugin.inspection.UnitTestBotInspectionManager import org.utbot.intellij.plugin.models.GenerateTestsModel import org.utbot.intellij.plugin.models.packageName import org.utbot.intellij.plugin.process.EngineProcess import org.utbot.intellij.plugin.process.RdTestGenerationResult import org.utbot.intellij.plugin.sarif.SarifReportIdea -import org.utbot.intellij.plugin.sarif.SourceFindingStrategyIdea -import org.utbot.intellij.plugin.ui.* +import org.utbot.intellij.plugin.ui.CommonLoggingNotifier +import org.utbot.intellij.plugin.ui.DetailsTestsReportNotifier +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.WarningTestsReportNotifier import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots -import org.utbot.intellij.plugin.util.IntelliJApiHelper.Target.* +import org.utbot.intellij.plugin.util.IntelliJApiHelper.Target.EDT_LATER +import org.utbot.intellij.plugin.util.IntelliJApiHelper.Target.THREAD_POOL +import org.utbot.intellij.plugin.util.IntelliJApiHelper.Target.WRITE_ACTION import org.utbot.intellij.plugin.util.IntelliJApiHelper.run import org.utbot.intellij.plugin.util.RunConfigurationHelper +import org.utbot.intellij.plugin.util.assertIsDispatchThread +import org.utbot.intellij.plugin.util.assertIsWriteThread import org.utbot.intellij.plugin.util.extractClassMethodsIncludingNested -import org.utbot.sarif.SarifReport import java.nio.file.Path +import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import org.utbot.intellij.plugin.util.showSettingsEditor +import org.utbot.sarif.* object CodeGenerationController { private val logger = KotlinLogging.logger {} @@ -85,53 +117,89 @@ object CodeGenerationController { model: GenerateTestsModel, classesWithTests: Map, psi2KClass: Map, - proc: EngineProcess + process: EngineProcess, + indicator: ProgressIndicator ) { + assertIsDispatchThread() val baseTestDirectory = model.testSourceRoot?.toPsiDirectory(model.project) ?: return val allTestPackages = getPackageDirectories(baseTestDirectory) val latch = CountDownLatch(classesWithTests.size) val testFilesPointers = mutableListOf>() + val srcClassPathToSarifReport = mutableMapOf() val utilClassListener = UtilClassListener() + var index = 0 for ((srcClass, generateResult) in classesWithTests) { + if (indicator.isCanceled) { + when (UtSettings.cancellationStrategyType) { + NONE, + SAVE_PROCESSED_RESULTS -> {} + CANCEL_EVERYTHING -> break + } + } + val (count, testSetsId) = generateResult - if (count <= 0) continue + if (count <= 0) { + latch.countDown() + continue + } + + val classUnderTest = psi2KClass[srcClass] ?: error("Didn't find KClass instance for class ${srcClass.name}") + val testClassName = process.findTestClassName(classUnderTest) + try { + UtTestsDialogProcessor.updateIndicator(indicator, UtTestsDialogProcessor.ProgressRange.CODEGEN, "Write test cases for class ${srcClass.name}", index.toDouble() / classesWithTests.size) val classPackageName = model.getTestClassPackageNameFor(srcClass) val testDirectory = allTestPackages[classPackageName] ?: baseTestDirectory - val testClass = createTestClass(srcClass, testDirectory, model) ?: continue - val testFilePointer = SmartPointerManager.getInstance(model.project).createSmartPsiElementPointer(testClass.containingFile) - val cut = psi2KClass[srcClass] ?: error("Didn't find KClass instance for class ${srcClass.name}") - runWriteCommandAction(model.project, "Generate tests with UtBot", null, { - try { - generateCodeAndReport(proc, testSetsId, srcClass, cut, testClass, testFilePointer, model, latch, utilClassListener) - testFilesPointers.add(testFilePointer) - } catch (e: IncorrectOperationException) { - logger.error { e } - showCreatingClassError(model.project, createTestClassName(srcClass)) - } + + val testClass = createTestClass(testClassName, testDirectory, model) ?: continue + val testFilePointer = SmartPointerManager.getInstance(model.project) + .createSmartPsiElementPointer(testClass.containingFile) + runWriteCommandAction(model.project, "Generate tests with UnitTestBot", null, { + generateCodeAndReport( + process, + testSetsId, + srcClass, + classUnderTest, + testClass, + testFilePointer, + srcClassPathToSarifReport, + model, + latch, + utilClassListener, + indicator + ) + testFilesPointers.add(testFilePointer) }) - } catch (e: IncorrectOperationException) { - logger.error { e } - showCreatingClassError(model.project, createTestClassName(srcClass)) + } catch (e : CancellationException) { + throw e + } catch (e: Exception) { + showCreatingClassError(model.project, testClassName) + } finally { + index++ } } - run(THREAD_POOL) { - waitForCountDown(latch) { - run(EDT_LATER) { - run(WRITE_ACTION) { - createUtilityClassIfNeed(utilClassListener, model, baseTestDirectory) - run(EDT_LATER) { - proceedTestReport(proc, model) - run(THREAD_POOL) { - val sarifReportsPath = - model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot) - mergeSarifReports(model, sarifReportsPath) - if (model.runGeneratedTestsWithCoverage) { - RunConfigurationHelper.runTestsWithCoverage(model, testFilesPointers) - } - proc.forceTermination() + run(THREAD_POOL, indicator, "Waiting for per-class Sarif reports") { + waitForCountDown(latch, indicator = indicator) { + run(EDT_LATER, indicator,"Go to EDT for utility class creation") { + run(WRITE_ACTION, indicator, "Need write action for utility class creation") { + createUtilityClassIfNeeded(utilClassListener, model, baseTestDirectory, indicator) + run(THREAD_POOL, indicator, "Generate summary Sarif report") { + proceedTestReport(process, model) + val sarifReportsPath = + model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot) + UtTestsDialogProcessor.updateIndicator(indicator, UtTestsDialogProcessor.ProgressRange.SARIF, "Merge Sarif reports", 0.75) + mergeSarifReports(model, sarifReportsPath) + if (model.runGeneratedTestsWithCoverage) { + UtTestsDialogProcessor.updateIndicator(indicator, UtTestsDialogProcessor.ProgressRange.SARIF, "Start tests with coverage", 0.95) + RunConfigurationHelper.runTestsWithCoverage(model, testFilesPointers) + } + process.terminate() + UtTestsDialogProcessor.updateIndicator(indicator, UtTestsDialogProcessor.ProgressRange.SARIF, "Generation finished", 1.0) + + run(EDT_LATER, null, "Run sarif-based inspections") { + runInspectionsIfNeeded(model, srcClassPathToSarifReport) } } } @@ -140,6 +208,28 @@ object CodeGenerationController { } } + /** + * Runs the UTBot inspection if there are detected errors. + */ + private fun runInspectionsIfNeeded( + model: GenerateTestsModel, + srcClassPathToSarifReport: MutableMap + ) { + if (!model.runInspectionAfterTestGeneration) { + return + } + val sarifHasResults = srcClassPathToSarifReport.any { (_, sarif) -> + sarif.getAllResults().isNotEmpty() + } + if (!sarifHasResults) { + return + } + UnitTestBotInspectionManager + .getInstance(model.project, SarifReport.minimizeSarifResults(srcClassPathToSarifReport)) + .createNewGlobalContext() + .doInspections(AnalysisScope(model.project)) + } + private fun proceedTestReport(proc: EngineProcess, model: GenerateTestsModel) { try { // Parametrized tests are not supported in tests report yet @@ -148,6 +238,7 @@ object CodeGenerationController { showTestsReport(proc, model) } } catch (e: Exception) { + logger.error(e) { "Failed to save tests report" } showErrorDialogLater( model.project, message = "Cannot save tests generation report: error occurred '${e.message}'", @@ -155,22 +246,25 @@ object CodeGenerationController { ) } } - private fun createUtilityClassIfNeed( + + private fun createUtilityClassIfNeeded( utilClassListener: UtilClassListener, model: GenerateTestsModel, - baseTestDirectory: PsiDirectory + baseTestDirectory: PsiDirectory, + indicator: ProgressIndicator ) { val requiredUtilClassKind = utilClassListener.requiredUtilClassKind ?: return // no util class needed val existingUtilClass = model.codegenLanguage.getUtilClassOrNull(model.project, model.testModule) - val utilClassKind = newUtilClassKindOrNull(existingUtilClass, requiredUtilClassKind) + val utilClassKind = newUtilClassKindOrNull(existingUtilClass, requiredUtilClassKind, model.codegenLanguage) if (utilClassKind != null) { createOrUpdateUtilClass( testDirectory = baseTestDirectory, utilClassKind = utilClassKind, existingUtilClass = existingUtilClass, - model = model + model = model, + indicator = indicator ) } } @@ -189,17 +283,21 @@ object CodeGenerationController { * @param requiredUtilClassKind the kind of the new util class that we attempt to generate. * @return an [UtilClassKind] of a new util class that will be created or `null`, if no new util class is needed. */ - private fun newUtilClassKindOrNull(existingUtilClass: PsiFile?, requiredUtilClassKind: UtilClassKind): UtilClassKind? { + private fun newUtilClassKindOrNull( + existingUtilClass: PsiFile?, + requiredUtilClassKind: UtilClassKind, + codegenLanguage: CodegenLanguage, + ): UtilClassKind? { if (existingUtilClass == null) { // If no util class exists, then we should create a new one with the given kind. return requiredUtilClassKind } val existingUtilClassVersion = existingUtilClass.utilClassVersionOrNull ?: return requiredUtilClassKind - val newUtilClassVersion = requiredUtilClassKind.utilClassVersion + val newUtilClassVersion = requiredUtilClassKind.utilClassVersion(codegenLanguage) val versionIsUpdated = existingUtilClassVersion != newUtilClassVersion - val existingUtilClassKind = existingUtilClass.utilClassKindOrNull ?: return requiredUtilClassKind + val existingUtilClassKind = existingUtilClass.utilClassKindOrNull(codegenLanguage) ?: return requiredUtilClassKind if (versionIsUpdated) { // If an existing util class is out of date, then we must overwrite it with a newer version. @@ -237,17 +335,18 @@ object CodeGenerationController { testDirectory: PsiDirectory, utilClassKind: UtilClassKind, existingUtilClass: PsiFile?, - model: GenerateTestsModel + model: GenerateTestsModel, + indicator: ProgressIndicator ) { val language = model.codegenLanguage val utUtilsFile = if (existingUtilClass == null) { // create a directory to put utils class into - val utilClassDirectory = createUtUtilSubdirectories(testDirectory) + val utilClassDirectory = createUtUtilSubdirectories(testDirectory, language) // create util class file and put it into utils directory createNewUtilClass(utilClassDirectory, language, utilClassKind, model) } else { - overwriteUtilClass(existingUtilClass, utilClassKind, model) + overwriteUtilClass(existingUtilClass, utilClassKind, model, indicator) } val utUtilsClass = runReadAction { @@ -255,7 +354,7 @@ object CodeGenerationController { (utUtilsFile as PsiClassOwner).classes.first() } - runWriteCommandAction(model.project, "UtBot util class reformatting", null, { + runWriteCommandAction(model.project, "UnitTestBot util class reformatting", null, { reformat(model, SmartPointerManager.getInstance(model.project).createSmartPsiElementPointer(utUtilsFile), utUtilsClass) }) @@ -271,7 +370,8 @@ object CodeGenerationController { private fun overwriteUtilClass( existingUtilClass: PsiFile, utilClassKind: UtilClassKind, - model: GenerateTestsModel + model: GenerateTestsModel, + indicator: ProgressIndicator ): PsiFile { val utilsClassDocument = runReadAction { PsiDocumentManager @@ -282,8 +382,8 @@ object CodeGenerationController { val utUtilsText = utilClassKind.getUtilClassText(model.codegenLanguage) - run(EDT_LATER) { - run(WRITE_ACTION) { + run(EDT_LATER, indicator, "Overwrite utility class") { + run(WRITE_ACTION, indicator, "Overwrite utility class") { unblockDocument(model.project, utilsClassDocument) executeCommand { utilsClassDocument.setText(utUtilsText.replace("jdk.internal.misc", "sun.misc")) @@ -312,7 +412,7 @@ object CodeGenerationController { val utUtilsText = utilClassKind.getUtilClassText(model.codegenLanguage) - val utUtilsFile = runReadAction { + var utUtilsFile = runReadAction { PsiFileFactory.getInstance(model.project) .createFileFromText( utUtilsName, @@ -323,7 +423,8 @@ object CodeGenerationController { // add UtUtils class file into the utils directory runWriteCommandAction(model.project) { - utilClassDirectory.add(utUtilsFile) + // The file actually added to subdirectory may be the copy of original file -- see [PsiElement.add] docs + utUtilsFile = utilClassDirectory.add(utUtilsFile) as PsiFile } return utUtilsFile @@ -344,6 +445,7 @@ object CodeGenerationController { .map { comment -> comment.text } .firstOrNull { text -> UtilClassKind.UTIL_CLASS_VERSION_COMMENT_PREFIX in text } ?.substringAfterLast(UtilClassKind.UTIL_CLASS_VERSION_COMMENT_PREFIX) + ?.substringBefore("\n") ?.trim() } @@ -351,8 +453,8 @@ object CodeGenerationController { * Util class must have a comment that specifies its kind. * This property obtains the kind specified by this comment if it exists. Otherwise, the property is `null`. */ - private val PsiFile.utilClassKindOrNull: UtilClassKind? - get() = runReadAction { + private fun PsiFile.utilClassKindOrNull(codegenLanguage: CodegenLanguage): UtilClassKind? + = runReadAction { val utilClass = (this as? PsiClassOwner) ?.classes ?.firstOrNull() @@ -360,8 +462,7 @@ object CodeGenerationController { utilClass.getChildrenOfType() .map { comment -> comment.text } - .mapNotNull { text -> UtilClassKind.utilClassKindByCommentOrNull(text) } - .firstOrNull() + .firstNotNullOfOrNull { text -> UtilClassKind.utilClassKindByCommentOrNull(text, codegenLanguage) } } /** @@ -377,14 +478,17 @@ object CodeGenerationController { } private val CodegenLanguage.utilClassFileName: String - get() = "$UT_UTILS_CLASS_NAME${this.extension}" + get() = "$UT_UTILS_INSTANCE_NAME${this.extension}" /** * @param testDirectory root test directory where we will put our generated tests. * @return directory for util class if it exists or null otherwise. */ - private fun getUtilDirectoryOrNull(testDirectory: PsiDirectory): PsiDirectory? { - val directoryNames = UtilClassKind.utilsPackages + private fun getUtilDirectoryOrNull( + testDirectory: PsiDirectory, + codegenLanguage: CodegenLanguage, + ): PsiDirectory? { + val directoryNames = UtilClassKind.utilsPackageNames(codegenLanguage) var currentDirectory = testDirectory for (name in directoryNames) { val subdirectory = runReadAction { currentDirectory.findSubdirectory(name) } ?: return null @@ -397,9 +501,12 @@ object CodeGenerationController { * @param testDirectory root test directory where we will put our generated tests. * @return file of util class if it exists or null otherwise. */ - private fun CodegenLanguage.getUtilClassOrNull(testDirectory: PsiDirectory): PsiFile? { + private fun CodegenLanguage.getUtilClassOrNull( + testDirectory: PsiDirectory, + codegenLanguage: CodegenLanguage, + ): PsiFile? { return runReadAction { - val utilDirectory = getUtilDirectoryOrNull(testDirectory) + val utilDirectory = getUtilDirectoryOrNull(testDirectory, codegenLanguage) utilDirectory?.findFile(this.utilClassFileName) } } @@ -421,17 +528,18 @@ object CodeGenerationController { } // return an util class from one of the test source roots or null if no util class was found - return testRoots - .mapNotNull { testRoot -> getUtilClassOrNull(testRoot) } - .firstOrNull() + return testRoots.firstNotNullOfOrNull { testRoot -> getUtilClassOrNull(testRoot, this) } } /** * Create all package directories for UtUtils class. * @return the innermost directory - utils from `org.utbot.runtime.utils` */ - private fun createUtUtilSubdirectories(baseTestDirectory: PsiDirectory): PsiDirectory { - val directoryNames = UtilClassKind.utilsPackages + private fun createUtUtilSubdirectories( + baseTestDirectory: PsiDirectory, + codegenLanguage: CodegenLanguage, + ): PsiDirectory { + val directoryNames = UtilClassKind.utilsPackageNames(codegenLanguage) var currentDirectory = baseTestDirectory runWriteCommandAction(baseTestDirectory.project) { for (name in directoryNames) { @@ -450,10 +558,11 @@ object CodeGenerationController { CodegenLanguage.KOTLIN -> KotlinFileType.INSTANCE } - private fun waitForCountDown(latch: CountDownLatch, timeout: Long = 5, timeUnit: TimeUnit = TimeUnit.SECONDS, action: Runnable) { + private fun waitForCountDown(latch: CountDownLatch, timeout: Long = 5, timeUnit: TimeUnit = TimeUnit.SECONDS, indicator : ProgressIndicator, action: Runnable) { try { if (!latch.await(timeout, timeUnit)) { - run(THREAD_POOL) { waitForCountDown(latch, timeout, timeUnit, action) } + run(THREAD_POOL, indicator, "Waiting for ${latch.count} sarif report(s) in a loop") { + waitForCountDown(latch, timeout, timeUnit, indicator, action) } } else { action.run() } @@ -502,8 +611,7 @@ object CodeGenerationController { } } - private fun createTestClass(srcClass: PsiClass, testDirectory: PsiDirectory, model: GenerateTestsModel): PsiClass? { - val testClassName = createTestClassName(srcClass) + private fun createTestClass(testClassName: String, testDirectory: PsiDirectory, model: GenerateTestsModel): PsiClass? { val aPackage = JavaDirectoryService.getInstance().getPackage(testDirectory) if (aPackage != null) { @@ -557,44 +665,56 @@ object CodeGenerationController { classUnderTest: ClassId, testClass: PsiClass, filePointer: SmartPsiElementPointer, + srcClassPathToSarifReport: MutableMap, model: GenerateTestsModel, reportsCountDown: CountDownLatch, - utilClassListener: UtilClassListener + utilClassListener: UtilClassListener, + indicator: ProgressIndicator ) { + assertIsWriteThread() val classMethods = srcClass.extractClassMethodsIncludingNested(false) - val paramNames = DumbService.getInstance(model.project) - .runReadActionInSmartMode(Computable { proc.findMethodParamNames(classUnderTest, classMethods) }) val testPackageName = testClass.packageName val editor = CodeInsightUtil.positionCursorAtLBrace(testClass.project, filePointer.containingFile, testClass) //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 (generatedTestsCode, utilClassKind) = proc.render( - testSetsId, - classUnderTest, - paramNames.toMutableMap(), - generateUtilClassFile = true, - model.testFramework, - model.mockFramework, - model.staticsMocking, - model.forceStaticMocking, - model.generateWarningsForStaticMocking, - model.codegenLanguage, - model.parametrizedTestSource, - model.runtimeExceptionTestsBehaviour, - model.hangingTestsTimeout, - enableTestsTimeout = true, - testPackageName - ) + run(THREAD_POOL, indicator, "Rendering test code") { + val (generatedTestsCode, utilClassKind) = try { + val paramNames = try { + proc.findMethodParamNames(classUnderTest, classMethods) + } catch (e: Exception) { + logger.warn(e) { "Cannot find method param names for ${classUnderTest.name}" } + reportsCountDown.countDown() + return@run + } + proc.render( + model, + testSetsId, + classUnderTest, + paramNames.toMutableMap(), + generateUtilClassFile = true, + enableTestsTimeout = true, + testPackageName, + ) + } catch (e: Exception) { + logger.warn(e) { "Cannot render test class ${testClass.name}" } + reportsCountDown.countDown() + return@run + } utilClassListener.onTestClassGenerated(utilClassKind) - 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.replace("jdk.internal.misc.Unsafe", "sun.misc.Unsafe")) + run(EDT_LATER, indicator, "Writing generation text to documents") { + run(WRITE_ACTION, indicator, "Writing generation text to documents") { + try { + 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.replace("jdk.internal.misc.Unsafe", "sun.misc.Unsafe")) + } + unblockDocument(testClass.project, editor.document) + } catch (e: Exception) { + logger.error(e) { "Cannot save document for ${testClass.name}" } + reportsCountDown.countDown() + return@run } - 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` @@ -602,30 +722,37 @@ object CodeGenerationController { // reformatting before creating reports due to // SarifReport requires the final version of the generated tests code - run(THREAD_POOL) { +// run(THREAD_POOL, indicator) { // IntentionHelper(model.project, editor, filePointer).applyIntentions() - run(EDT_LATER) { - runWriteCommandAction(filePointer.project, "UtBot tests reformatting", null, { - reformat(model, filePointer, testClassUpdated) - }) - unblockDocument(testClassUpdated.project, editor.document) - + run(EDT_LATER, indicator, "Tests reformatting") { + try { + runWriteCommandAction(filePointer.project, "UnitTestBot tests reformatting", null, { + reformat(model, filePointer, testClassUpdated) + }) + unblockDocument(testClassUpdated.project, editor.document) + } catch (e : Exception) { + logger.error(e) { "Cannot save Sarif report for ${testClassUpdated.name}" } + } // uploading formatted code val file = filePointer.containingFile + val srcClassPath = srcClass.containingFile.virtualFile.toNioPath() saveSarifReport( proc, testSetsId, testClassUpdated, classUnderTest, model, - file?.text?: generatedTestsCode + reportsCountDown, + file?.text ?: generatedTestsCode, + srcClassPathToSarifReport, + srcClassPath, + indicator ) - unblockDocument(testClassUpdated.project, editor.document) - reportsCountDown.countDown() + unblockDocument(testClassUpdated.project, editor.document) } - } +// } } } } @@ -635,7 +762,21 @@ object CodeGenerationController { val project = model.project val codeStyleManager = CodeStyleManager.getInstance(project) val file = smartPointer.containingFile?: return + val fileLength = runReadAction { + FileDocumentManager.getInstance().getDocument(file.virtualFile)?.textLength + ?: file.virtualFile.length.toInt() + } + if (fileLength > UtSettings.maxTestFileSize && file.name != model.codegenLanguage.utilClassFileName) { + CommonLoggingNotifier().notify( + "Size of ${file.virtualFile.presentableName} exceeds configured limit " + + "(${FileUtil.byteCountToDisplaySize(UtSettings.maxTestFileSize.toLong())}), reformatting was skipped.", + model.project, model.testModule, arrayOf(DumbAwareAction.create("Configure the Limit") { showSettingsEditor(model.project, "maxTestFileSize") } + )) + return + } + DumbService.getInstance(model.project).runWhenSmart { + OptimizeImportsProcessor(project, file).run() codeStyleManager.reformat(file) when (model.codegenLanguage) { CodegenLanguage.JAVA -> { @@ -643,7 +784,7 @@ object CodeGenerationController { val startOffset = range.startOffset val endOffset = range.endOffset val reformatRange = codeStyleManager.reformatRange(file, startOffset, endOffset, false) - JavaCodeStyleManager.getInstance(project).shortenClassReferences(reformatRange) + JavaCodeStyleManager.getInstance(project).shortenClassReferences(reformatRange, DO_NOT_ADD_IMPORTS) } CodegenLanguage.KOTLIN -> ShortenReferences.DEFAULT.process((testClass as KtUltraLightClass).kotlinOrigin.containingKtFile) } @@ -656,18 +797,30 @@ object CodeGenerationController { testClass: PsiClass, testClassId: ClassId, model: GenerateTestsModel, + reportsCountDown: CountDownLatch, generatedTestsCode: String, + srcClassPathToSarifReport: MutableMap, + srcClassPath: Path, + indicator: ProgressIndicator ) { val project = model.project try { // saving sarif report - val sourceFinding = SourceFindingStrategyIdea(testClass) - executeCommand(testClass.project, "Saving Sarif report") { - SarifReportIdea.createAndSave(proc, testSetsId, testClassId, model, generatedTestsCode, sourceFinding) - } + SarifReportIdea.createAndSave( + proc, + testSetsId, + testClassId, + model, + generatedTestsCode, + testClass, + reportsCountDown, + srcClassPathToSarifReport, + srcClassPath, + indicator + ) } catch (e: Exception) { - logger.error { e } + logger.error(e) { "error in saving sarif report"} showErrorDialogLater( project, message = "Cannot save Sarif report via generated tests: error occurred '${e.message}'", @@ -677,24 +830,26 @@ object CodeGenerationController { } - private fun eventLogMessage(project: Project): String? { - if (ToolWindowManager.getInstance(project).getToolWindow("Event Log") != null) - return """ + private fun eventLogMessage(project: Project): String? = runReadAction { + return@runReadAction if (ToolWindowManager.getInstance(project).getToolWindow("Event Log") != null) + """ See details in Event Log. """.trimIndent() - return null + else null } private fun showTestsReport(proc: EngineProcess, model: GenerateTestsModel) { val (notifyMessage, statistics, hasWarnings) = proc.generateTestsReport(model, eventLogMessage(model.project)) - if (hasWarnings) { - WarningTestsReportNotifier.notify(notifyMessage) - } else { - TestsReportNotifier.notify(notifyMessage) - } + runReadAction { + if (hasWarnings) { + WarningTestsReportNotifier.notify(notifyMessage) + } else { + TestsReportNotifier.notify(notifyMessage) + } - statistics?.let { DetailsTestsReportNotifier.notify(it) } + statistics?.let { DetailsTestsReportNotifier.notify(it) } + } } @Suppress("unused") @@ -716,14 +871,13 @@ object CodeGenerationController { } } is RegularImport -> { } + else -> { } } } } unblockDocument(testClass.project, editor.document) } - private fun createTestClassName(srcClass: PsiClass) = srcClass.name + "Test" - @Suppress("unused") // this method was used in the past, not used in the present but may be used in the future private fun insertMethods(testClass: PsiClass, superBody: String, editor: Editor) { diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/IntentionHelper.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/IntentionHelper.kt index 1cf3c20618..6cb224bd97 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/IntentionHelper.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/IntentionHelper.kt @@ -18,16 +18,16 @@ import mu.KotlinLogging import org.jetbrains.kotlin.idea.util.application.runReadAction private val logger = KotlinLogging.logger {} - +// The required part of IntelliJ API was changed to com.intellij.codeInsight.daemon.impl.MainPassesRunner that is available since 2022.1 only class IntentionHelper(val project: Project, private val editor: Editor, private val testFile: SmartPsiElementPointer) { fun applyIntentions() { val actions = DumbService.getInstance(project).runReadActionInSmartMode(Computable> { val daemonProgressIndicator = DaemonProgressIndicator() - Disposer.register(project, daemonProgressIndicator)//check it - val list = ProgressManager.getInstance().runProcess(Computable> { + Disposer.register(project) { daemonProgressIndicator.cancel() }//check it + val list = ProgressManager.getInstance().runProcess(Computable> inner@{ try { - val containingFile = testFile.containingFile ?: return@Computable emptyList() + val containingFile = testFile.containingFile ?: return@inner emptyList() DaemonCodeAnalyzerEx.getInstanceEx(project).runMainPasses( containingFile, editor.document, diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt index 030d529321..94fed03ce8 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt @@ -1,9 +1,6 @@ package org.utbot.intellij.plugin.generator -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.PathManager -import com.intellij.openapi.application.ReadAction -import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.* import com.intellij.openapi.compiler.CompilerPaths import com.intellij.openapi.components.service import com.intellij.openapi.module.Module @@ -12,49 +9,100 @@ import com.intellij.openapi.progress.Task import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.roots.OrderEnumerator +import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.Computable import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.project.stateStore import com.intellij.psi.PsiClass import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiUtil import com.intellij.refactoring.util.classMembers.MemberInfo +import com.intellij.task.ProjectTask import com.intellij.task.ProjectTaskManager +import com.intellij.task.impl.ModuleBuildTaskImpl +import com.intellij.task.impl.ModuleFilesBuildTaskImpl +import com.intellij.task.impl.ProjectTaskList +import com.intellij.util.PathsList import com.intellij.util.concurrency.AppExecutorUtil +import com.intellij.util.containers.ContainerUtil import com.intellij.util.containers.nullize -import com.intellij.util.io.exists -import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Arrays +import java.util.concurrent.TimeUnit +import java.util.stream.Collectors +import kotlin.io.path.exists +import kotlin.io.path.pathString import mu.KotlinLogging -import org.jetbrains.kotlin.idea.util.module +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.all +import org.jetbrains.idea.maven.project.MavenProjectsManager +import org.jetbrains.kotlin.idea.base.util.module +import org.utbot.framework.CancellationStrategyType.CANCEL_EVERYTHING +import org.utbot.framework.CancellationStrategyType.NONE +import org.utbot.framework.CancellationStrategyType.SAVE_PROCESSED_RESULTS import org.utbot.framework.UtSettings -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.JavaDocCommentStyle +import org.utbot.framework.codegen.domain.ProjectType.* +import org.utbot.framework.context.simple.SimpleApplicationContext +import org.utbot.framework.context.simple.SimpleMockerContext +import org.utbot.framework.context.spring.SpringApplicationContextImpl +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.SpringSettings.* +import org.utbot.framework.plugin.api.SpringConfiguration.* +import org.utbot.framework.plugin.api.SpringTestType.* +import org.utbot.framework.plugin.api.util.LockFile import org.utbot.framework.plugin.api.util.withStaticsSubstitutionRequired import org.utbot.framework.plugin.services.JdkInfoService import org.utbot.framework.plugin.services.WorkingDirService +import org.utbot.framework.process.SpringAnalyzerTask +import org.utbot.instrumentation.instrumentation.spring.SpringUtExecutionInstrumentation import org.utbot.intellij.plugin.generator.CodeGenerationController.generateTests import org.utbot.intellij.plugin.models.GenerateTestsModel -import org.utbot.intellij.plugin.models.packageName import org.utbot.intellij.plugin.process.EngineProcess import org.utbot.intellij.plugin.process.RdTestGenerationResult import org.utbot.intellij.plugin.settings.Settings import org.utbot.intellij.plugin.ui.GenerateTestsDialogWindow import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater -import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots import org.utbot.intellij.plugin.ui.utils.testModules -import org.utbot.intellij.plugin.util.* +import org.utbot.intellij.plugin.util.IntelliJApiHelper +import org.utbot.intellij.plugin.util.PsiClassHelper +import org.utbot.intellij.plugin.util.isAbstract +import org.utbot.intellij.plugin.util.binaryName +import org.utbot.intellij.plugin.util.PluginJdkInfoProvider +import org.utbot.intellij.plugin.util.PluginWorkingDirProvider +import org.utbot.intellij.plugin.util.assertIsNonDispatchThread +import org.utbot.intellij.plugin.util.extractClassMethodsIncludingNested import org.utbot.rd.terminateOnException -import java.io.File -import java.nio.file.Path -import java.nio.file.Paths -import java.util.concurrent.TimeUnit -import kotlin.io.path.pathString object UtTestsDialogProcessor { - private val logger = KotlinLogging.logger {} + enum class ProgressRange(val from : Double, val to: Double) { + INITIALIZATION(from = 0.01, to = 0.1), + SOLVING(from = 0.1, to = 0.9), + CODEGEN(from = 0.9, to = 0.95), + SARIF(from = 0.95, to = 1.0) + } + + fun updateIndicator(indicator: ProgressIndicator, range : ProgressRange, text: String? = null, fraction: Double? = null) { + invokeLater { + if (indicator.isCanceled) return@invokeLater + text?.let { indicator.text = it } + fraction?.let { + indicator.fraction = + indicator.fraction.coerceAtLeast(range.from + (range.to - range.from) * fraction.coerceIn(0.0, 1.0)) + } + logger.debug("Phase ${indicator.text} with progress ${String.format("%.2f",indicator.fraction)}") + } + } + + fun createDialogAndGenerateTests( project: Project, srcClasses: Set, @@ -76,10 +124,19 @@ object UtTestsDialogProcessor { val testModules = srcModule.testModules(project) JdkInfoService.jdkInfoProvider = PluginJdkInfoProvider(project) - // we want to start the child process in the same directory as the test runner + // we want to start the instrumented process in the same directory as the test runner WorkingDirService.workingDirProvider = PluginWorkingDirProvider(project) - if (project.isBuildWithGradle && testModules.flatMap { it.suitableTestSourceRoots() }.isEmpty()) { + val model = GenerateTestsModel( + project, + srcModule, + testModules, + srcClasses, + extractMembersFromSrcClasses, + focusedMethods, + project.service().generationTimeoutInMillis + ) + if (model.getAllTestSourceRoots().isEmpty() && project.isBuildWithGradle) { val errorMessage = """ No test source roots found in the project.
    Please, create or configure at least one test source root. @@ -88,48 +145,104 @@ object UtTestsDialogProcessor { return null } - return GenerateTestsDialogWindow( - GenerateTestsModel( - project, - srcModule, - testModules, - srcClasses, - extractMembersFromSrcClasses, - focusedMethods, - UtSettings.utBotGenerationTimeoutInMillis, - ) - ) + return GenerateTestsDialogWindow(model) + } + + private fun compile( + project: Project, + files: Array, + springConfigClass: PsiClass?, + ): Promise { + val buildTasks = runReadAction { + // For Maven project narrow compile scope may not work, see https://github.com/UnitTestBot/UTBotJava/issues/2021. + // For Spring project classes may contain `@ComponentScan` annotations, so we need to compile the whole module. + val isMavenProject = MavenProjectsManager.getInstance(project)?.hasProjects() ?: false + val isSpringProject = springConfigClass != null + val wholeModules = isMavenProject || isSpringProject + + ContainerUtil.map>, ProjectTask>( + Arrays.stream(files).collect(Collectors.groupingBy { file: VirtualFile -> + ProjectFileIndex.getInstance(project).getModuleForFile(file, false) + }).entries + ) { (key, value): Map.Entry?> -> + if (wholeModules) { + // This is a specific case, we have to compile the whole module + ModuleBuildTaskImpl(key!!, false) + } else { + // Compile only chosen classes and their dependencies before generation. + ModuleFilesBuildTaskImpl(key, false, value) + } + } + } + return ProjectTaskManager.getInstance(project).run(ProjectTaskList(buildTasks)) } private fun createTests(project: Project, model: GenerateTestsModel) { - val promise = ProjectTaskManager.getInstance(project).compile( - // Compile only chosen classes and their dependencies before generation. - *model.srcClasses.map { it.containingFile.virtualFile }.toTypedArray() - ) - promise.onSuccess { - if (it.hasErrors() || it.isAborted) + val springConfigClass = + when (val settings = model.springSettings) { + is AbsentSpringSettings -> null + is PresentSpringSettings -> + when (val config = settings.configuration) { + is JavaBasedConfiguration -> { + PsiClassHelper + .findClass(config.configBinaryName, project) + ?: error("Cannot find configuration class ${config.configBinaryName}.") + } + // TODO: for XML config we also need to compile module containing, + // since it may reference classes from that module + is XMLConfiguration -> null + } + } + + val filesToCompile = (model.srcClasses + listOfNotNull(springConfigClass)) + .map { it.containingFile.virtualFile } + .toTypedArray() + + compile(project, filesToCompile, springConfigClass).onSuccess { task -> + if (task.hasErrors() || task.isAborted) return@onSuccess (object : Task.Backgroundable(project, "Generate tests") { override fun run(indicator: ProgressIndicator) { - val ldef = LifetimeDefinition() - ldef.terminateOnException { lifetime -> - val startTime = System.currentTimeMillis() + assertIsNonDispatchThread() + if (!LockFile.lock()) { + return + } + + UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis = model.hangingTestsTimeout.timeoutMs + UtSettings.useCustomJavaDocTags = model.commentStyle == JavaDocCommentStyle.CUSTOM_JAVADOC_TAGS + UtSettings.summaryGenerationType = model.summariesGenerationType + + fun now() = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")) + + try { + logger.info { "Collecting information phase started at ${now()}" } val secondsTimeout = TimeUnit.MILLISECONDS.toSeconds(model.timeout) - val totalTimeout = model.timeout * model.srcClasses.size indicator.isIndeterminate = false - indicator.text = "Generate tests: read classes" - - val timerHandler = - AppExecutorUtil.getAppScheduledExecutorService().scheduleWithFixedDelay({ - indicator.fraction = - (System.currentTimeMillis() - startTime).toDouble() / totalTimeout - }, 0, 500, TimeUnit.MILLISECONDS) + updateIndicator(indicator, ProgressRange.INITIALIZATION, "Generate tests: starting engine", 0.0) + + // TODO sometimes preClasspathCollectionPromises get stuck, even though all + // needed dependencies get installed, we need to figure out why that happens + try { + model.preClasspathCollectionPromises + .all() + .blockingGet(10, TimeUnit.SECONDS) + } catch (e: java.util.concurrent.TimeoutException) { + logger.warn { "preClasspathCollectionPromises are stuck over 10 seconds, ignoring them" } + } val buildPaths = ReadAction - .nonBlocking { findPaths(model.srcClasses) } + .nonBlocking { + findPaths(listOf(findSrcModule(model.srcClasses)) + when (model.projectType) { + Spring -> listOfNotNull( + model.testModule, // needed so we can use `TestContextManager` from `spring-test` + springConfigClass?.let { it.module ?: error("Module for Spring configuration class not found") } + ) + else -> emptyList() + }) + } .executeSynchronously() ?: return @@ -139,144 +252,290 @@ object UtTestsDialogProcessor { val psi2KClass = mutableMapOf() var processedClasses = 0 val totalClasses = model.srcClasses.size - - val proc = EngineProcess(lifetime, project) - - proc.setupUtContext(buildDirs + classpathList) - proc.createTestGenerator( - buildDirs, - classpath, - pluginJarsPath.joinToString(separator = File.pathSeparator), - JdkInfoService.provide() - ) { - ApplicationManager.getApplication().runReadAction(Computable { - indicator.isCanceled - }) + val classNameToPath = runReadAction { + model.srcClasses.associate { psiClass -> + psiClass.binaryName to psiClass.containingFile.virtualFile.canonicalPath + } } - for (srcClass in model.srcClasses) { - val (methods, className) = ReadAction.nonBlocking, String?>> { - val canonicalName = srcClass.canonicalName - val classId = proc.obtainClassId(canonicalName) - psi2KClass[srcClass] = classId - - val srcMethods = if (model.extractMembersFromSrcClasses) { - val chosenMethods = model.selectedMembers.filter { it.member is PsiMethod } - val chosenNestedClasses = - model.selectedMembers.mapNotNull { it.member as? PsiClass } - chosenMethods + chosenNestedClasses.flatMap { - it.extractClassMethodsIncludingNested(false) - } - } else { - srcClass.extractClassMethodsIncludingNested(false) - } - DumbService.getInstance(project).runReadActionInSmartMode(Computable { - proc.findMethodsInClassMatchingSelected(classId, srcMethods) - }) to srcClass.name - }.executeSynchronously() - - if (methods.isEmpty()) { - logger.error { "No methods matching selected found in class $className." } - continue - } + val mockFrameworkInstalled = model.mockFramework.isInstalled + val staticMockingConfigured = model.staticsMocking.isConfigured - indicator.text = "Generate test cases for class $className" - if (totalClasses > 1) { - indicator.fraction = - indicator.fraction.coerceAtLeast(0.9 * processedClasses / totalClasses) + val process = EngineProcess.createBlocking(project, classNameToPath) + updateIndicator(indicator, ProgressRange.INITIALIZATION, fraction = 0.2) + + process.terminateOnException { _ -> + val classpathForClassLoader = buildDirs + classpathList + when (model.projectType) { + Spring -> listOf(SpringUtExecutionInstrumentation.springCommonsJar.path) + else -> emptyList() } - // set timeout for concrete execution and for generated tests - UtSettings.concreteExecutionTimeoutInChildProcess = - model.hangingTestsTimeout.timeoutMs + process.setupUtContext(classpathForClassLoader) + val simpleApplicationContext = SimpleApplicationContext( + SimpleMockerContext(mockFrameworkInstalled, staticMockingConfigured) + ) + val applicationContext = when (model.projectType) { + Spring -> { + val beanDefinitions = + when (val settings = model.springSettings) { + is AbsentSpringSettings -> emptyList() + is PresentSpringSettings -> { + process.perform( + SpringAnalyzerTask( + classpath = classpathForClassLoader, + settings = settings + ) + ) + } + } + + val clarifiedBeanDefinitions = + clarifyBeanDefinitionReturnTypes(beanDefinitions, project) + + SpringApplicationContextImpl.internalCreate( + simpleApplicationContext, + clarifiedBeanDefinitions, + model.springTestType, + model.springSettings, + ) + } + else -> simpleApplicationContext + } + updateIndicator(indicator, ProgressRange.INITIALIZATION, fraction = 0.25) + process.createTestGenerator( + buildDirs, + classpath, + pluginJarsPath.joinToString(separator = File.pathSeparator), + JdkInfoService.provide(), + applicationContext, + ) { + ApplicationManager.getApplication().runReadAction(Computable { + indicator.isCanceled + }) + } + updateIndicator(indicator, ProgressRange.INITIALIZATION, fraction = 1.0) + + for (srcClass in model.srcClasses) { + if (indicator.isCanceled) { + when (UtSettings.cancellationStrategyType) { + NONE -> {} + SAVE_PROCESSED_RESULTS, + CANCEL_EVERYTHING -> break + } + } - UtSettings.useCustomJavaDocTags = - model.commentStyle == JavaDocCommentStyle.CUSTOM_JAVADOC_TAGS + val (methods, classNameForLog) = process.executeWithTimeoutSuspended { + var binaryName = "" + var srcMethods: List = emptyList() + var srcNameForLog: String? = null + DumbService.getInstance(project) + .runReadActionInSmartMode(Computable { + binaryName = srcClass.binaryName + srcNameForLog = srcClass.name + srcMethods = if (model.extractMembersFromSrcClasses) { + val chosenMethods = + model.selectedMembers.filter { it.member is PsiMethod } + val chosenNestedClasses = + model.selectedMembers.mapNotNull { it.member as? PsiClass } + chosenMethods + chosenNestedClasses.flatMap { + it.extractClassMethodsIncludingNested(false) + } + } else { + srcClass.extractClassMethodsIncludingNested(false) + } + }) + val classId = process.obtainClassId(binaryName) + psi2KClass[srcClass] = classId + process.findMethodsInClassMatchingSelected( + classId, + srcMethods + ) to srcNameForLog + } - val searchDirectory = ReadAction - .nonBlocking { - project.basePath?.let { Paths.get(it) } - ?: Paths.get(srcClass.containingFile.virtualFile.parent.path) + if (methods.isEmpty()) { + logger.error { "No methods matching selected found in class $classNameForLog." } + continue } - .executeSynchronously() - - withStaticsSubstitutionRequired(true) { - val mockFrameworkInstalled = model.mockFramework?.isInstalled ?: true - - val rdGenerateResult = proc.generate( - mockFrameworkInstalled, - model.staticsMocking.isConfigured, - model.conflictTriggers, - methods, - model.mockStrategy, - model.chosenClassesToMockAlways, - model.timeout, - model.timeout, - true, - UtSettings.useFuzzing, - project.service().fuzzingValue, - searchDirectory.pathString + + logger.info { "Collecting information phase finished at ${now()}" } + + updateIndicator( + indicator, + ProgressRange.SOLVING, + "Generate test cases for class $classNameForLog", + processedClasses.toDouble() / totalClasses ) - if (rdGenerateResult.notEmptyCases == 0) { - if (model.srcClasses.size > 1) { - logger.error { "Failed to generate any tests cases for class $className" } - } else { - showErrorDialogLater( - model.project, - errorMessage(className, secondsTimeout), - title = "Failed to generate unit tests for class $className" + val searchDirectory = ReadAction + .nonBlocking { + project.basePath?.let { Paths.get(it) } + ?: Paths.get(srcClass.containingFile.virtualFile.parent.path) + } + .executeSynchronously() + + val taintConfigPath = getTaintConfigPath(project) + + withStaticsSubstitutionRequired(true) { + val startTime = System.currentTimeMillis() + val timerHandler = + AppExecutorUtil.getAppScheduledExecutorService().scheduleWithFixedDelay({ + val innerTimeoutRatio = + ((System.currentTimeMillis() - startTime).toDouble() / model.timeout) + .coerceIn(0.0, 1.0) + updateIndicator( + indicator, + ProgressRange.SOLVING, + "Generate test cases for class $classNameForLog", + (processedClasses.toDouble() + innerTimeoutRatio) / totalClasses + ) + }, 0, 500, TimeUnit.MILLISECONDS) + try { + val useEngine = when (model.projectType) { + Spring -> when (model.springTestType) { + UNIT_TEST -> true + INTEGRATION_TEST -> false + } + else -> true + } + val useFuzzing = when (model.projectType) { + Spring -> when (model.springTestType) { + UNIT_TEST -> UtSettings.useFuzzing + INTEGRATION_TEST -> true + } + + else -> UtSettings.useFuzzing + } + val rdGenerateResult = process.generate( + conflictTriggers = model.conflictTriggers, + methods = methods, + mockStrategyApi = model.mockStrategy, + chosenClassesToMockAlways = model.chosenClassesToMockAlways, + timeout = model.timeout, + generationTimeout = model.timeout, + isSymbolicEngineEnabled = useEngine, + isFuzzingEnabled = useFuzzing, + fuzzingValue = project.service().fuzzingValue, + searchDirectory = searchDirectory.pathString, + taintConfigPath = taintConfigPath?.pathString ) + + if (rdGenerateResult.notEmptyCases == 0) { + if (!indicator.isCanceled) { + if (model.srcClasses.size > 1) { + logger.error { "Failed to generate any tests cases for class $classNameForLog" } + } else { + showErrorDialogLater( + model.project, + errorMessage(classNameForLog, secondsTimeout), + title = "Failed to generate unit tests for class $classNameForLog" + ) + } + } else { + logger.warn { "Generation was cancelled for class $classNameForLog" } + } + } else { + testSetsByClass[srcClass] = rdGenerateResult + } + } finally { + timerHandler.cancel(true) } - } else { - testSetsByClass[srcClass] = rdGenerateResult } + processedClasses++ + } - timerHandler.cancel(true) + if (processedClasses == 0) { + invokeLater { + Messages.showInfoMessage( + model.project, + "No methods for test generation were found among selected items", + "No Methods Found" + ) + } + return } - processedClasses++ - } + updateIndicator(indicator, ProgressRange.CODEGEN, "Generate code for tests", 0.0) + // Commented out to generate tests for collected executions even if action was canceled. + // indicator.checkCanceled() - if (processedClasses == 0) { invokeLater { - Messages.showInfoMessage( - model.project, - "No methods for test generation were found among selected items", - "No methods found" - ) + generateTests(model, testSetsByClass, psi2KClass, process, indicator) + logger.info { "Generation complete" } } - return - } - - indicator.fraction = indicator.fraction.coerceAtLeast(0.9) - indicator.text = "Generate code for tests" - // Commented out to generate tests for collected executions even if action was canceled. - // indicator.checkCanceled() - - invokeLater { - generateTests(model, testSetsByClass, psi2KClass, proc) } + } finally { + LockFile.unlock() } } }).queue() } } - private val PsiClass.canonicalName: String - get() { - return if (packageName.isEmpty()) { - qualifiedName?.replace(".", "$") ?: "" - } else { - val name = qualifiedName - ?.substringAfter("$packageName.") - ?.replace(".", "$") - ?: error("Unable to get canonical name for $this") - "$packageName.$name" - } + /** + * Returns "{project}/.idea/utbot-taint-config.yaml" or null if it does not exists + */ + private fun getTaintConfigPath(project: Project): Path? { + val path = project.stateStore.directoryStorePath?.resolve("utbot-taint-config.yaml") + return if (path != null && path.toFile().exists()) path else null + } + + private fun clarifyBeanDefinitionReturnTypes(beanDefinitions: List, project: Project) = + beanDefinitions.map { bean -> + // Here we extract a real return type. + // E.g. for a method + // public Toy getToy() { return new SpecToy() } + // we want not Toy but SpecToy type. + // If there are more than one return type, we take type from a signature. + // We process beans with present additional data only. + val beanType = runReadAction { + val additionalData = bean.additionalData ?: return@runReadAction null + + val configPsiClass = + PsiClassHelper + .findClass(additionalData.configClassName, project) + ?: return@runReadAction null + .also { + logger.warn("Cannot find configuration class ${additionalData.configClassName}.") + } + + val beanPsiMethod = + configPsiClass + .findMethodsByName(bean.beanName) + .mapNotNull { jvmMethod -> + (jvmMethod as PsiMethod) + .takeIf { method -> + !method.isAbstract && method.body?.isEmpty == false && + method.parameterList.parameters.map { it.type.canonicalText } == additionalData.parameterTypes + } + } + // Here we try to take a single element + // because we expect no or one method matching previous conditions only. + // If there were two or more similar methods in one class, it would be a weird case. + .singleOrNull() + ?: return@runReadAction null + .also { + logger.warn( + "Several similar methods named ${bean.beanName} " + + "were found in ${additionalData.configClassName} configuration class." + ) + } + + val beanTypes = + PsiUtil + .findReturnStatements(beanPsiMethod) + .mapNotNullTo(mutableSetOf()) { stmt -> stmt.returnValue?.type?.canonicalText } + + beanTypes.singleOrNull() ?: bean.beanTypeName + } ?: return@map bean + + BeanDefinitionData( + beanName = bean.beanName, + beanTypeName = beanType, + additionalData = bean.additionalData + ) } private fun errorMessage(className: String?, timeout: Long) = buildString { - appendLine("UtBot failed to generate any test cases for class $className.") + appendLine("UnitTestBot failed to generate any test cases for class $className.") appendLine() appendLine("Try to alter test generation configuration, e.g. enable mocking and static mocking.") appendLine("Alternatively, you could try to increase current timeout $timeout sec for generating tests in generation dialog.") @@ -291,15 +550,18 @@ object UtTestsDialogProcessor { } } - private fun findPaths(srcClasses: Set): BuildPaths? { - val srcModule = findSrcModule(srcClasses) - - val buildDirs = CompilerPaths.getOutputPaths(arrayOf(srcModule)) + private fun findPaths(modules: List): BuildPaths? { + val buildDirs = CompilerPaths.getOutputPaths(modules.distinct().toTypedArray()) .toList() .filter { Paths.get(it).exists() } .nullize() ?: return null - val pathsList = OrderEnumerator.orderEntries(srcModule).recursively().pathsList + val pathsList = PathsList() + + modules + .distinct() + .map { module -> OrderEnumerator.orderEntries(module).recursively().pathsList } + .forEach { pathsList.addAll(it.pathList) } val (classpath, classpathList) = if (IntelliJApiHelper.isAndroidStudio()) { // Filter out manifests from classpath. @@ -314,7 +576,7 @@ object UtTestsDialogProcessor { val classpathList = pathsList.pathList Pair(classpath, classpathList) } - val pluginJarsPath = Paths.get(PathManager.getPluginsPath(), "utbot-intellij", "lib").toFile().listFiles() + val pluginJarsPath = Paths.get(PathManager.getPluginsPath(), "utbot-intellij-main", "lib").toFile().listFiles() ?: error("Can't find plugin folder.") return BuildPaths(buildDirs, classpath, classpathList, pluginJarsPath.map { it.path }) } @@ -324,6 +586,6 @@ object UtTestsDialogProcessor { val classpath: String, val classpathList: List, val pluginJarsPath: List - // ^ TODO: Now we collect ALL dependent libs and pass them to the child process. Most of them are redundant. + // ^ TODO: Now we collect ALL dependent libs and pass them to the instrumented and spring analyzer processes. Most of them are redundant. ) -} +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/AnalyzeStackTraceFix.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/AnalyzeStackTraceFix.kt new file mode 100644 index 0000000000..434f24399c --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/AnalyzeStackTraceFix.kt @@ -0,0 +1,49 @@ +package org.utbot.intellij.plugin.inspection + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Iconable +import com.intellij.unscramble.AnalyzeStacktraceUtil +import javax.swing.Icon + +/** + * Button that launches the built-in "Analyze Stack Trace" action. Displayed as a quick fix. + * + * @param exceptionMessage short description of the detected exception. + * @param stackTraceLines list of strings of the form "className.methodName(fileName:lineNumber)". + */ +class AnalyzeStackTraceFix( + private val exceptionMessage: String, + private val stackTraceLines: List +) : LocalQuickFix, Iconable { + + /** + * Without `invokeLater` the [com.intellij.execution.impl.ConsoleViewImpl.myPredefinedFilters] will not be filled. + * + * See [com.intellij.execution.impl.ConsoleViewImpl.createCompositeFilter] for more details. + */ + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val stackTraceContent = stackTraceLines.joinToString("\n") { "at $it" } + ApplicationManager.getApplication().invokeLater { + AnalyzeStacktraceUtil.addConsole( + /* project = */ project, + /* consoleFactory = */ null, + /* tabTitle = */ "StackTrace", + /* text = */ "$exceptionMessage\n\n$stackTraceContent", + /* icon = */ AllIcons.Actions.Lightning + ) + } + } + + /** + * This text is displayed on the quick fix button. + */ + override fun getName() = "Analyze stack trace" + + override fun getFamilyName() = name + + override fun getIcon(flags: Int): Icon = AllIcons.Actions.Lightning +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionContext.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionContext.kt new file mode 100644 index 0000000000..076c2612e5 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionContext.kt @@ -0,0 +1,71 @@ +@file:Suppress("UnstableApiUsage") + +package org.utbot.intellij.plugin.inspection + +import com.intellij.codeInspection.ex.* +import com.intellij.codeInspection.ui.InspectionToolPresentation +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.NotNullLazyValue +import com.intellij.ui.content.ContentManager +import org.utbot.sarif.Sarif +import java.nio.file.Path +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +/** + * Overrides some methods of [GlobalInspectionContextImpl] to satisfy the logic of [UnitTestBotInspectionTool]. + */ +class UnitTestBotInspectionContext( + project: Project, + contentManager: NotNullLazyValue, + private val srcClassPathToSarifReport: MutableMap +) : GlobalInspectionContextImpl(project, contentManager) { + + /** + * See [GlobalInspectionContextImpl.myPresentationMap] for more details. + */ + private val myPresentationMap: ConcurrentMap, InspectionToolPresentation> = + ConcurrentHashMap() + + private val globalInspectionToolWrapper by lazy { + val utbotInspectionTool = UnitTestBotInspectionTool.getInstance(srcClassPathToSarifReport) + GlobalInspectionToolWrapper(utbotInspectionTool).also { + it.initialize(/* context = */ this) + } + } + + /** + * Returns [InspectionProfileImpl] with only one inspection tool - [UnitTestBotInspectionTool]. + */ + override fun getCurrentProfile(): InspectionProfileImpl { + val supplier = InspectionToolsSupplier.Simple(listOf(globalInspectionToolWrapper)) + return InspectionProfileImpl("UnitTestBot", supplier, BASE_PROFILE) + } + + override fun close(noSuspiciousCodeFound: Boolean) { + try { + if (!noSuspiciousCodeFound && (view == null || view.isRerun)) { + return + } + myPresentationMap.clear() + super.close(noSuspiciousCodeFound) + } catch (_: Throwable) { + // already closed + } + } + + override fun cleanup() { + myPresentationMap.clear() + super.cleanup() + } + + /** + * Overriding is needed to provide [UnitTestBotInspectionToolPresentation] + * instead of the standard implementation of the [InspectionToolPresentation]. + */ + override fun getPresentation(toolWrapper: InspectionToolWrapper<*, *>): InspectionToolPresentation { + return myPresentationMap.computeIfAbsent(toolWrapper) { + UnitTestBotInspectionToolPresentation(globalInspectionToolWrapper, context = this) + } + } +} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionManager.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionManager.kt new file mode 100644 index 0000000000..c942e014d9 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionManager.kt @@ -0,0 +1,41 @@ +@file:Suppress("UnstableApiUsage") + +package org.utbot.intellij.plugin.inspection + +import com.intellij.codeInspection.ex.GlobalInspectionContextImpl +import com.intellij.codeInspection.ex.InspectionManagerEx +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.NotNullLazyValue +import com.intellij.ui.content.ContentManager +import org.utbot.sarif.Sarif +import java.nio.file.Path + +/** + * Overrides some methods of [InspectionManagerEx] to satisfy the logic of [UnitTestBotInspectionTool]. + */ +class UnitTestBotInspectionManager(project: Project) : InspectionManagerEx(project) { + + private var srcClassPathToSarifReport: MutableMap = mutableMapOf() + + companion object { + fun getInstance(project: Project, srcClassPathToSarifReport: MutableMap) = + UnitTestBotInspectionManager(project).also { + it.srcClassPathToSarifReport = srcClassPathToSarifReport + } + } + + /** + * See [InspectionManagerEx.myContentManager] for more details. + */ + private val myContentManager: NotNullLazyValue by lazy { + NotNullLazyValue.createValue { + getProblemsViewContentManager(project) + } + } + + /** + * Overriding is needed to provide [UnitTestBotInspectionContext] instead of [GlobalInspectionContextImpl]. + */ + override fun createNewGlobalContext(): GlobalInspectionContextImpl = + UnitTestBotInspectionContext(project, myContentManager, srcClassPathToSarifReport) +} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionTool.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionTool.kt new file mode 100644 index 0000000000..ad1e36fb73 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionTool.kt @@ -0,0 +1,134 @@ +package org.utbot.intellij.plugin.inspection + +import com.intellij.codeInspection.* +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFile +import org.jetbrains.kotlin.idea.core.util.toPsiFile +import com.intellij.psi.search.GlobalSearchScope +import org.utbot.sarif.* +import java.nio.file.Path + +/** + * Global inspection tool that displays detected errors from the SARIF report. + */ +class UnitTestBotInspectionTool : GlobalSimpleInspectionTool() { + + /** + * Map from the path to the class under test to [Sarif] for it. + */ + private var srcClassPathToSarifReport: MutableMap = mutableMapOf() + + companion object { + fun getInstance(srcClassPathToSarifReport: MutableMap) = + UnitTestBotInspectionTool().also { + it.srcClassPathToSarifReport = srcClassPathToSarifReport + } + } + + override fun getShortName() = "UnitTestBotInspectionTool" + + override fun getDisplayName() = "Unchecked exceptions" + + override fun getGroupDisplayName() = "Errors detected by UnitTestBot" + + /** + * Appends all the errors from the SARIF report for [srcPsiFile] to the [problemDescriptionsProcessor]. + */ + override fun checkFile( + srcPsiFile: PsiFile, + manager: InspectionManager, + problemsHolder: ProblemsHolder, + globalContext: GlobalInspectionContext, + problemDescriptionsProcessor: ProblemDescriptionsProcessor + ) { + val sarifReport = srcClassPathToSarifReport[srcPsiFile.virtualFile.toNioPath()] + ?: return // no results for this file + + for (sarifResult in sarifReport.getAllResults()) { + val srcFilePhysicalLocation = sarifResult.locations + .filterIsInstance(SarifPhysicalLocationWrapper::class.java) + .firstOrNull()?.physicalLocation ?: continue + val srcFileLogicalLocation = sarifResult.locations + .filterIsInstance(SarifLogicalLocationsWrapper::class.java) + .firstOrNull() + ?.logicalLocations?.firstOrNull() + + // srcPsiFile may != errorPsiFile (if srcFileLogicalLocation != null) + val errorPsiFile = srcFileLogicalLocation?.fullyQualifiedName?.let { errorClassFqn -> + val psiFacade = JavaPsiFacade.getInstance(srcPsiFile.project) + val psiClass = psiFacade.findClass(errorClassFqn, GlobalSearchScope.allScope(srcPsiFile.project)) + val psiFile = psiClass?.containingFile ?: return@let null + + // We can't just return psiFile because it may be non-physical + if (psiFile.isPhysical) { + psiFile + } else { + psiFile.virtualFile.toPsiFile(srcPsiFile.project) + } + } ?: srcPsiFile + val errorRegion = srcFilePhysicalLocation.region + val errorTextRange = getTextRange(problemsHolder.project, errorPsiFile, errorRegion) + + // see `org.utbot.sarif.SarifReport.processUncheckedException` for the message template + val (exceptionMessage, testCaseMessage) = + sarifResult.message.text.split('\n').take(2) + val sarifResultMessage = "$exceptionMessage $testCaseMessage" + + val testFileLocation = sarifResult.relatedLocations.firstOrNull()?.physicalLocation + val viewGeneratedTestFix = testFileLocation?.let { + ViewGeneratedTestFix( + testFileRelativePath = it.artifactLocation.uri, + lineNumber = it.region.startLine, + columnNumber = it.region.startColumn ?: 1 + ) + } + + val stackTraceLines = sarifResult.extractStackTraceLines() + val analyzeStackTraceFix = AnalyzeStackTraceFix(exceptionMessage, stackTraceLines) + + val problemDescriptor = problemsHolder.manager.createProblemDescriptor( + errorPsiFile, + errorTextRange, + sarifResultMessage, + ProblemHighlightType.ERROR, + /* onTheFly = */ true, + viewGeneratedTestFix as LocalQuickFix, + analyzeStackTraceFix + ) + problemDescriptionsProcessor.addProblemElement( + globalContext.refManager.getReference(errorPsiFile), + problemDescriptor + ) + } + } + + // internal + + /** + * Converts [SarifRegion] to the [TextRange] of the given [file]. + */ + private fun getTextRange(project: Project, file: PsiFile, region: SarifRegion): TextRange { + val documentManager = PsiDocumentManager.getInstance(project) + val document = documentManager.getDocument(file.containingFile) + ?: return TextRange.EMPTY_RANGE + + val lineNumber = region.startLine - 1 // to 0-based + val columnNumber = region.startColumn ?: 1 + + val lineStartOffset = document.getLineStartOffset(lineNumber) + columnNumber - 1 + val lineEndOffset = document.getLineEndOffset(lineNumber) + return TextRange(lineStartOffset, lineEndOffset) + } + + private fun SarifResult.extractStackTraceLines(): List = + this.codeFlows.flatMap { sarifCodeFlow -> + sarifCodeFlow.threadFlows.flatMap { sarifThreadFlow -> + sarifThreadFlow.locations.map { sarifFlowLocationWrapper -> + sarifFlowLocationWrapper.location.message.text + } + } + }.reversed() +} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionToolPresentation.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionToolPresentation.kt new file mode 100644 index 0000000000..f777c2cfb6 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/UnitTestBotInspectionToolPresentation.kt @@ -0,0 +1,26 @@ +@file:Suppress("UnstableApiUsage") + +package org.utbot.intellij.plugin.inspection + +import com.intellij.codeInspection.CommonProblemDescriptor +import com.intellij.codeInspection.ex.InspectionToolWrapper +import com.intellij.codeInspection.ui.DefaultInspectionToolPresentation + +/** + * Overrides [resolveProblem] to avoid suppressing quick fix buttons. + */ +class UnitTestBotInspectionToolPresentation( + toolWrapper: InspectionToolWrapper<*, *>, + context: UnitTestBotInspectionContext +) : DefaultInspectionToolPresentation(toolWrapper, context) { + + /** + * This method is called when the user clicks on the quick fix button. + * In the case of [UnitTestBotInspectionTool] we do not want to remove the button after applying the fix. + * + * See [DefaultInspectionToolPresentation.resolveProblem] for more details. + */ + override fun resolveProblem(descriptor: CommonProblemDescriptor) { + // nothing + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/ViewGeneratedTestFix.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/ViewGeneratedTestFix.kt new file mode 100644 index 0000000000..0261677e7d --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/inspection/ViewGeneratedTestFix.kt @@ -0,0 +1,51 @@ +package org.utbot.intellij.plugin.inspection + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Iconable +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.util.ui.EmptyIcon +import javax.swing.Icon +import org.utbot.common.PathUtil.toPath + +/** + * Button with a link to the [testFileRelativePath]. Displayed as a quick fix. + * + * @param testFileRelativePath path to the generated test file. + * Should be relative to the project root + * @param lineNumber one-based line number + * @param columnNumber one-based column number + */ +class ViewGeneratedTestFix( + val testFileRelativePath: String, + val lineNumber: Int, + val columnNumber: Int +) : LocalQuickFix, Iconable { + + /** + * Navigates the user to the [lineNumber] line of the [testFileRelativePath] file. + */ + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val testFileAbsolutePath = project.basePath?.toPath()?.resolve(testFileRelativePath) ?: return + val virtualFile = VfsUtil.findFile(testFileAbsolutePath, /* refreshIfNeeded = */ true) ?: return + val editor = FileEditorManager.getInstance(project) + editor.openFile(virtualFile, /* focusEditor = */ true) + val caretModel = editor.selectedTextEditor?.caretModel ?: return + val zeroBasedPosition = LogicalPosition(lineNumber - 1, columnNumber - 1) + caretModel.moveToLogicalPosition(zeroBasedPosition) + val selectionModel = editor.selectedTextEditor?.selectionModel ?: return + selectionModel.selectLineAtCaret() + } + + /** + * This text is displayed on the quick fix button. + */ + override fun getName() = "View generated test" + + override fun getFamilyName() = name + + override fun getIcon(flags: Int): Icon = EmptyIcon.ICON_0 +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtCustomJavaDocTagProvider.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtCustomJavaDocTagProvider.kt index c82e37b027..cd3dd4566c 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtCustomJavaDocTagProvider.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtCustomJavaDocTagProvider.kt @@ -6,8 +6,8 @@ import com.intellij.psi.PsiReference import com.intellij.psi.javadoc.CustomJavadocTagProvider import com.intellij.psi.javadoc.JavadocTagInfo import com.intellij.psi.javadoc.PsiDocTagValue -import org.utbot.summary.comment.CustomJavaDocTag -import org.utbot.summary.comment.CustomJavaDocTagProvider +import org.utbot.summary.comment.customtags.symbolic.CustomJavaDocTag +import org.utbot.summary.comment.customtags.symbolic.CustomJavaDocTagProvider /** * Provides plugin's custom JavaDoc tags to make test summaries structured. diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt new file mode 100644 index 0000000000..b755acd5f9 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt @@ -0,0 +1,250 @@ +package org.utbot.intellij.plugin.language + +import com.intellij.openapi.actionSystem.ActionPlaces +import org.utbot.intellij.plugin.generator.UtTestsDialogProcessor +import org.utbot.intellij.plugin.ui.utils.PsiElementHandler +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.util.classMembers.MemberInfo +import org.jetbrains.kotlin.idea.core.getPackage +import org.jetbrains.kotlin.idea.core.util.toPsiDirectory +import org.jetbrains.kotlin.idea.core.util.toPsiFile +import org.jetbrains.kotlin.idea.util.module +import org.utbot.intellij.plugin.util.extractFirstLevelMembers +import org.utbot.intellij.plugin.util.isVisible +import java.util.* +import org.jetbrains.kotlin.j2k.getContainingClass +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.utils.addIfNotNull +import org.utbot.framework.plugin.api.util.LockFile +import org.utbot.intellij.plugin.models.packageName +import org.utbot.intellij.plugin.ui.InvalidClassNotifier +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import org.utbot.intellij.plugin.util.findSdkVersionOrNull + +@Suppress("unused") // is used in org.utbot.intellij.plugin.language.agnostic.LanguageAssistant via reflection +object JvmLanguageAssistant : LanguageAssistant() { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + + val (srcClasses, focusedMethods, extractMembersFromSrcClasses) = getPsiTargets(e) ?: return + val validatedSrcClasses = validateSrcClasses(srcClasses) ?: return + + UtTestsDialogProcessor.createDialogAndGenerateTests(project, validatedSrcClasses, extractMembersFromSrcClasses, focusedMethods) + } + + override fun update(e: AnActionEvent) { + if (LockFile.isLocked()) { + e.presentation.isEnabled = false + return + } + if (e.place == ActionPlaces.POPUP) { + e.presentation.text = "Tests with UnitTestBot..." + } + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): Triple, Set, Boolean>? { + val project = e.project ?: return null + val editor = e.getData(CommonDataKeys.EDITOR) + if (editor != null) { + //The action is being called from editor + val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null + val element = findPsiElement(file, editor) ?: return null + + val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) + + if (psiElementHandler.isCreateTestActionAvailable(element)) { + val srcClass = psiElementHandler.identifiedContainingClass(element) ?: return null + val srcSourceRoot = srcClass.getSourceRoot() ?: return null + val srcMembers = srcClass.extractFirstLevelMembers(false) + val focusedMethod = focusedMethodOrNull(element, srcMembers, psiElementHandler) + + val module = ModuleUtil.findModuleForFile(srcSourceRoot, project) ?: return null + val matchingRoot = ModuleRootManager.getInstance(module).contentEntries + .flatMap { entry -> entry.sourceFolders.toList() } + .firstOrNull { folder -> folder.file == srcSourceRoot } + if (srcMembers.isEmpty() || matchingRoot == null || matchingRoot.rootType.isForTests) { + return null + } + + return Triple(setOf(srcClass), if (focusedMethod != null) setOf(focusedMethod) else emptySet(), true) + } + } else { + // The action is being called from 'Project' tool window + val srcClasses = mutableSetOf() + val selectedMethods = mutableSetOf() + var extractMembersFromSrcClasses = false + val element = e.getData(CommonDataKeys.PSI_ELEMENT) + if (element is PsiFileSystemItem) { + e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { + srcClasses += getAllClasses(project, it) + } + } else if (element is PsiElement){ + val file = element.containingFile ?: return null + val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) + + if (psiElementHandler.isCreateTestActionAvailable(element)) { + psiElementHandler.identifiedContainingClass(element)?.let { + srcClasses += setOf(it) + extractMembersFromSrcClasses = true + val memberInfoList = runReadAction> { + it.extractFirstLevelMembers(false) + } + if (memberInfoList.isEmpty()) + return null + } + + if (element is PsiMethod) { + selectedMethods.add(MemberInfo(element)) + } + } + } else { + val someSelection = e.getData(PlatformDataKeys.PSI_ELEMENT_ARRAY)?: return null + someSelection.forEach { + when(it) { + is PsiFileSystemItem -> srcClasses += getAllClasses(project, arrayOf(it.virtualFile)) + is PsiClass -> srcClasses.add(it) + is KtClass -> srcClasses += getClassesFromFile(it.containingKtFile) + is PsiElement -> { + srcClasses.addIfNotNull(it.getContainingClass()) + if (it is PsiMethod) { + selectedMethods.add(MemberInfo(it)) + extractMembersFromSrcClasses = true + } + } + } + } + } + + if (srcClasses.size > 1) { + extractMembersFromSrcClasses = false + } + var commonSourceRoot = null as VirtualFile? + for (srcClass in srcClasses) { + if (commonSourceRoot == null) { + commonSourceRoot = srcClass.getSourceRoot()?: return null + } else if (commonSourceRoot != srcClass.getSourceRoot()) return null + } + if (commonSourceRoot == null) return null + val module = ModuleUtil.findModuleForFile(commonSourceRoot, project)?: return null + + if (!Arrays.stream(ModuleRootManager.getInstance(module).contentEntries) + .flatMap { entry -> Arrays.stream(entry.sourceFolders) } + .filter { folder -> !folder.rootType.isForTests && folder.file == commonSourceRoot} + .findAny().isPresent ) return null + + return Triple(srcClasses.toSet(), selectedMethods.toSet(), extractMembersFromSrcClasses) + } + return null + } + + private fun PsiElementHandler.identifiedContainingClass(element: PsiElement): PsiClass? { + val clazz = containingClass(element) + return if (clazz is PsiAnonymousClass) PsiTreeUtil.getParentOfType(clazz, PsiClass::class.java) else clazz + } + + /** + * Validates that a set of source classes matches some requirements from [isInvalid]. + * If no one of them matches, shows a warning about the first mismatch reason. + */ + private fun validateSrcClasses(srcClasses: Set): Set? { + val filteredClasses = srcClasses + .filterNot { it.isInvalid(withWarnings = false) } + .toSet() + + if (filteredClasses.isEmpty()) { + srcClasses.first().isInvalid(withWarnings = true) + return null + } + + return filteredClasses + } + + private fun PsiClass.isInvalid(withWarnings: Boolean): Boolean { + if (this.module?.let { findSdkVersionOrNull(it) } == null) { + if (withWarnings) InvalidClassNotifier.notify("class out of module or with undefined SDK") + return true + } + + val isInvisible = !this.isVisible + if (isInvisible) { + if (withWarnings) InvalidClassNotifier.notify("private or protected class ${this.name}") + return true + } + + val packageIsIncorrect = this.packageName.split(".").firstOrNull() == "java" + if (packageIsIncorrect) { + if (withWarnings) InvalidClassNotifier.notify("class ${this.name} located in java.* package") + return true + } + + return false + } + + private fun PsiElement?.getSourceRoot() : VirtualFile? { + val project = this?.project?: return null + val virtualFile = this.containingFile?.originalFile?.virtualFile?: return null + return ProjectFileIndex.getInstance(project).getSourceRootForFile(virtualFile) + } + + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + + return element + } + + private fun focusedMethodOrNull(element: PsiElement, methods: List, psiElementHandler: PsiElementHandler): MemberInfo? { + // getParentOfType might return element which does not correspond to the standard Psi hierarchy. + // Thus, make transition to the Psi if it is required. + val currentMethod = PsiTreeUtil.getParentOfType(element, psiElementHandler.methodClass) + ?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) } + // For anonymous class, we cannot select the nearest parent method directly. + // So, we have to suggest the nearest "outer" method from the named class. + val topmostCurrentMethod = PsiTreeUtil.getTopmostParentOfType(element, psiElementHandler.methodClass) + ?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) } + + return methods.singleOrNull { it.member == currentMethod } + ?: methods.singleOrNull { it.member == topmostCurrentMethod } + } + + private fun getAllClasses(directory: PsiDirectory): Set { + val allClasses = directory.files.flatMap { getClassesFromFile(it) }.toMutableSet() + for (subDir in directory.subdirectories) allClasses += getAllClasses(subDir) + return allClasses + } + + private fun getAllClasses(project: Project, virtualFiles: Array): Set { + val psiFiles = virtualFiles.mapNotNull { it.toPsiFile(project) } + val psiDirectories = virtualFiles.mapNotNull { it.toPsiDirectory(project) } + val dirsArePackages = psiDirectories.all { it.getPackage()?.qualifiedName?.isNotEmpty() == true } + + if (!dirsArePackages) { + return emptySet() + } + val allClasses = psiFiles.flatMap { getClassesFromFile(it) }.toMutableSet() + for (psiDir in psiDirectories) allClasses += getAllClasses(psiDir) + + return allClasses + } + + private fun getClassesFromFile(psiFile: PsiFile): List { + val psiElementHandler = PsiElementHandler.makePsiElementHandler(psiFile) + return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, psiElementHandler.classClass) + .map { psiElementHandler.toPsi(it, PsiClass::class.java) } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/ExternalLibraryDescriptors.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/ExternalLibraryDescriptors.kt index c8e77720ad..23448876c7 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/ExternalLibraryDescriptors.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/ExternalLibraryDescriptors.kt @@ -2,25 +2,89 @@ package org.utbot.intellij.plugin.models import com.intellij.openapi.roots.ExternalLibraryDescriptor import org.jetbrains.idea.maven.utils.library.RepositoryLibraryDescription +import org.utbot.intellij.plugin.ui.utils.Version val ExternalLibraryDescriptor.mavenCoordinates: String get() = "$libraryGroupId:$libraryArtifactId:${preferredVersion ?: RepositoryLibraryDescription.ReleaseVersionId}" val ExternalLibraryDescriptor.id: String - get() = "$libraryGroupId:$libraryArtifactId" + get() = "$libraryGroupId:$libraryArtifactId" //TODO: think about using JUnitExternalLibraryDescriptor from intellij-community sources (difficult to install) -fun jUnit4LibraryDescriptor(versionInProject: String?) = - ExternalLibraryDescriptor("junit", "junit", "4.12", null, versionInProject ?: "4.13.2") +fun jUnit4LibraryDescriptor(versionInProject: Version?): ExternalLibraryDescriptor { + val preferredVersion = if (versionInProject?.hasNumericOrEmptyPatch() == true) versionInProject?.plainText else "4.13.2" + return ExternalLibraryDescriptor( + "junit", "junit", + "4.12", null, preferredVersion + ) +} -fun jUnit5LibraryDescriptor(versionInProject: String?) = - ExternalLibraryDescriptor("org.junit.jupiter", "junit-jupiter", "5.8.1", null, versionInProject ?: "5.8.1") +fun jUnit5LibraryDescriptor(versionInProject: Version?): ExternalLibraryDescriptor { + val preferredVersion = if (versionInProject?.hasNumericOrEmptyPatch() == true) versionInProject?.plainText else "5.8.1" + return ExternalLibraryDescriptor( + "org.junit.jupiter", "junit-jupiter", + "5.8.1", null, preferredVersion + ) +} -fun testNgLibraryDescriptor(versionInProject: String?) = - ExternalLibraryDescriptor("org.testng", "testng", "7.6.0", null, versionInProject ?: "7.6.0") +fun jUnit5ParametrizedTestsLibraryDescriptor(versionInProject: Version?): ExternalLibraryDescriptor { + val preferredVersion = if (versionInProject?.hasNumericOrEmptyPatch() == true) versionInProject?.plainText else "5.8.1" + return ExternalLibraryDescriptor( + "org.junit.jupiter", "junit-jupiter-params", + "5.8.1", null, preferredVersion + ) +} -fun jUnit5ParametrizedTestsLibraryDescriptor(versionInProject: String?) = - ExternalLibraryDescriptor("org.junit.jupiter", "junit-jupiter-params", "5.8.1", null, versionInProject ?: "5.8.1") +fun mockitoCoreLibraryDescriptor(versionInProject: Version?): ExternalLibraryDescriptor { + val preferredVersion = if (versionInProject?.hasNumericOrEmptyPatch() == true) versionInProject?.plainText else "4.11.0" + return ExternalLibraryDescriptor( + "org.mockito", "mockito-core", + "3.5.0", null, preferredVersion + ) +} -fun mockitoCoreLibraryDescriptor(versionInProject: String?) = - ExternalLibraryDescriptor("org.mockito", "mockito-core", "3.5.0", "4.2.0", versionInProject ?: "4.2.0") \ No newline at end of file +fun springBootTestLibraryDescriptor(versionInProject: Version?): ExternalLibraryDescriptor { + val preferredVersion = if (versionInProject?.hasNumericOrEmptyPatch() == true) versionInProject?.plainText else "3.0.6" + return ExternalLibraryDescriptor( + "org.springframework.boot", "spring-boot-test", + "2.4.0", null, preferredVersion + ) +} + +private const val MIN_SUPPORTED_SPRING_VERSION = "2.5" + +fun springTestLibraryDescriptor(versionInProject: Version?): ExternalLibraryDescriptor { + val preferredVersion = if (versionInProject?.hasNumericOrEmptyPatch() == true) versionInProject.plainText else "6.0.8" + return ExternalLibraryDescriptor( + "org.springframework", "spring-test", + MIN_SUPPORTED_SPRING_VERSION, null, preferredVersion + ) +} + +fun springSecurityLibraryDescriptor(versionInProject: Version?): ExternalLibraryDescriptor { + val preferredVersion = if (versionInProject?.hasNumericOrEmptyPatch() == true) versionInProject.plainText else "6.0.8" + return ExternalLibraryDescriptor( + "org.springframework.security", "spring-security-test", + MIN_SUPPORTED_SPRING_VERSION, null, preferredVersion + ) +} + + +/** + * TestNg requires JDK 11 since version 7.6.0 + * For projects with JDK 8 version 7.5 should be installed. + * See https://groups.google.com/g/testng-users/c/BAFB1vk-kok?pli=1 for more details. + */ +fun testNgNewLibraryDescriptor(versionInProject: Version?): ExternalLibraryDescriptor { + val preferredVersion = if (versionInProject?.hasNumericOrEmptyPatch() == true) versionInProject?.plainText else "7.6.0" + return ExternalLibraryDescriptor( + "org.testng", "testng", + "7.6.0", null, preferredVersion + ) +} + +fun testNgOldLibraryDescriptor() = + ExternalLibraryDescriptor( + "org.testng", "testng", + "7.5", "7.5", "7.5" + ) \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt index e11adde947..2ba4bff9bd 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt @@ -1,32 +1,59 @@ package org.utbot.intellij.plugin.models -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.HangingTestsTimeout -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour -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 com.intellij.openapi.components.service +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.JavaSdkVersion +import com.intellij.openapi.project.rootManager import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass import com.intellij.psi.PsiJavaFile +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.searches.AnnotatedElementsSearch import com.intellij.refactoring.util.classMembers.MemberInfo -import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass +import org.jetbrains.concurrency.Promise +import org.jetbrains.kotlin.idea.util.projectStructure.allModules +import org.jetbrains.kotlin.idea.util.sourceRoot import org.jetbrains.kotlin.psi.KtFile +import org.utbot.common.PathUtil.fileExtension +import org.utbot.framework.SummariesGenerationType +import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.JavaDocCommentStyle +import org.utbot.framework.plugin.api.SpringTestType +import org.utbot.framework.plugin.api.SpringSettings import org.utbot.framework.util.ConflictTriggers -import org.utbot.intellij.plugin.ui.utils.jdkVersion +import org.utbot.intellij.plugin.settings.Settings +import org.utbot.intellij.plugin.ui.utils.* +import org.utbot.intellij.plugin.util.binaryName +import java.nio.file.Files +import javax.xml.parsers.DocumentBuilder +import javax.xml.parsers.DocumentBuilderFactory +import kotlin.streams.asSequence + +const val HISTORY_LIMIT = 10 + +const val SPRINGBOOT_APPLICATION_FQN = "org.springframework.boot.autoconfigure.SpringBootApplication" +const val SPRINGBOOT_CONFIGURATION_FQN = "org.springframework.boot.SpringBootConfiguration" +const val SPRING_CONFIGURATION_ANNOTATION_FQN = "org.springframework.context.annotation.Configuration" +const val SPRING_TESTCONFIGURATION_ANNOTATION_FQN = "org.springframework.boot.test.context.TestConfiguration" -data class GenerateTestsModel( - val project: Project, +const val SPRING_BEANS_SCHEMA_URL = "http://www.springframework.org/schema/beans" +const val SPRING_LOAD_DTD_GRAMMAR_PROPERTY = "http://apache.org/xml/features/nonvalidating/load-dtd-grammar" +const val SPRING_LOAD_EXTERNAL_DTD_PROPERTY = "http://apache.org/xml/features/nonvalidating/load-external-dtd" + +class GenerateTestsModel( + project: Project, val srcModule: Module, val potentialTestModules: List, var srcClasses: Set, @@ -35,18 +62,45 @@ data class GenerateTestsModel( var timeout: Long, var generateWarningsForStaticMocking: Boolean = false, var fuzzingValue: Double = 0.05 +): BaseTestsModel( + project, ) { // GenerateTestsModel is supposed to be created with non-empty list of potentialTestModules. // Otherwise, the error window is supposed to be shown earlier. var testModule: Module = potentialTestModules.firstOrNull() ?: error("Empty list of test modules in model") - var testSourceRoot: VirtualFile? = null + override var sourceRootHistory = project.service().sourceRootHistory + override var codegenLanguage = project.service().codegenLanguage + + lateinit var testFramework: TestFramework + lateinit var mockStrategy: MockStrategyApi + lateinit var mockFramework: MockFramework + lateinit var staticsMocking: StaticsMocking + lateinit var parametrizedTestSource: ParametrizedTestSource + lateinit var runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour + lateinit var hangingTestsTimeout: HangingTestsTimeout + var useTaintAnalysis: Boolean = false + var runInspectionAfterTestGeneration: Boolean = true + lateinit var forceStaticMocking: ForceStaticMocking + lateinit var chosenClassesToMockAlways: Set + lateinit var commentStyle: JavaDocCommentStyle + + lateinit var springSettings: SpringSettings + lateinit var springTestType: SpringTestType + lateinit var springConfig: String + lateinit var springProfileNames: String + + val conflictTriggers: ConflictTriggers = ConflictTriggers() + val preClasspathCollectionPromises: MutableList> = mutableListOf() + + var runGeneratedTestsWithCoverage : Boolean = false + var summariesGenerationType : SummariesGenerationType = UtSettings.summaryGenerationType fun setSourceRootAndFindTestModule(newTestSourceRoot: VirtualFile?) { requireNotNull(newTestSourceRoot) testSourceRoot = newTestSourceRoot var target = newTestSourceRoot - while(target != null && target is FakeVirtualFile) { + while (target != null && target is FakeVirtualFile) { target = target.parent } if (target == null) { @@ -57,34 +111,129 @@ data class GenerateTestsModel( ?: error("Could not find module for $newTestSourceRoot") } - var codegenLanguage = if (srcClasses.all { it is KtUltraLightClass }) CodegenLanguage.KOTLIN else CodegenLanguage.JAVA + val isMultiPackage: Boolean by lazy { + srcClasses.map { it.packageName }.distinct().size != 1 + } - var testPackageName: String? = null - lateinit var testFramework: TestFramework - lateinit var mockStrategy: MockStrategyApi - lateinit var mockFramework: MockFramework - lateinit var staticsMocking: StaticsMocking - lateinit var parametrizedTestSource: ParametrizedTestSource - lateinit var runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour - lateinit var hangingTestsTimeout: HangingTestsTimeout - lateinit var forceStaticMocking: ForceStaticMocking - lateinit var chosenClassesToMockAlways: Set - lateinit var commentStyle: JavaDocCommentStyle + fun getAllTestSourceRoots() : MutableList { + with(if (project.isBuildWithGradle) project.allModules() else potentialTestModules) { + return this.flatMap { it.suitableTestSourceRoots().toList() }.toMutableList().distinct().toMutableList() + } + } - val conflictTriggers: ConflictTriggers = ConflictTriggers() + fun getSortedTestRoots(): MutableList = getSortedTestRoots( + getAllTestSourceRoots(), + sourceRootHistory, + srcModule.rootManager.sourceRoots.map { file: VirtualFile -> file.toNioPath().toString() }, + codegenLanguage + ) - val isMultiPackage: Boolean by lazy { - srcClasses.map { it.packageName }.distinct().size != 1 + /** + * Finds @SpringBootApplication classes in Spring application. + * + * @see [getSortedAnnotatedClasses] + */ + fun getSortedSpringBootApplicationClasses(): Set = + getSortedAnnotatedClasses(SPRINGBOOT_CONFIGURATION_FQN) + + getSortedAnnotatedClasses(SPRINGBOOT_APPLICATION_FQN) + + /** + * Finds @TestConfiguration and @Configuration classes in Spring application. + * + * @see [getSortedAnnotatedClasses] + */ + fun getSortedSpringConfigurationClasses(): Set = + getSortedAnnotatedClasses(SPRING_TESTCONFIGURATION_ANNOTATION_FQN) + + getSortedAnnotatedClasses(SPRING_CONFIGURATION_ANNOTATION_FQN) + + /** + * Finds classes annotated with given annotation in [srcModule] and [potentialTestModules]. + * + * Sorting order: + * - classes from test source roots (in the order provided by [getSortedTestRoots]) + * - classes from production source roots + */ + private fun getSortedAnnotatedClasses(annotationFqn: String): Set { + val searchScope = + potentialTestModules + .fold(GlobalSearchScope.moduleScope(srcModule)) { accScope, module -> + accScope.union(GlobalSearchScope.moduleScope(module)) + } + + val annotationClass = JavaPsiFacade + .getInstance(project) + .findClass(annotationFqn, GlobalSearchScope.allScope(project)) + ?: return emptySet() + + val testRootToIndex = + getSortedTestRoots() + .withIndex() + .associate { (i, root) -> root.dir to i } + + return AnnotatedElementsSearch + .searchPsiClasses(annotationClass, searchScope) + .findAll() + .sortedBy { testRootToIndex[it.containingFile.sourceRoot] ?: Int.MAX_VALUE } + .mapNotNullTo(mutableSetOf()) { it.binaryName } } - var runGeneratedTestsWithCoverage : Boolean = false - val jdkVersion: JavaSdkVersion? - get() = try { - testModule.jdkVersion() - } catch (e: IllegalStateException) { - // Just ignore it here, notification will be shown in org.utbot.intellij.plugin.ui.utils.ModuleUtilsKt.jdkVersionBy - null + fun getSpringXMLConfigurationFiles(): Set { + val resourcesPaths = + buildList { + addAll(potentialTestModules) + add(srcModule) + }.distinct().flatMapTo(mutableSetOf()) { it.getResourcesPaths() } + val xmlFilePaths = resourcesPaths.flatMapTo(mutableListOf()) { path -> + Files.walk(path) + .asSequence() + .filter { it.fileExtension == ".xml" } } + + val builder = customizeXmlBuilder() + return xmlFilePaths.mapNotNullTo(mutableSetOf()) { path -> + try { + val doc = builder.parse(path.toFile()) + + val hasBeanTagName = doc.documentElement.tagName == "beans" + val hasAttribute = doc.documentElement.getAttribute("xmlns") == SPRING_BEANS_SCHEMA_URL + when { + hasBeanTagName && hasAttribute -> path.toString() + else -> null + } + } catch (e: Exception) { + // `DocumentBuilder.parse` is an unpredictable operation, may have some side effects, we suppress them. + null + } + } + } + + /** + * Creates "safe" xml builder instance. + * + * Using standard `DocumentBuilderFactory.newInstance()` may lead to some problems like + * https://stackoverflow.com/questions/343383/unable-to-parse-xml-file-using-documentbuilder. + * + * We try to solve it in accordance with top-rated recommendation here + * https://stackoverflow.com/questions/155101/make-documentbuilder-parse-ignore-dtd-references. + */ + private fun customizeXmlBuilder(): DocumentBuilder { + val builderFactory = DocumentBuilderFactory.newInstance() + builderFactory.isNamespaceAware = true + + // See documentation https://xerces.apache.org/xerces2-j/features.html + builderFactory.setFeature(SPRING_LOAD_DTD_GRAMMAR_PROPERTY, false) + builderFactory.setFeature(SPRING_LOAD_EXTERNAL_DTD_PROPERTY, false) + + return builderFactory.newDocumentBuilder() + } + + fun updateSourceRootHistory(path: String) { + sourceRootHistory.apply { + remove(path)//Remove existing entry if any + add(path)//Add the most recent entry to the end to be brought first at sorting, see org.utbot.intellij.plugin.ui.utils.RootUtilsKt.getSortedTestRoots + while (size > HISTORY_LIMIT) removeFirst() + } + } } val PsiClass.packageName: String diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/process/EngineProcess.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/process/EngineProcess.kt index a8323b22f4..7d180f6363 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/process/EngineProcess.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/process/EngineProcess.kt @@ -1,225 +1,227 @@ package org.utbot.intellij.plugin.process import com.intellij.ide.plugins.cl.PluginClassLoader +import com.intellij.openapi.application.runReadAction import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Computable import com.intellij.psi.PsiMethod +import com.intellij.psi.impl.file.impl.JavaFileManager +import com.intellij.psi.search.GlobalSearchScope import com.intellij.refactoring.util.classMembers.MemberInfo -import com.jetbrains.rd.util.lifetime.Lifetime -import com.jetbrains.rd.util.lifetime.throwIfNotAlive +import com.jetbrains.rd.util.ConcurrentHashMap +import com.jetbrains.rd.util.lifetime.LifetimeDefinition import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import mu.KotlinLogging -import org.utbot.common.AbstractSettings -import org.utbot.common.getPid -import org.utbot.common.osSpecificJavaExecutable -import org.utbot.common.utBotTempDirectory +import org.utbot.common.* import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.* -import org.utbot.framework.codegen.model.UtilClassKind +import org.utbot.framework.codegen.tree.ututils.UtilClassKind +import org.utbot.framework.context.ApplicationContext import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.services.JdkInfo -import org.utbot.framework.plugin.services.JdkInfoDefaultProvider -import org.utbot.framework.plugin.services.JdkInfoService import org.utbot.framework.plugin.services.WorkingDirService -import org.utbot.framework.process.OpenModulesContainer +import org.utbot.framework.process.AbstractRDProcessCompanion +import org.utbot.framework.process.EngineProcessTask import org.utbot.framework.process.generated.* -import org.utbot.framework.process.generated.Signature +import org.utbot.framework.process.generated.MethodDescription +import org.utbot.framework.process.kryo.KryoHelper import org.utbot.framework.util.Conflict import org.utbot.framework.util.ConflictTriggers -import org.utbot.instrumentation.Settings -import org.utbot.instrumentation.util.KryoHelper +import org.utbot.intellij.plugin.UtbotBundle import org.utbot.intellij.plugin.models.GenerateTestsModel import org.utbot.intellij.plugin.ui.TestReportUrlOpeningListener -import org.utbot.intellij.plugin.util.signature -import org.utbot.rd.ProcessWithRdServer -import org.utbot.rd.rdPortArgument -import org.utbot.rd.startUtProcessWithRdServer +import org.utbot.intellij.plugin.util.assertReadAccessNotAllowed +import org.utbot.intellij.plugin.util.methodDescription +import org.utbot.rd.* +import org.utbot.rd.exceptions.InstantProcessDeathException +import org.utbot.rd.generated.SettingForResult +import org.utbot.rd.generated.SettingsModel +import org.utbot.rd.generated.settingsModel +import org.utbot.rd.generated.synchronizationModel +import org.utbot.rd.loggers.overrideDefaultRdLoggerFactoryWithKLogger import org.utbot.sarif.SourceFindingStrategy import java.io.File +import java.nio.charset.Charset +import java.nio.file.Files import java.nio.file.Path -import kotlin.io.path.deleteIfExists +import java.nio.file.StandardCopyOption import kotlin.io.path.pathString -import kotlin.random.Random import kotlin.reflect.KProperty1 import kotlin.reflect.full.memberProperties -private val engineProcessLogConfigurations = utBotTempDirectory.toFile().resolve("rdEngineProcessLogConfigurations") -private val logger = KotlinLogging.logger {} -private val engineProcessLogDirectory = utBotTempDirectory.toFile().resolve("rdEngineProcessLogs") +private val engineProcessLogConfigurationsDirectory = utBotTempDirectory.toFile().resolve("rdEngineProcessLogConfigurations").also { it.mkdirs() } +private val logger = KotlinLogging.logger {}.also { overrideDefaultRdLoggerFactoryWithKLogger(it) } +private val engineProcessLogDirectory = utBotTempDirectory.toFile().resolve("rdEngineProcessLogs").also { it.mkdirs() } -data class RdTestGenerationResult(val notEmptyCases: Int, val testSetsId: Long) +private const val configurationFileDeleteKey = "delete_this_comment_key" +private const val deleteOpenComment = "" -class EngineProcess(parent: Lifetime, val project: Project) { - private val ldef = parent.createNested() - private val id = Random.nextLong() - private var count = 0 - private var configPath: Path? = null - - private fun getOrCreateLogConfig(): String { - var realPath = configPath - if (realPath == null) { - engineProcessLogConfigurations.mkdirs() - configPath = File.createTempFile("epl", ".xml", engineProcessLogConfigurations).apply { - val onMatch = if (UtSettings.logConcreteExecutionErrors) "NEUTRAL" else "DENY" - writeText( - """ - - - - - - - - - - - - -""" - ) - }.toPath() - realPath = configPath - } - return realPath!!.pathString - } +private fun createEngineProcessLog4j2Config(): File { + val customFile = File(UtSettings.engineProcessLogConfigFile) - private fun debugArgument(): String { - return "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=5005".takeIf { Settings.runIdeaProcessWithDebug } - ?: "" + val log4j2ConfigFile = + if (customFile.exists()) customFile + else Files.createTempFile(engineProcessLogConfigurationsDirectory.toPath(), null, ".xml").toFile() + + EngineProcess::class.java.classLoader.getResourceAsStream("log4j2.xml")?.use { logConfig -> + val resultConfig = logConfig.readBytes().toString(Charset.defaultCharset()) + .replace(Regex("$deleteOpenComment|$deleteCloseComment"), "") + .replace("ref=\"IdeaAppender\"", "ref=\"EngineProcessAppender\"") + .replace("\${env:UTBOT_LOG_DIR}", engineProcessLogDirectory.canonicalPath.trimEnd(File.separatorChar) + File.separatorChar) + Files.copy( + resultConfig.byteInputStream(), + log4j2ConfigFile.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) } + return log4j2ConfigFile +} + +private val log4j2ConfigFile: File = createEngineProcessLog4j2Config() + +private val log4j2ConfigSwitch = "-Dlog4j2.configurationFile=${log4j2ConfigFile.canonicalPath}" + +private val pluginClasspath: String + get() = (EngineProcess::class.java.classLoader as PluginClassLoader).classPath.baseUrls.joinToString( + separator = File.pathSeparator + ) + +private const val startFileName = "org.utbot.framework.process.EngineProcessMainKt" + +data class RdTestGenerationResult(val notEmptyCases: Int, val testSetsId: Long) + +class EngineProcessInstantDeathException : + InstantProcessDeathException(UtSettings.engineProcessDebugPort, UtSettings.runEngineProcessWithDebug) + +class EngineProcess private constructor(val project: Project, private val classNameToPath: Map, rdProcess: ProcessWithRdServer) : + ProcessWithRdServer by rdProcess { + companion object : AbstractRDProcessCompanion( + debugPort = UtSettings.engineProcessDebugPort, + runWithDebug = UtSettings.runEngineProcessWithDebug, + suspendExecutionInDebugMode = UtSettings.suspendEngineProcessExecutionInDebugMode, + processSpecificCommandLineArgs = { listOf("-ea", log4j2ConfigSwitch, "-cp", pluginClasspath, startFileName) } + ) { + fun createBlocking(project: Project, classNameToPath: Map): EngineProcess = runBlocking { EngineProcess(project, classNameToPath) } - private val kryoHelper = KryoHelper(ldef) - - private suspend fun engineModel(): EngineProcessModel { - ldef.throwIfNotAlive() - return lock.withLock { - var proc = current - - if (proc == null) { - proc = startUtProcessWithRdServer(ldef) { port -> - val current = JdkInfoDefaultProvider().info - val required = JdkInfoService.jdkInfoProvider.info - val java = - JdkInfoService.jdkInfoProvider.info.path.resolve("bin${File.separatorChar}${osSpecificJavaExecutable()}").toString() - val cp = (this.javaClass.classLoader as PluginClassLoader).classPath.baseUrls.joinToString( - separator = ";", - prefix = "\"", - postfix = "\"" - ) - val classname = "org.utbot.framework.process.EngineMainKt" - val javaVersionSpecificArguments = OpenModulesContainer.javaVersionSpecificArguments + suspend operator fun invoke(project: Project, classNameToPath: Map): EngineProcess = + LifetimeDefinition().terminateOnException { lifetime -> + val rdProcess = startUtProcessWithRdServer(lifetime) { port -> + val cmd = obtainProcessCommandLine(port) val directory = WorkingDirService.provide().toFile() - val log4j2ConfigFile = "\"-Dlog4j2.configurationFile=${getOrCreateLogConfig()}\"" - val debugArg = debugArgument() - logger.info { "java - $java\nclasspath - $cp\nport - $port" } - val cmd = mutableListOf(java, "-ea") - if (javaVersionSpecificArguments.isNotEmpty()) { - cmd.addAll(javaVersionSpecificArguments) - } - if (debugArg.isNotEmpty()) { - cmd.add(debugArg) - } - cmd.add(log4j2ConfigFile) - cmd.add("-cp") - cmd.add(cp) - cmd.add(classname) - cmd.add(rdPortArgument(port)) - ProcessBuilder(cmd).directory(directory).apply { - if (UtSettings.logConcreteExecutionErrors) { - engineProcessLogDirectory.mkdirs() - val logFile = File(engineProcessLogDirectory, "${id}-${count++}.log") - logger.info { "logFile - ${logFile.canonicalPath}" } - redirectOutput(logFile) - redirectError(logFile) - } - }.start().apply { - logger.debug { "Engine process started with PID = ${this.getPid}" } - } - }.initModels { - engineProcessModel - rdSourceFindingStrategy - settingsModel.settingFor.set { params -> - SettingForResult(AbstractSettings.allSettings[params.key]?.let { settings: AbstractSettings -> - val members: Collection> = - settings.javaClass.kotlin.memberProperties - val names: List> = - members.filter { it.name == params.propertyName } - val sing: KProperty1 = names.single() - val result = sing.get(settings) - logger.trace { "request for settings ${params.key}:${params.propertyName} - $result" } - result.toString() - }) + val builder = ProcessBuilder(cmd).directory(directory) + val process = builder.start() + + logger.info { "Engine process started with PID = ${process.getPid}" } + logger.info { "Engine process log directory - ${engineProcessLogDirectory.canonicalPath}" } + logger.info { "Engine process log file - ${engineProcessLogDirectory.resolve("utbot-engine-current.log")}" } + logger.info { "Log4j2 configuration file path - ${log4j2ConfigFile.canonicalPath}" } + + if (!process.isAlive) { + throw EngineProcessInstantDeathException() } - }.awaitSignal() - current = proc - } - proc.protocol.engineProcessModel - } + process + } + rdProcess.awaitProcessReady() + + return EngineProcess(project, classNameToPath, rdProcess) + } } - private val lock = Mutex() - private var current: ProcessWithRdServer? = null + private val engineModel: EngineProcessModel = onSchedulerBlocking { protocol.engineProcessModel } + private val instrumenterAdapterModel: RdInstrumenterAdapter = onSchedulerBlocking { protocol.rdInstrumenterAdapter } + private val sourceFindingModel: RdSourceFindingStrategy = onSchedulerBlocking { protocol.rdSourceFindingStrategy } + private val settingsModel: SettingsModel = onSchedulerBlocking { protocol.settingsModel } - fun setupUtContext(classpathForUrlsClassloader: List) = runBlocking { - engineModel().setupUtContext.startSuspending(ldef, SetupContextParams(classpathForUrlsClassloader)) + private val kryoHelper = KryoHelper(lifetime) + private val sourceFindingStrategies = ConcurrentHashMap() + + fun setupUtContext(classpathForUrlsClassloader: List) { + assertReadAccessNotAllowed() + engineModel.setupUtContext.startBlocking(SetupContextParams(classpathForUrlsClassloader)) } - // suppose that only 1 simultaneous test generator process can be executed in idea - // so every time test generator is created - we just overwrite previous + private fun computeSourceFileByClass(params: ComputeSourceFileByClassArguments): String? = + DumbService.getInstance(project).runReadActionInSmartMode { + val scope = GlobalSearchScope.allScope(project) + // JavaFileManager requires canonical name as it is said in import + val psiClass = JavaFileManager.getInstance(project).findClass(params.canonicalClassName, scope) + val sourceFile = psiClass?.navigationElement?.containingFile?.virtualFile?.canonicalPath + + logger.debug { "computeSourceFileByClass result: $sourceFile" } + sourceFile ?: classNameToPath[params.canonicalClassName] + } + fun createTestGenerator( buildDir: List, classPath: String?, dependencyPaths: String, jdkInfo: JdkInfo, + applicationContext: ApplicationContext, isCancelled: (Unit) -> Boolean - ) = runBlocking { - engineModel().isCancelled.set(handler = isCancelled) - engineModel().createTestGenerator.startSuspending( - ldef, - TestGeneratorParams(buildDir.toTypedArray(), classPath, dependencyPaths, JdkInfo(jdkInfo.path.pathString, jdkInfo.version)) + ) { + assertReadAccessNotAllowed() + + engineModel.isCancelled.set(handler = isCancelled) + instrumenterAdapterModel.computeSourceFileByClass.set(handler = this::computeSourceFileByClass) + + val params = TestGeneratorParams( + buildDir.toTypedArray(), + classPath, + dependencyPaths, + JdkInfo(jdkInfo.path.pathString, jdkInfo.version), + kryoHelper.writeObject(applicationContext) ) + engineModel.createTestGenerator.startBlocking(params) } - fun obtainClassId(canonicalName: String): ClassId = runBlocking { - kryoHelper.readObject(engineModel().obtainClassId.startSuspending(canonicalName)) + fun obtainClassId(binaryName: String): ClassId { + assertReadAccessNotAllowed() + return kryoHelper.readObject(engineModel.obtainClassId.startBlocking(binaryName)) } - fun findMethodsInClassMatchingSelected(clazzId: ClassId, srcMethods: List): List = - runBlocking { - val srcSignatures = srcMethods.map { it.signature() } - val rdSignatures = srcSignatures.map { - Signature(it.name, it.parameterTypes) - } - kryoHelper.readObject( - engineModel().findMethodsInClassMatchingSelected.startSuspending( - FindMethodsInClassMatchingSelectedArguments(kryoHelper.writeObject(clazzId), rdSignatures) - ).executableIds - ) - } + fun findMethodsInClassMatchingSelected(clazzId: ClassId, srcMethods: List): List { + assertReadAccessNotAllowed() - fun findMethodParamNames(classId: ClassId, methods: List): Map> = - runBlocking { - val bySignature = methods.associate { it.signature() to it.paramNames() } - kryoHelper.readObject( - engineModel().findMethodParamNames.startSuspending( - FindMethodParamNamesArguments( - kryoHelper.writeObject( - classId - ), kryoHelper.writeObject(bySignature) - ) - ).paramNames - ) + val srcDescriptions = runReadAction { srcMethods.map { it.methodDescription() } } + val rdDescriptions = srcDescriptions.map { MethodDescription(it.name, it.containingClass, it.parameterTypes) } + val binaryClassId = kryoHelper.writeObject(clazzId) + val arguments = FindMethodsInClassMatchingSelectedArguments(binaryClassId, rdDescriptions) + val result = engineModel.findMethodsInClassMatchingSelected.startBlocking(arguments) + + return kryoHelper.readObject(result.executableIds) + } + + fun findMethodParamNames(classId: ClassId, methods: List): Map> { + assertReadAccessNotAllowed() + + val bySignature = executeWithTimeoutSuspended { + DumbService.getInstance(project).runReadActionInSmartMode(Computable { + methods.map { it.methodDescription() to it.paramNames() } + }) } + val arguments = FindMethodParamNamesArguments( + kryoHelper.writeObject(classId), + kryoHelper.writeObject(bySignature) + ) + val result = engineModel.findMethodParamNames.startBlocking(arguments).paramNames + + return kryoHelper.readObject(result) + } - private fun MemberInfo.paramNames(): List = - (this.member as PsiMethod).parameterList.parameters.map { it.name } + private fun MemberInfo.paramNames(): List = (this.member as PsiMethod).parameterList.parameters.map { + if (it.name.startsWith("\$this")) + // If member is Kotlin extension function, name of first argument isn't good for further usage, + // so we better choose name based on type of receiver. + // + // There seems no API to check whether parameter is an extension receiver by PSI + it.type.presentableText + else + it.name + } fun generate( - mockInstalled: Boolean, - staticsMockingIsConfigured: Boolean, conflictTriggers: ConflictTriggers, methods: List, mockStrategyApi: MockStrategyApi, @@ -229,145 +231,196 @@ class EngineProcess(parent: Lifetime, val project: Project) { isSymbolicEngineEnabled: Boolean, isFuzzingEnabled: Boolean, fuzzingValue: Double, - searchDirectory: String - ): RdTestGenerationResult = runBlocking { - val result = engineModel().generate.startSuspending( - ldef, - GenerateParams( - mockInstalled, - staticsMockingIsConfigured, - kryoHelper.writeObject(conflictTriggers.toMutableMap()), - kryoHelper.writeObject(methods), - mockStrategyApi.name, - kryoHelper.writeObject(chosenClassesToMockAlways), - timeout, - generationTimeout, - isSymbolicEngineEnabled, - isFuzzingEnabled, - fuzzingValue, - searchDirectory - ) + searchDirectory: String, + taintConfigPath: String? + ): RdTestGenerationResult { + assertReadAccessNotAllowed() + val params = GenerateParams( + kryoHelper.writeObject(methods), + mockStrategyApi.name, + kryoHelper.writeObject(chosenClassesToMockAlways), + timeout, + generationTimeout, + isSymbolicEngineEnabled, + isFuzzingEnabled, + fuzzingValue, + searchDirectory, + taintConfigPath ) + val result = engineModel.generate.startBlocking(params) - return@runBlocking RdTestGenerationResult(result.notEmptyCases, result.testSetsId) + return RdTestGenerationResult(result.notEmptyCases, result.testSetsId) } fun render( + model: GenerateTestsModel, testSetsId: Long, classUnderTest: ClassId, paramNames: MutableMap>, generateUtilClassFile: Boolean, - testFramework: TestFramework, - mockFramework: MockFramework, - staticsMocking: StaticsMocking, - forceStaticMocking: ForceStaticMocking, - generateWarningsForStaticsMocking: Boolean, - codegenLanguage: CodegenLanguage, - parameterizedTestSource: ParametrizedTestSource, - runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour, - hangingTestSource: HangingTestsTimeout, enableTestsTimeout: Boolean, - testClassPackageName: String - ): Pair = runBlocking { - val result = engineModel().render.startSuspending( - ldef, RenderParams( - testSetsId, - kryoHelper.writeObject(classUnderTest), - kryoHelper.writeObject(paramNames), - generateUtilClassFile, - testFramework.id.lowercase(), - mockFramework.name, - codegenLanguage.name, - parameterizedTestSource.name, - staticsMocking.id, - kryoHelper.writeObject(forceStaticMocking), - generateWarningsForStaticsMocking, - runtimeExceptionTestsBehaviour.name, - hangingTestSource.timeoutMs, - enableTestsTimeout, - testClassPackageName - ) - ) - result.generatedCode to kryoHelper.readObject(result.utilClassKind) + testClassPackageName: String, + ): Pair { + assertReadAccessNotAllowed() + val params = makeParams( + model, + testSetsId, + classUnderTest, + paramNames, + generateUtilClassFile, + enableTestsTimeout, + testClassPackageName, + ) + val result = engineModel.render.startBlocking(params) + val realUtilClassKind = result.utilClassKind?.let { + if (UtilClassKind.RegularUtUtils(model.codegenLanguage).javaClass.simpleName == it) + UtilClassKind.RegularUtUtils(model.codegenLanguage) + else + UtilClassKind.UtUtilsWithMockito(model.codegenLanguage) + } + + return result.generatedCode to realUtilClassKind } - fun forceTermination() = runBlocking { - configPath?.deleteIfExists() - engineModel().stopProcess.start(Unit) - current?.terminate() + fun findTestClassName(classUnderTest: ClassId): String { + val params = TestClassNameParams(kryoHelper.writeObject(classUnderTest)) + val result = engineModel.findTestClassName.startBlocking(params) + return result.testClassName } - fun writeSarif(reportFilePath: Path, - testSetsId: Long, - generatedTestsCode: String, - sourceFindingStrategy: SourceFindingStrategy - ) = runBlocking { - current!!.protocol.rdSourceFindingStrategy.let { - it.getSourceFile.set { params -> - DumbService.getInstance(project).runReadActionInSmartMode { - sourceFindingStrategy.getSourceFile( - params.classFqn, - params.extension - )?.canonicalPath - } - } - it.getSourceRelativePath.set { params -> - DumbService.getInstance(project).runReadActionInSmartMode { - sourceFindingStrategy.getSourceRelativePath( - params.classFqn, - params.extension - ) - } - } - it.testsRelativePath.set { _ -> - DumbService.getInstance(project).runReadActionInSmartMode { - sourceFindingStrategy.testsRelativePath - } - } + private fun makeParams( + model: GenerateTestsModel, + testSetsId: Long, + classUnderTest: ClassId, + paramNames: MutableMap>, + generateUtilClassFile: Boolean, + enableTestsTimeout: Boolean, + testClassPackageName: String, + ): RenderParams = + RenderParams( + testSetsId, + kryoHelper.writeObject(classUnderTest), + model.projectType.toString(), + kryoHelper.writeObject(paramNames), + generateUtilClassFile, + model.testFramework.id.lowercase(), + model.mockFramework.name, + model.codegenLanguage.name, + model.parametrizedTestSource.name, + model.staticsMocking.id, + kryoHelper.writeObject(model.forceStaticMocking), + model.generateWarningsForStaticMocking, + model.runtimeExceptionTestsBehaviour.name, + model.hangingTestsTimeout.timeoutMs, + enableTestsTimeout, + testClassPackageName, + ) + + private fun getSourceFile(params: SourceStrategyMethodArgs): String? = + DumbService.getInstance(project).runReadActionInSmartMode { + sourceFindingStrategies[params.testSetId]!!.getSourceFile( + params.classFqn, + params.extension + )?.canonicalPath } - engineModel().writeSarifReport.startSuspending(WriteSarifReportArguments(testSetsId, reportFilePath.pathString, generatedTestsCode)) - } - fun generateTestsReport(model: GenerateTestsModel, eventLogMessage: String?): Triple = runBlocking { - val forceMockWarning = if (model.conflictTriggers[Conflict.ForceMockHappened] == true) { - """ - 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() - } else null - val forceStaticMockWarnings = if (model.conflictTriggers[Conflict.ForceStaticMockHappened] == true) { - """ - Warning: Some test cases were ignored, because mockito-inline is not installed in the project.
    - Better results could be achieved by configuring mockito-inline. - """.trimIndent() - } else null - val testFrameworkWarnings = if (model.conflictTriggers[Conflict.TestFrameworkConflict] == true) { - """ - 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() - } else null - val result = engineModel().generateTestReport.startSuspending( - GenerateTestReportArgs( - eventLogMessage, - model.testPackageName, - model.isMultiPackage, - forceMockWarning, - forceStaticMockWarnings, - testFrameworkWarnings, - model.conflictTriggers.triggered + private fun getSourceRelativePath(params: SourceStrategyMethodArgs): String = + DumbService.getInstance(project).runReadActionInSmartMode { + sourceFindingStrategies[params.testSetId]!!.getSourceRelativePath( + params.classFqn, + params.extension ) + } + + private fun testsRelativePath(testSetId: Long): String = + DumbService.getInstance(project).runReadActionInSmartMode { + sourceFindingStrategies[testSetId]!!.testsRelativePath + } + + private fun initSourceFindingStrategies() { + sourceFindingModel.getSourceFile.set(handler = this::getSourceFile) + sourceFindingModel.getSourceRelativePath.set(handler = this::getSourceRelativePath) + sourceFindingModel.testsRelativePath.set(handler = this::testsRelativePath) + } + + fun writeSarif( + reportFilePath: Path, + testSetsId: Long, + generatedTestsCode: String, + sourceFindingStrategy: SourceFindingStrategy + ): String { + assertReadAccessNotAllowed() + + val params = WriteSarifReportArguments(testSetsId, reportFilePath.pathString, generatedTestsCode) + + sourceFindingStrategies[testSetsId] = sourceFindingStrategy + return engineModel.writeSarifReport.startBlocking(params) + } + + fun generateTestsReport(model: GenerateTestsModel, eventLogMessage: String?): Triple { + assertReadAccessNotAllowed() + + val forceMockWarning = UtbotBundle.takeIf( + "test.report.force.mock.warning", + TestReportUrlOpeningListener.prefix, + TestReportUrlOpeningListener.mockitoSuffix + ) { model.conflictTriggers[Conflict.ForceMockHappened] == true } + val forceStaticMockWarnings = UtbotBundle.takeIf( + "test.report.force.static.mock.warning", + TestReportUrlOpeningListener.prefix, + TestReportUrlOpeningListener.mockitoInlineSuffix + ) { model.conflictTriggers[Conflict.ForceStaticMockHappened] == true } + val testFrameworkWarnings = + UtbotBundle.takeIf("test.report.test.framework.warning") { model.conflictTriggers[Conflict.TestFrameworkConflict] == true } + val params = GenerateTestReportArgs( + eventLogMessage, + model.testPackageName, + model.isMultiPackage, + forceMockWarning, + forceStaticMockWarnings, + testFrameworkWarnings, + model.conflictTriggers.anyTriggered ) + val result = engineModel.generateTestReport.startBlocking(params) - return@runBlocking Triple(result.notifyMessage, result.statistics, result.hasWarnings) + return Triple(result.notifyMessage, result.statistics, result.hasWarnings) + } + + fun perform(engineProcessTask: EngineProcessTask): R { + assertReadAccessNotAllowed() + return kryoHelper.readObject( + engineModel.perform.startBlocking(PerformParams(kryoHelper.writeObject(engineProcessTask))) + ) } init { - ldef.onTermination { - forceTermination() + lifetime.onTermination { + protocol.synchronizationModel.stopProcess.fire(Unit) + } + settingsModel.settingFor.set { params -> + SettingForResult(AbstractSettings.allSettings[params.key]?.let { settings: AbstractSettings -> + val members: Collection> = + settings.javaClass.kotlin.memberProperties + val names: List> = + members.filter { it.name == params.propertyName } + val sing: KProperty1 = names.single() + val result = sing.get(settings) + logger.trace { "request for settings ${params.key}:${params.propertyName} - $result" } + result.toString() + }) + } + initSourceFindingStrategies() + } + + fun executeWithTimeoutSuspended(block: () -> T): T { + try { + assertReadAccessNotAllowed() + protocol.synchronizationModel.suspendTimeoutTimer.startBlocking(true) + return block() + } + finally { + assertReadAccessNotAllowed() + protocol.synchronizationModel.suspendTimeoutTimer.startBlocking(false) } } } \ No newline at end of file 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 b7077863aa..7a24088189 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 @@ -1,15 +1,22 @@ package org.utbot.intellij.plugin.sarif +import com.intellij.openapi.application.WriteAction +import com.intellij.psi.PsiClass +import com.intellij.openapi.progress.ProgressIndicator import org.utbot.common.PathUtil.classFqnToPath import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath -import com.intellij.openapi.vfs.VfsUtil -import org.jetbrains.kotlin.idea.util.application.runWriteAction +import java.util.concurrent.CountDownLatch +import mu.KotlinLogging import org.utbot.framework.plugin.api.ClassId +import org.utbot.intellij.plugin.generator.UtTestsDialogProcessor import org.utbot.intellij.plugin.models.GenerateTestsModel import org.utbot.intellij.plugin.process.EngineProcess +import org.utbot.sarif.Sarif +import org.utbot.intellij.plugin.util.IntelliJApiHelper +import java.nio.file.Path object SarifReportIdea { - + private val logger = KotlinLogging.logger {} /** * Creates the SARIF report by calling the SarifReport.createReport(), * saves it to test resources directory and notifies the user about the creation. @@ -20,17 +27,32 @@ object SarifReportIdea { classId: ClassId, model: GenerateTestsModel, generatedTestsCode: String, - sourceFinding: SourceFindingStrategyIdea + psiClass: PsiClass, + reportsCountDown: CountDownLatch, + srcClassPathToSarifReport: MutableMap, + srcClassPath: Path, + indicator: ProgressIndicator ) { + UtTestsDialogProcessor.updateIndicator(indicator, UtTestsDialogProcessor.ProgressRange.SARIF, "Generate SARIF report for ${classId.name}", .5) // building the path to the report file val classFqn = classId.name - val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot) + val (sarifReportsPath, sourceFinding) = WriteAction.computeAndWait, Exception> { + model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot) to SourceFindingStrategyIdea(psiClass) + } val reportFilePath = sarifReportsPath.resolve("${classFqnToPath(classFqn)}Report.sarif") - // creating report related directory - runWriteAction { VfsUtil.createDirectoryIfMissing(reportFilePath.parent.toString()) } - - proc.writeSarif(reportFilePath, testSetsId, generatedTestsCode, sourceFinding) + IntelliJApiHelper.run(IntelliJApiHelper.Target.THREAD_POOL, indicator, "Save SARIF report for ${classId.name}") { + try { + val sarifReportAsJson = proc.writeSarif(reportFilePath, testSetsId, generatedTestsCode, sourceFinding) + val newSarifReport = Sarif.fromJson(sarifReportAsJson) + val oldSarifReport = srcClassPathToSarifReport[srcClassPath] ?: Sarif.empty() + srcClassPathToSarifReport[srcClassPath] = oldSarifReport + newSarifReport + } catch (e: Exception) { + logger.error { e } + } finally { + reportsCountDown.countDown() + } + } } } diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SourceFindingStrategyIdea.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SourceFindingStrategyIdea.kt index 456cc749fe..e98f6247f1 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SourceFindingStrategyIdea.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SourceFindingStrategyIdea.kt @@ -2,7 +2,7 @@ package org.utbot.intellij.plugin.sarif import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass -import org.jetbrains.kotlin.idea.search.allScope +import com.intellij.psi.search.GlobalSearchScope import org.utbot.common.PathUtil.classFqnToPath import org.utbot.common.PathUtil.safeRelativize import org.utbot.common.PathUtil.toPath @@ -59,7 +59,7 @@ class SourceFindingStrategyIdea(testClass: PsiClass) : SourceFindingStrategy() { */ private fun findPsiClass(classFqn: String): PsiClass? { val psiFacade = JavaPsiFacade.getInstance(project) - val psiClass = psiFacade.findClass(classFqn, project.allScope()) + val psiClass = psiFacade.findClass(classFqn, GlobalSearchScope.allScope(project)) if (psiClass != null) return psiClass diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/JavaConfigurable.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/JavaConfigurable.kt new file mode 100644 index 0000000000..b22f57ce78 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/JavaConfigurable.kt @@ -0,0 +1,23 @@ +package org.utbot.intellij.plugin.settings + +import com.intellij.openapi.options.SearchableConfigurable +import com.intellij.openapi.project.Project +import javax.swing.JComponent + +class JavaConfigurable(val project: Project) : SearchableConfigurable { + private val displayName: String = "UtBot Java Configuration" + private val id: String = "org.utbot.intellij.plugin.settings.UtBotSettingsConfigurableJava" + private val settingsWindow = SettingsWindow(project) + + override fun createComponent(): JComponent = settingsWindow.panel + + override fun isModified(): Boolean = settingsWindow.isModified() + + override fun apply() = settingsWindow.apply() + + override fun reset() = settingsWindow.reset() + + override fun getDisplayName(): String = displayName + + override fun getId(): String = id +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/JavaTestFrameworkMapper.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/JavaTestFrameworkMapper.kt new file mode 100644 index 0000000000..e3aee633cf --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/JavaTestFrameworkMapper.kt @@ -0,0 +1,28 @@ +package org.utbot.intellij.plugin.settings + +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.TestNg + +object JavaTestFrameworkMapper : TestFrameworkMapper { + override fun toString(value: TestFramework): String = value.id + + override fun fromString(value: String): TestFramework = when (value) { + Junit4.id -> Junit4 + Junit5.id -> Junit5 + TestNg.id -> TestNg + else -> error("Unknown TestFramework $value") + } + + override fun handleUnknown(testFramework: TestFramework): TestFramework { + if (TestFramework.allItems.contains(testFramework)) { + return testFramework + } + return try { + fromString(testFramework.id) + } catch (ex: IllegalStateException) { + TestFramework.defaultItem + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/MockAlwaysClassesTable.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/MockAlwaysClassesTable.kt index 028bdb23f2..133167ff2e 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/MockAlwaysClassesTable.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/MockAlwaysClassesTable.kt @@ -13,6 +13,7 @@ import com.intellij.openapi.ui.cellvalidators.ValidatingTableCellRendererWrapper import com.intellij.openapi.util.Disposer import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass +import com.intellij.psi.search.GlobalSearchScope import com.intellij.ui.components.fields.ExtendableTextField import com.intellij.ui.table.JBTable import com.intellij.util.ui.ColumnInfo @@ -25,7 +26,6 @@ import javax.swing.JTextField import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.TableCellEditor import javax.swing.table.TableCellRenderer -import org.jetbrains.kotlin.idea.search.allScope @Suppress("UnstableApiUsage") internal class MockAlwaysClassesTable(project: Project) : ListTableWithButtons() { @@ -123,7 +123,7 @@ internal class MockAlwaysClassesTable(project: Project) : ListTableWithButtons { - data class State( - var codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem, - @OptionTag(converter = TestFrameworkConverter::class) - var testFramework: TestFramework = TestFramework.defaultItem, - var mockStrategy: MockStrategyApi = MockStrategyApi.defaultItem, - var mockFramework: MockFramework = MockFramework.defaultItem, - @OptionTag(converter = StaticsMockingConverter::class) - var staticsMocking: StaticsMocking = StaticsMocking.defaultItem, - var runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, - @OptionTag(converter = HangingTestsTimeoutConverter::class) - var hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), - var forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem, - var treatOverflowAsError: TreatOverflowAsError = TreatOverflowAsError.defaultItem, - var parametrizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem, - var classesToMockAlways: Array = Mocker.defaultSuperClassesToMockAlwaysNames.toTypedArray(), - var fuzzingValue: Double = 0.05, - var runGeneratedTestsWithCoverage: Boolean = false, - var commentStyle: JavaDocCommentStyle = JavaDocCommentStyle.defaultItem - ) { - constructor(model: GenerateTestsModel) : this( - codegenLanguage = model.codegenLanguage, - testFramework = model.testFramework, - mockStrategy = model.mockStrategy, - mockFramework = model.mockFramework ?: MockFramework.defaultItem, - staticsMocking = model.staticsMocking, - runtimeExceptionTestsBehaviour = model.runtimeExceptionTestsBehaviour, - hangingTestsTimeout = model.hangingTestsTimeout, - forceStaticMocking = model.forceStaticMocking, - parametrizedTestSource = model.parametrizedTestSource, - classesToMockAlways = model.chosenClassesToMockAlways.mapTo(mutableSetOf()) { it.name }.toTypedArray(), - fuzzingValue = model.fuzzingValue, - runGeneratedTestsWithCoverage = model.runGeneratedTestsWithCoverage, - commentStyle = model.commentStyle - ) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as State - - if (codegenLanguage != other.codegenLanguage) return false - if (testFramework != other.testFramework) return false - if (mockStrategy != other.mockStrategy) return false - if (mockFramework != other.mockFramework) return false - if (staticsMocking != other.staticsMocking) return false - if (runtimeExceptionTestsBehaviour != other.runtimeExceptionTestsBehaviour) return false - if (hangingTestsTimeout != other.hangingTestsTimeout) return false - if (forceStaticMocking != other.forceStaticMocking) return false - if (treatOverflowAsError != other.treatOverflowAsError) return false - if (parametrizedTestSource != other.parametrizedTestSource) return false - if (!classesToMockAlways.contentEquals(other.classesToMockAlways)) return false - if (fuzzingValue != other.fuzzingValue) return false - if (runGeneratedTestsWithCoverage != other.runGeneratedTestsWithCoverage) return false - - return true - } - override fun hashCode(): Int { - var result = codegenLanguage.hashCode() - result = 31 * result + testFramework.hashCode() - result = 31 * result + mockStrategy.hashCode() - result = 31 * result + mockFramework.hashCode() - result = 31 * result + staticsMocking.hashCode() - result = 31 * result + runtimeExceptionTestsBehaviour.hashCode() - result = 31 * result + hangingTestsTimeout.hashCode() - result = 31 * result + forceStaticMocking.hashCode() - result = 31 * result + treatOverflowAsError.hashCode() - result = 31 * result + parametrizedTestSource.hashCode() - result = 31 * result + classesToMockAlways.contentHashCode() - result = 31 * result + fuzzingValue.hashCode() - result = 31 * result + if (runGeneratedTestsWithCoverage) 1 else 0 - - return result - } - } - - private var state = State() - - val codegenLanguage: CodegenLanguage get() = state.codegenLanguage - - val testFramework: TestFramework get() = state.testFramework - - val mockStrategy: MockStrategyApi get() = state.mockStrategy - - val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour get() = state.runtimeExceptionTestsBehaviour - - var hangingTestsTimeout: HangingTestsTimeout - get() = state.hangingTestsTimeout - set(value) { - state.hangingTestsTimeout = value - } - - val staticsMocking: StaticsMocking get() = state.staticsMocking - - val forceStaticMocking: ForceStaticMocking get() = state.forceStaticMocking - - val treatOverflowAsError: TreatOverflowAsError get() = state.treatOverflowAsError - - val parametrizedTestSource: ParametrizedTestSource get() = state.parametrizedTestSource - - val classesToMockAlways: Set get() = state.classesToMockAlways.toSet() - - val javaDocCommentStyle: JavaDocCommentStyle get() = state.commentStyle - - var fuzzingValue: Double - get() = state.fuzzingValue - set(value) { - state.fuzzingValue = value.coerceIn(0.0, 1.0) - } - var runGeneratedTestsWithCoverage = state.runGeneratedTestsWithCoverage - - fun setClassesToMockAlways(classesToMockAlways: List) { - state.classesToMockAlways = classesToMockAlways.distinct().toTypedArray() - } - - override fun getState(): State = state - - override fun initializeComponent() { - super.initializeComponent() - CompletableFuture.runAsync { FileUtil.clearTempDirectory(UtSettings.daysLimitForTempFiles) } - } - - override fun loadState(state: State) { - this.state = state - } - - fun loadStateFromModel(model: GenerateTestsModel) { - loadState(State(model)) - } - - // these classes are all ref types so we can use only names here - fun chosenClassesToMockAlways(): Set = state.classesToMockAlways.mapTo(mutableSetOf()) { ClassId(it) } - - fun setProviderByLoader(loader: KClass<*>, provider: CodeGenerationSettingItem) = - when (loader) { - // TODO: service loaders for test generator and code generator are removed from settings temporarily -// TestGeneratorServiceLoader::class -> setGeneratorName(provider) -// CodeGeneratorServiceLoader::class -> setCodeGeneratorName(provider) - MockStrategyApi::class -> state.mockStrategy = provider as MockStrategyApi - CodegenLanguage::class -> state.codegenLanguage = provider as CodegenLanguage - RuntimeExceptionTestsBehaviour::class -> { - state.runtimeExceptionTestsBehaviour = provider as RuntimeExceptionTestsBehaviour - } - ForceStaticMocking::class -> state.forceStaticMocking = provider as ForceStaticMocking - TreatOverflowAsError::class -> { - // TODO: SAT-1566 - state.treatOverflowAsError = provider as TreatOverflowAsError - UtSettings.treatOverflowAsError = provider == TreatOverflowAsError.AS_ERROR - } - JavaDocCommentStyle::class -> state.commentStyle = provider as JavaDocCommentStyle - // TODO: add error processing - else -> error("Unknown class [$loader] to map value [$provider]") - } - - fun providerNameByServiceLoader(loader: KClass<*>): CodeGenerationSettingItem = - when (loader) { - // TODO: service loaders for test generator and code generator are removed from settings temporarily -// TestGeneratorServiceLoader::class -> generatorName -// CodeGeneratorServiceLoader::class -> codeGeneratorName - MockStrategyApi::class -> mockStrategy - CodegenLanguage::class -> codegenLanguage - RuntimeExceptionTestsBehaviour::class -> runtimeExceptionTestsBehaviour - ForceStaticMocking::class -> forceStaticMocking - TreatOverflowAsError::class -> treatOverflowAsError - JavaDocCommentStyle::class -> javaDocCommentStyle - // TODO: add error processing - else -> error("Unknown service loader: $loader") - } +fun loadStateFromModel(settings: Settings, model: GenerateTestsModel) { + settings.loadState(fromGenerateTestsModel(model)) } -// use it to serialize testFramework in State -private class TestFrameworkConverter : Converter() { - override fun toString(value: TestFramework): String = "$value" - - override fun fromString(value: String): TestFramework = when (value) { - Junit4.id -> Junit4 - Junit5.id -> Junit5 - TestNg.id -> TestNg - else -> error("Unknown TestFramework $value") - } -} - -// use it to serialize staticsMocking in State -private class StaticsMockingConverter : Converter() { - override fun toString(value: StaticsMocking): String = "$value" - - override fun fromString(value: String): StaticsMocking = when (value) { - NoStaticMocking.id -> NoStaticMocking - MockitoStaticMocking.id -> MockitoStaticMocking - else -> error("Unknown StaticsMocking $value") - } -} - -// TODO is it better to use kotlinx.serialization? -// use it to serialize hangingTestsTimeout in State -private class HangingTestsTimeoutConverter : Converter() { - override fun toString(value: HangingTestsTimeout): String = - "HangingTestsTimeout:${value.timeoutMs}" - - override fun fromString(value: String): HangingTestsTimeout { - val arguments = value.substringAfter("HangingTestsTimeout:") - val timeoutMs = arguments.toLong() - return HangingTestsTimeout(timeoutMs) - } -} +private fun fromGenerateTestsModel(model: GenerateTestsModel): Settings.State { + return Settings.State( + sourceRootHistory = model.sourceRootHistory, + codegenLanguage = model.codegenLanguage, + testFramework = model.testFramework, + mockStrategy = model.mockStrategy, + mockFramework = model.mockFramework ?: MockFramework.defaultItem, + staticsMocking = model.staticsMocking, + runtimeExceptionTestsBehaviour = model.runtimeExceptionTestsBehaviour, + hangingTestsTimeout = model.hangingTestsTimeout, + useTaintAnalysis = model.useTaintAnalysis, + runInspectionAfterTestGeneration = model.runInspectionAfterTestGeneration, + forceStaticMocking = model.forceStaticMocking, + parametrizedTestSource = model.parametrizedTestSource, + classesToMockAlways = model.chosenClassesToMockAlways.mapTo(mutableSetOf()) { it.name }.toTypedArray(), + springTestType = model.springTestType, + springConfig = model.springConfig, + springProfileNames = model.springProfileNames, + fuzzingValue = model.fuzzingValue, + runGeneratedTestsWithCoverage = model.runGeneratedTestsWithCoverage, + commentStyle = model.commentStyle, + generationTimeoutInMillis = model.timeout, + summariesGenerationType = model.summariesGenerationType + ) +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/SettingsWindow.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/SettingsWindow.kt index 52649656e2..6b268a71d4 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/SettingsWindow.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/SettingsWindow.kt @@ -2,104 +2,128 @@ package org.utbot.intellij.plugin.settings import com.intellij.openapi.components.service import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogPanel import com.intellij.ui.ContextHelpLabel import com.intellij.ui.components.JBLabel -import com.intellij.ui.layout.CCFlags -import com.intellij.ui.layout.LayoutBuilder -import com.intellij.ui.layout.PropertyBinding -import com.intellij.ui.layout.labelTable -import com.intellij.ui.layout.panel -import com.intellij.ui.layout.slider -import com.intellij.ui.layout.withValueBinding -import com.intellij.util.castSafelyTo +import com.intellij.ui.dsl.builder.* +import com.intellij.ui.layout.selected +import com.intellij.ui.layout.selectedValueMatches import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel -import javax.swing.DefaultComboBoxModel -import javax.swing.JCheckBox -import javax.swing.JPanel +import javax.swing.* import kotlin.reflect.KClass +import org.utbot.framework.SummariesGenerationType import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.HangingTestsTimeout -import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.ForceStaticMocking import org.utbot.framework.plugin.api.CodeGenerationSettingItem import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.JavaDocCommentStyle import org.utbot.framework.plugin.api.TreatOverflowAsError +import org.utbot.framework.plugin.api.isSummarizationCompatible import org.utbot.intellij.plugin.ui.components.CodeGenerationSettingItemRenderer -import javax.swing.JSlider +import org.utbot.intellij.plugin.util.showSettingsEditor class SettingsWindow(val project: Project) { private val settings = project.service() // TODO it is better to use something like SearchEverywhere for classes but it is complicated to implement + private lateinit var codegenLanguageCombo: ComboBox private val excludeTable = MockAlwaysClassesTable(project) + private lateinit var useTaintAnalysisCheckBox: JCheckBox + private lateinit var runInspectionAfterTestGenerationCheckBox: JCheckBox private lateinit var forceMockCheckBox: JCheckBox + private lateinit var enableSummarizationGenerationCheckBox: JCheckBox + + private fun Row.createCombo(loader: KClass<*>, values: Array<*>) { + comboBox(DefaultComboBoxModel(values)) + .bindItem( + getter = { settings.providerNameByServiceLoader(loader) }, + setter = { settings.setProviderByLoader(loader, it as CodeGenerationSettingItem) }, + ).component.renderer = CodeGenerationSettingItemRenderer() + } val panel: JPanel = panel { - val valuesComboBox: LayoutBuilder.(KClass<*>, Array<*>) -> Unit = { loader, values -> - val serviceLabels = mapOf( - CodegenLanguage::class to "Generated test language:", - RuntimeExceptionTestsBehaviour::class to "Tests with exceptions:", - TreatOverflowAsError::class to "Overflow detection:", - JavaDocCommentStyle::class to "Javadoc comment style:" - ) - val tooltipLabels = mapOf( - CodegenLanguage::class to "You can generate test methods in Java or Kotlin regardless of your source code language." - ) - - row(serviceLabels[loader] ?: error("Unknown service loader: $loader")) { - cell { - comboBox( - DefaultComboBoxModel(values), - getter = { settings.providerNameByServiceLoader(loader) }, - setter = { settings.setProviderByLoader(loader, it as CodeGenerationSettingItem) }, - ).apply { + group("Java Specific Settings") { + row("Generated test language:") { + codegenLanguageCombo = comboBox(DefaultComboBoxModel(CodegenLanguage.values())).gap(RightGap.COLUMNS) + .apply { component.renderer = CodeGenerationSettingItemRenderer() - ContextHelpLabel.create(tooltipLabels[loader] ?: return@apply)() + ContextHelpLabel.create("You can generate test methods in Java or Kotlin regardless of your source code language.") + }.bindItem( + getter = { settings.providerNameByServiceLoader(CodegenLanguage::class) as CodegenLanguage }, + setter = { + settings.setProviderByLoader( + CodegenLanguage::class, + it as CodeGenerationSettingItem + ) + } + ).component + codegenLanguageCombo.addActionListener { + if (!codegenLanguageCombo.item.isSummarizationCompatible()) { + enableSummarizationGenerationCheckBox.isSelected = false } } } - } - valuesComboBox(CodegenLanguage::class, CodegenLanguage.values()) - - row("Hanging test timeout:") { - cell { - spinner( - getter = { - settings.hangingTestsTimeout.timeoutMs - .coerceIn(HangingTestsTimeout.MIN_TIMEOUT_MS, HangingTestsTimeout.MAX_TIMEOUT_MS).toInt() - }, - setter = { - settings.hangingTestsTimeout = HangingTestsTimeout(it.toLong()) - }, - minValue = HangingTestsTimeout.MIN_TIMEOUT_MS.toInt(), - maxValue = HangingTestsTimeout.MAX_TIMEOUT_MS.toInt(), - step = 50, - ) + row("Overflow detection:") { + createCombo(TreatOverflowAsError::class, TreatOverflowAsError.values()) + } - label("milliseconds per method") - .apply { - ContextHelpLabel.create( - "Set this timeout to define which test is \"hanging\". Increase it to test the " + - "time-consuming method or decrease if the execution speed is critical for you." - )() + row { + useTaintAnalysisCheckBox = + checkBox("Enable taint analysis") + .onApply { + settings.state.useTaintAnalysis = useTaintAnalysisCheckBox.isSelected + } + .onReset { + useTaintAnalysisCheckBox.isSelected = settings.state.useTaintAnalysis + } + .onIsModified { + useTaintAnalysisCheckBox.isSelected xor settings.state.useTaintAnalysis + } + .component + contextHelp("Experimental taint analysis support") + } + row { + runInspectionAfterTestGenerationCheckBox = + checkBox("Display detected errors on the Problems tool window") + .onApply { + settings.state.runInspectionAfterTestGeneration = + runInspectionAfterTestGenerationCheckBox.isSelected + } + .onReset { + runInspectionAfterTestGenerationCheckBox.isSelected = + settings.state.runInspectionAfterTestGeneration + } + .onIsModified { + runInspectionAfterTestGenerationCheckBox.isSelected xor settings.state.runInspectionAfterTestGeneration + } + .component + contextHelp("Automatically run code inspection after test generation") + } + row { + enableSummarizationGenerationCheckBox = checkBox("Enable summaries generation") + .onApply { + settings.state.summariesGenerationType = + if (enableSummarizationGenerationCheckBox.isSelected) SummariesGenerationType.FULL else SummariesGenerationType.NONE } + .onReset { + enableSummarizationGenerationCheckBox.isSelected = + settings.state.summariesGenerationType != SummariesGenerationType.NONE + } + .onIsModified { + enableSummarizationGenerationCheckBox.isSelected xor (settings.state.summariesGenerationType != SummariesGenerationType.NONE) + }.enabledIf(codegenLanguageCombo.selectedValueMatches(CodegenLanguage?::isSummarizationCompatible)) + .component + } + indent { + row("Javadoc comment style:") { + createCombo(JavaDocCommentStyle::class, JavaDocCommentStyle.values()) + }.enabledIf(enableSummarizationGenerationCheckBox.selected).bottomGap(BottomGap.MEDIUM) } - } - - mapOf( - RuntimeExceptionTestsBehaviour::class to RuntimeExceptionTestsBehaviour.values(), - TreatOverflowAsError::class to TreatOverflowAsError.values(), - JavaDocCommentStyle::class to JavaDocCommentStyle.values() - ).forEach { (loader, values) -> - valuesComboBox(loader, values) - } - row { - cell { + row { forceMockCheckBox = checkBox("Force mocking static methods") .onApply { settings.state.forceStaticMocking = @@ -107,57 +131,79 @@ class SettingsWindow(val project: Project) { } .onReset { forceMockCheckBox.isSelected = settings.forceStaticMocking == ForceStaticMocking.FORCE } .onIsModified { forceMockCheckBox.isSelected xor (settings.forceStaticMocking != ForceStaticMocking.DO_NOT_FORCE) } - .apply { ContextHelpLabel.create("Overrides other mocking settings")() } .component + contextHelp("Overrides other mocking settings") } - } - - row("Classes to be forcedly mocked:") {} - row { - val excludeTableCellBuilder = excludeTable.component(CCFlags.grow) - val updater = Runnable { - UIUtil.setEnabled(excludeTableCellBuilder.component, forceMockCheckBox.isSelected, true) - } - excludeTableCellBuilder - .onApply { excludeTable.apply() } - .onReset { - excludeTable.reset() - updater.run() + row("Classes to be forcedly mocked:") {} + row { + val updater = Runnable { + UIUtil.setEnabled(excludeTable.component, forceMockCheckBox.isSelected, true) } - .onIsModified { excludeTable.isModified() } - forceMockCheckBox.addActionListener { updater.run() } - - - } - - val fuzzLabel = JBLabel("Fuzzing") - val symLabel = JBLabel("Symbolic execution") - row("Test generation method:") { - enabled = UtSettings.useFuzzing - val granularity = 20 - slider(0, granularity, 1, granularity / 4) - .labelTable { - // clear all labels - }.withValueBinding( - PropertyBinding( - get = { ((1 - settings.fuzzingValue) * granularity).toInt() }, - set = { settings.fuzzingValue = 1 - it / granularity.toDouble() } - ) - ) - .constraints(CCFlags.growX) - .component.castSafelyTo()?.apply { - toolTipText = "While fuzzer \"guesses\" the values to enter as much execution paths as possible, symbolic executor tries to \"deduce\" them. Choose the proportion of generation time allocated for each of these methods within Test generation timeout" - addChangeListener { - fuzzLabel.text = "Fuzzing " + "%.0f %%".format(100.0 * (granularity - value) / granularity) - symLabel.text = "%.0f %%".format(100.0 * value / granularity) + " Symbolic execution" + cell(excludeTable.component) + .align(Align.FILL) + .onApply { excludeTable.apply() } + .onReset { + excludeTable.reset() + updater.run() + } + .onIsModified { excludeTable.isModified() } + + forceMockCheckBox.addActionListener { updater.run() } + }.bottomGap(BottomGap.MEDIUM) + + val fuzzLabel = JBLabel("Fuzzing") + val symLabel = JBLabel("Symbolic execution") + row { + cell(BorderLayoutPanel().apply { + topGap(TopGap.SMALL) + addToLeft(JBLabel("Test generation method:").apply { verticalAlignment = SwingConstants.TOP }) + addToCenter(BorderLayoutPanel().apply { + val granularity = 20 + val slider = object : JSlider() { + val updater = Runnable() { + val fuzzingPercent = 100.0 * (granularity - value) / granularity + fuzzLabel.text = "Fuzzing %.0f %%".format(fuzzingPercent) + symLabel.text = "%.0f %% Symbolic execution".format(100.0 - fuzzingPercent) + } + + override fun getValue() = ((1 - settings.fuzzingValue) * granularity).toInt() + + override fun setValue(n: Int) { + val tmp = value + settings.fuzzingValue = 1 - n / granularity.toDouble() + if (tmp != n) { + updater.run() + } + } + } + UIUtil.setSliderIsFilled(slider, true) + slider.minimum = 0 + slider.maximum = granularity + slider.minorTickSpacing = 1 + slider.majorTickSpacing = granularity / 4 + slider.paintTicks = true + slider.paintTrack = true + slider.paintLabels = false + slider.toolTipText = + "While fuzzer \"guesses\" the values to enter as much execution paths as possible, symbolic executor tries to \"deduce\" them. Choose the proportion of generation time allocated for each of these methods within Test generation timeout. The slide has no effect for Spring Projects." + slider.updater.run() + addToTop(slider) + addToBottom(BorderLayoutPanel().apply { + addToLeft(fuzzLabel) + addToRight(symLabel) + }) + }) + }).align(Align.FILL) + }.enabled(UtSettings.useFuzzing) + if (!UtSettings.useFuzzing) { + row { + comment("Fuzzing is disabled in configuration file.") + link("Edit configuration") { + UIUtil.getWindow(fuzzLabel)?.dispose() + showSettingsEditor(project, "useFuzzing") } } - } - row("") { - BorderLayoutPanel().apply { - addToLeft(fuzzLabel) - addToRight(symLabel) - }().constraints(CCFlags.growX) + } } } 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 779261d3c2..760046ef60 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 @@ -4,15 +4,14 @@ package org.utbot.intellij.plugin.ui import com.intellij.codeInsight.hint.HintUtil import com.intellij.icons.AllIcons -import com.intellij.ide.impl.ProjectNewWindowDoNotAskOption import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.service import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.projectRoots.JavaSdkVersion -import com.intellij.openapi.roots.ContentEntry import com.intellij.openapi.roots.DependencyScope import com.intellij.openapi.roots.ExternalLibraryDescriptor import com.intellij.openapi.roots.JavaProjectModelModificationService @@ -30,7 +29,9 @@ import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.OptionAction import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.ui.popup.IconButton +import com.intellij.openapi.ui.popup.ListSeparator import com.intellij.openapi.util.Computable +import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtilCore.urlToPath @@ -45,7 +46,7 @@ import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo import com.intellij.refactoring.util.RefactoringUtil import com.intellij.refactoring.util.classMembers.MemberInfo import com.intellij.ui.ColoredListCellRenderer -import com.intellij.ui.ContextHelpLabel +import com.intellij.ui.GroupHeaderSeparator import com.intellij.ui.HyperlinkLabel import com.intellij.ui.IdeBorderFactory.createBorder import com.intellij.ui.InplaceButton @@ -53,18 +54,18 @@ import com.intellij.ui.JBColor import com.intellij.ui.JBIntSpinner import com.intellij.ui.SideBorder import com.intellij.ui.SimpleTextAttributes +import org.utbot.framework.plugin.api.SpringSettings.* import com.intellij.ui.components.CheckBox import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.Panel +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.JBTextField import com.intellij.ui.components.panels.HorizontalLayout import com.intellij.ui.components.panels.NonOpaquePanel -import com.intellij.ui.layout.Cell -import com.intellij.ui.layout.CellBuilder -import com.intellij.ui.layout.Row -import com.intellij.ui.layout.panel +import com.intellij.ui.components.panels.OpaquePanel +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.layout.ComboBoxPredicate 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 @@ -72,49 +73,37 @@ 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 java.awt.BorderLayout -import java.awt.Color -import java.awt.Dimension -import java.awt.event.ActionEvent -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.text.ParseException -import java.util.Objects -import java.util.concurrent.TimeUnit -import javax.swing.AbstractAction -import javax.swing.Action -import javax.swing.DefaultComboBoxModel -import javax.swing.JButton -import javax.swing.JCheckBox -import javax.swing.JComboBox -import javax.swing.JComponent -import javax.swing.JList -import javax.swing.JPanel -import javax.swing.JSpinner -import javax.swing.text.DefaultFormatter +import mu.KotlinLogging import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.thenRun 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.MockitoStaticMocking -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_FOLDER -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.codegen.domain.SpringModule +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.MockitoStaticMocking +import org.utbot.framework.codegen.domain.NoStaticMocking +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.SpringModule.* +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.TestNg import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.TreatOverflowAsError +import org.utbot.framework.plugin.api.MockFramework.MOCKITO +import org.utbot.framework.plugin.api.SpringTestType +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.CodeGenerationSettingItem +import org.utbot.framework.plugin.api.SpringConfiguration +import org.utbot.framework.plugin.api.SpringTestType.* +import org.utbot.framework.plugin.api.SpringProfileNames +import org.utbot.framework.plugin.api.utils.MOCKITO_EXTENSIONS_FILE_CONTENT +import org.utbot.framework.plugin.api.utils.MOCKITO_EXTENSIONS_FOLDER +import org.utbot.framework.plugin.api.utils.MOCKITO_MOCKMAKER_FILE_NAME +import org.utbot.framework.plugin.api.NO_SPRING_CONFIGURATION_OPTION import org.utbot.framework.util.Conflict import org.utbot.intellij.plugin.models.GenerateTestsModel import org.utbot.intellij.plugin.models.id @@ -123,31 +112,60 @@ import org.utbot.intellij.plugin.models.jUnit5LibraryDescriptor import org.utbot.intellij.plugin.models.jUnit5ParametrizedTestsLibraryDescriptor import org.utbot.intellij.plugin.models.mockitoCoreLibraryDescriptor import org.utbot.intellij.plugin.models.packageName -import org.utbot.intellij.plugin.models.testNgLibraryDescriptor +import org.utbot.intellij.plugin.models.springBootTestLibraryDescriptor +import org.utbot.intellij.plugin.models.springSecurityLibraryDescriptor +import org.utbot.intellij.plugin.models.springTestLibraryDescriptor +import org.utbot.intellij.plugin.models.testNgNewLibraryDescriptor +import org.utbot.intellij.plugin.models.testNgOldLibraryDescriptor +import org.utbot.intellij.plugin.settings.JavaTestFrameworkMapper import org.utbot.intellij.plugin.settings.Settings +import org.utbot.intellij.plugin.settings.loadStateFromModel import org.utbot.intellij.plugin.ui.components.CodeGenerationSettingItemRenderer 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.allLibraries +import org.utbot.intellij.plugin.ui.utils.createTestFrameworksRenderer +import org.utbot.intellij.plugin.ui.utils.findDependencyInjectionLibrary +import org.utbot.intellij.plugin.ui.utils.findDependencyInjectionTestLibrary import org.utbot.intellij.plugin.ui.utils.findFrameworkLibrary import org.utbot.intellij.plugin.ui.utils.findParametrizedTestsLibrary import org.utbot.intellij.plugin.ui.utils.getOrCreateTestResourcesPath import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle -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.IntelliJApiHelper -import org.utbot.intellij.plugin.util.extractFirstLevelMembers +import org.utbot.intellij.plugin.util.* +import java.awt.BorderLayout +import java.awt.Color +import java.awt.Component +import java.awt.Dimension +import java.awt.event.ActionEvent +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.text.ParseException +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.concurrent.TimeUnit +import javax.swing.AbstractAction +import javax.swing.Action +import javax.swing.DefaultComboBoxModel +import javax.swing.JButton +import javax.swing.JCheckBox +import javax.swing.JComboBox +import javax.swing.JComponent +import javax.swing.JList +import javax.swing.JSpinner +import javax.swing.text.DefaultFormatter +import kotlin.io.path.notExists + private const val RECENTS_KEY = "org.utbot.recents" private const val SAME_PACKAGE_LABEL = "same as for sources" private const val WILL_BE_INSTALLED_LABEL = " (will be installed)" -private const val WILL_BE_CONFIGURED_LABEL = " (will be configured)" -private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 1 private const val ACTION_GENERATE = "Generate Tests" private const val ACTION_GENERATE_AND_RUN = "Generate and Run" @@ -158,6 +176,8 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m const val maxSupportedSdkVersion = 17 } + private val logger = KotlinLogging.logger {} + private val membersTable = MemberSelectionTable(emptyList(), null) private val cbSpecifyTestPackage = CheckBox("Specify destination package", false) @@ -165,22 +185,26 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m findTestPackageComboValue(), model.project, RECENTS_KEY, - "Choose destination package" + "Choose Destination Package" ) private val testSourceFolderField = TestFolderComboWithBrowseButton(model) private val codegenLanguages = createComboBox(CodegenLanguage.values()) private val testFrameworks = createComboBox(TestFramework.allItems.toTypedArray()) + + private val javaConfigurationHelper = SpringConfigurationsHelper(SpringConfigurationType.ClassConfiguration) + private val xmlConfigurationHelper = SpringConfigurationsHelper(SpringConfigurationType.FileConfiguration) + private val mockStrategies = createComboBox(MockStrategyApi.values()) private val staticsMocking = JCheckBox("Mock static methods") + + private val springTestType = createComboBox(SpringTestType.values()).also { it.setMinimumAndPreferredWidth(300) } + private val springConfig = createComboBoxWithSeparatorsForSpringConfigs(shortenConfigurationNames()) + private val springProfileNames = JBTextField(23).apply { emptyText.text = SpringProfileNames.defaultItem } + private val timeoutSpinner = - JBIntSpinner( - TimeUnit.MILLISECONDS.toSeconds(UtSettings.utBotGenerationTimeoutInMillis).toInt(), - MINIMUM_TIMEOUT_VALUE_IN_SECONDS, - Int.MAX_VALUE, - MINIMUM_TIMEOUT_VALUE_IN_SECONDS - ).also { + JBIntSpinner(TimeUnit.MILLISECONDS.toSeconds(model.timeout).toInt(), 1, Int.MAX_VALUE, 1).also { when(val editor = it.editor) { is JSpinner.DefaultEditor -> { when(val formatter = editor.textField.formatter) { @@ -201,10 +225,34 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m parametrizedTestSources to null ) + private fun shortenConfigurationNames(): Set>> { + val springBootApplicationClasses = model.getSortedSpringBootApplicationClasses() + val configurationClasses = model.getSortedSpringConfigurationClasses() + val xmlConfigurationFiles = model.getSpringXMLConfigurationFiles() + + val shortenedJavaConfigurationClasses = + javaConfigurationHelper.shortenSpringConfigNames(springBootApplicationClasses + configurationClasses) + + val shortenedSpringXMLConfigurationFiles = + xmlConfigurationHelper.shortenSpringConfigNames(xmlConfigurationFiles) + + return setOf( + null to listOf(NO_SPRING_CONFIGURATION_OPTION), + "@SpringBootApplication" to springBootApplicationClasses.map(shortenedJavaConfigurationClasses::getValue), + "@Configuration" to configurationClasses.map(shortenedJavaConfigurationClasses::getValue), + "XML configuration" to xmlConfigurationFiles.map(shortenedSpringXMLConfigurationFiles::getValue) + ) + } + + private fun shortenConfigurationNameByFullname(fullname: String): String? { + val allShortenConfigurationNames = shortenConfigurationNames().flatMap { it.second } + return allShortenConfigurationNames.firstOrNull { fullname.endsWith(it) } + } + private fun createComboBox(values: Array) : ComboBox { val comboBox = object:ComboBox(DefaultComboBoxModel(values)) { var maxWidth = 0 - //Don't shrink strategy + // Do not shrink strategy override fun getPreferredSize(): Dimension { val size = super.getPreferredSize() if (size.width > maxWidth) maxWidth = size.width @@ -216,6 +264,87 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } } + private fun createComboBoxWithSeparatorsForSpringConfigs( + separatorToValues: Collection>>, + width: Int = 300 + ): ComboBox { + val comboBox = object : ComboBox() { + override fun setSelectedItem(anObject: Any?) { + if (anObject !is ListSeparator) { + super.setSelectedItem(anObject) + } + } + }.apply { + isSwingPopup = false + renderer = MyListCellRenderer() + + setMinimumAndPreferredWidth(width) + separatorToValues.forEach { (separator, values) -> + if (values.isEmpty()) return@forEach + separator?.let { + addItem(ListSeparator(it)) + } + values.forEach(::addItem) + } + } + + return comboBox + } + + private class MyListCellRenderer: ColoredListCellRenderer() { + private val separatorRenderer = SeparatorRenderer() + override fun getListCellRendererComponent( + list: JList?, + value: Any?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ): Component { + return when (value) { + is ListSeparator -> { + separatorRenderer.init(value.text, index < 0) + } + else -> { + super.getListCellRendererComponent(list, value, index, selected, hasFocus) + } + } + } + + override fun customizeCellRenderer( + list: JList, + value: Any?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + append(java.lang.String.valueOf(value)) + } + } + + class SeparatorRenderer : OpaquePanel() { + private val separator = GroupHeaderSeparator(JBUI.insets(3, 8, 1, 0)) + private var emptyPreferredHeight = false + + init { + layout = BorderLayout() + add(separator) + } + + fun init(@NlsContexts.Separator caption: String, emptyPreferredHeight: Boolean) : SeparatorRenderer { + separator.caption = caption + this.emptyPreferredHeight = emptyPreferredHeight + return this + } + + override fun getPreferredSize(): Dimension { + return super.getPreferredSize().apply { + if (emptyPreferredHeight) { + height = 0 + } + } + } + } + private fun createHelpLabel(commonTooltip: String? = null) = JBLabel(AllIcons.General.ContextHelp).apply { if (!commonTooltip.isNullOrEmpty()) toolTipText = commonTooltip } @@ -225,16 +354,31 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m setResizable(false) TestFramework.allItems.forEach { - it.isInstalled = findFrameworkLibrary(model.project, model.testModule, it) != null - it.isParametrizedTestsConfigured = findParametrizedTestsLibrary(model.project, model.testModule, it) != null + it.isInstalled = findFrameworkLibrary(model.testModule, it) != null + it.isParametrizedTestsConfigured = findParametrizedTestsLibrary(model.testModule, it) != null } MockFramework.allItems.forEach { - it.isInstalled = findFrameworkLibrary(model.project, model.testModule, it) != null + it.isInstalled = findFrameworkLibrary(model.testModule, it) != null } StaticsMocking.allItems.forEach { it.isConfigured = staticsMockingConfigured() } + + SpringModule.values().forEach { + it.isInstalled = findDependencyInjectionLibrary(model.srcModule, it) != null + } + SpringModule.installedItems.forEach { + it.testFrameworkInstalled = findDependencyInjectionTestLibrary(model.testModule, it) != null + } + + val isUtBotSpringRuntimePresent = this::class.java.classLoader.getResource("lib/utbot-spring-analyzer-shadow.jar") != null + + model.projectType = + // TODO show some warning, when we see Spring project, but don't have `utBotSpringRuntime` + if (isUtBotSpringRuntimePresent && SpringModule.installedItems.isNotEmpty()) ProjectType.Spring + else ProjectType.PureJvm + // Configure notification urls callbacks TestsReportNotifier.urlOpeningListener.callbacks[TestReportUrlOpeningListener.mockitoSuffix]?.plusAssign { configureMockFramework() @@ -262,69 +406,99 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m override fun createCenterPanel(): JComponent { panel = panel { row("Test sources root:") { - component(testSourceFolderField) + cell(testSourceFolderField).align(Align.FILL) } row("Testing framework:") { - makePanelWithHelpTooltip( - testFrameworks, - null + cell(testFrameworks) + } + + if (model.projectType == ProjectType.Spring) { + row("Spring configuration:") { + cell(springConfig) + contextHelp( + "100% Symbolic execution mode.
    " + + "Classes defined in Spring configuration will be used instead " + + "of interfaces and abstract classes.
    " + + "Mocks will be used when necessary." + ) + } + row("Test type:") { + cell(springTestType) + contextHelp( + "Unit tests do not initialize ApplicationContext
    " + + "and do not autowire beans, while integration tests do." + ) + }.enabledIf( + ComboBoxPredicate(springConfig) { isSpringConfigSelected() && !isXmlSpringConfigUsed() } + ) + row("Active profile(s):") { + cell(springProfileNames) + contextHelp( + "One or several comma-separated names.
    " + + "If all names are incorrect, default profile is used" + ) + }.enabledIf( + ComboBoxPredicate(springConfig) { isSpringConfigSelected() } ) } - row { component(parametrizedTestSources) } + row("Mocking strategy:") { - makePanelWithHelpTooltip( - mockStrategies, - ContextHelpLabel.create("Mock everything around the target class or the whole package except the system classes. " + - "Otherwise, mock nothing. Mockito will be installed, if you don't have one.") + cell(mockStrategies) + contextHelp( + "Mock everything around the target class or the whole package except the system classes.
    " + + "Otherwise, mock nothing. Mockito will be installed, if you don't have one." ) - } - row { component(staticsMocking)} + }.enabledIf(ComboBoxPredicate(springConfig) { + model.projectType == ProjectType.PureJvm || !isSpringConfigSelected() + }) + row { cell(staticsMocking)} + row { + cell(parametrizedTestSources) + contextHelp("Parametrization is not supported in some configurations, e.g. if mocks are used.") + }.enabledIf(ComboBoxPredicate(springConfig) { + model.projectType == ProjectType.PureJvm + }) row("Test generation timeout:") { - cell { - component(timeoutSpinner) - label("seconds per class") - component(ContextHelpLabel.create("Set the timeout for all test generation processes per class to complete.")) - } + cell(BorderLayoutPanel().apply { + addToLeft(timeoutSpinner) + addToRight(JBLabel("seconds per class")) + }) + contextHelp("Set the timeout for all test generation processes per class to complete.") } row("Generate tests for:") {} row { - scrollPane(membersTable) + cell(JBScrollPane(membersTable)).align(Align.FILL) } } initDefaultValues() setListeners() updateMembersTable() + initValidation() return panel } - private inline fun Cell.panelWithHelpTooltip(tooltipText: String?, crossinline init: Cell.() -> Unit): Cell { - init() - tooltipText?.let { component(ContextHelpLabel.create(it)) } - return this + // TODO:SAT-1571 investigate Android Studio specific sdk issues + fun isSdkSupported() : Boolean = + findSdkVersion(model.srcModule).feature in minSupportedSdkVersion..maxSupportedSdkVersion + || IntelliJApiHelper.isAndroidStudio() + + override fun setOKActionEnabled(isEnabled: Boolean) { + super.setOKActionEnabled(isEnabled) + getButton(okAction)?.apply { + UIUtil.setEnabled(this, isEnabled, true) + okOptionAction?.isEnabled = isEnabled + okOptionAction?.options?.forEach { it.isEnabled = isEnabled } + } } - private fun Row.makePanelWithHelpTooltip( - mainComponent: JComponent, - label: JBLabel? - ): CellBuilder = - component(Panel().apply { - add(mainComponent, BorderLayout.LINE_START) - label?.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? = if (isSdkSupported()) null else SdkNotificationPanel(model) - override fun createTitlePane(): JComponent? { - val sdkVersion = findSdkVersion() - //TODO:SAT-1571 investigate Android Studio specific sdk issues - if (sdkVersion?.feature in minSupportedSdkVersion..maxSupportedSdkVersion || IntelliJApiHelper.isAndroidStudio()) return null - isOKActionEnabled = false - return SdkNotificationPanel(model, sdkVersion) + override fun createSouthPanel(): JComponent { + val southPanel = super.createSouthPanel() + if (!isSdkSupported()) isOKActionEnabled = false + return southPanel } private fun findTestPackageComboValue(): String { @@ -340,19 +514,20 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m * * Note: this implementation was encouraged by NonModalCommitPromoter. */ - private inner class SdkNotificationPanel( - private val model: GenerateTestsModel, - private val sdkVersion: JavaVersion?, - ) : BorderLayoutPanel() { + private inner class SdkNotificationPanel(private val model: GenerateTestsModel) : + BorderLayoutPanel(scale(UIUtil.DEFAULT_HGAP), 0) { init { border = merge(empty(10), createBorder(JBColor.border(), SideBorder.BOTTOM), true) - addToLeft(JBLabel().apply { + addToCenter(JBLabel().apply { icon = AllIcons.Ide.FatalError - text = if (sdkVersion != null) { - "SDK version $sdkVersion is not supported, use ${JavaSdkVersion.JDK_1_8}, ${JavaSdkVersion.JDK_11} or ${JavaSdkVersion.JDK_17}" - } else { - "SDK is not defined" + text = run { + val sdkVersion = findSdkVersionOrNull(this@GenerateTestsDialogWindow.model.srcModule)?.feature + if (sdkVersion != null) { + "SDK version $sdkVersion is not supported, use ${JavaSdkVersion.JDK_1_8.toReadableString()}, ${JavaSdkVersion.JDK_11.toReadableString()} or ${JavaSdkVersion.JDK_17.toReadableString()} instead." + } else { + "SDK is not defined" + } } }) @@ -362,6 +537,8 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m }) } + fun JavaSdkVersion.toReadableString() : String = toString().replace("JDK_", "").replace('_', '.') + override fun getBackground(): Color? = EditorColorsManager.getInstance().globalScheme.getColor(HintUtil.ERROR_COLOR_KEY) ?: super.getBackground() @@ -372,11 +549,11 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m val isEdited = ShowSettingsUtil.getInstance().editConfigurable(model.project, projectStructure) { projectStructure.select(model.srcModule.name, ClasspathEditor.getName(), true) } - val sdkVersion = findSdkVersion() - val sdkFixed = isEdited && sdkVersion?.feature in minSupportedSdkVersion..maxSupportedSdkVersion + val sdkVersion = findSdkVersion(model.srcModule) + val sdkFixed = isEdited && sdkVersion.feature in minSupportedSdkVersion..maxSupportedSdkVersion if (sdkFixed) { this@SdkNotificationPanel.isVisible = false - isOKActionEnabled = true + initValidation() } } } @@ -394,13 +571,13 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m srcClasses.flatMap { it.extractFirstLevelMembers(false) } } else { srcClasses.map { MemberInfo(it) } - }.toSortedSet { o1, o2 -> o1.displayName.compareTo(o2.displayName, true) } + }.toMutableList().sortedWith { o1, o2 -> o1.displayName.compareTo(o2.displayName, true) } checkMembers(items) membersTable.setMemberInfos(items) if (items.isEmpty()) isOKActionEnabled = false - // fix issue with MemberSelectionTable height, set it directly. + // Fix issue with MemberSelectionTable height, set it directly. // Use row height times methods (12 max) plus one more for header val height = membersTable.rowHeight * (items.size.coerceAtMost(12) + 1) membersTable.preferredScrollableViewportSize = size(-1, height) @@ -421,12 +598,14 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m return null } + private fun VirtualFile.toRealFile():VirtualFile = if (this is FakeVirtualFile) this.parent else this + override fun doValidate(): ValidationInfo? { val testRoot = getTestRoot() ?: return ValidationInfo("Test source root is not configured", testSourceFolderField.childComponent) - if (!model.project.isBuildWithGradle && findReadOnlyContentEntry(testRoot) == null) { - return ValidationInfo("Test source root is located out of content entry", testSourceFolderField.childComponent) + if (!model.project.isBuildWithGradle && ModuleUtil.findModuleForFile(testRoot.toRealFile(), model.project) == null) { + return ValidationInfo("Test source root is located out of any module", testSourceFolderField.childComponent) } membersTable.tableHeader?.background = UIUtil.getTableBackground() @@ -438,32 +617,35 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m "Tick any methods to generate tests for", membersTable ) } + if (!isSdkSupported()) { + return ValidationInfo("") + } return null } - class OKOptionAction(val testsModel: GenerateTestsModel, val okAction : Action) : AbstractAction(testsModel.getActionText()), OptionAction { + inner class OKOptionAction(val okAction : Action) : AbstractAction(model.getActionText()), OptionAction { init { putValue(DEFAULT_ACTION, java.lang.Boolean.TRUE) putValue(FOCUSED_ACTION, java.lang.Boolean.TRUE) } private val generateAction = object : AbstractAction(ACTION_GENERATE) { override fun actionPerformed(e: ActionEvent?) { - testsModel.runGeneratedTestsWithCoverage = false + model.runGeneratedTestsWithCoverage = false updateButtonText(e) } } private val generateAndRunAction = object : AbstractAction(ACTION_GENERATE_AND_RUN) { override fun actionPerformed(e: ActionEvent?) { - testsModel.runGeneratedTestsWithCoverage = true + model.runGeneratedTestsWithCoverage = true updateButtonText(e) } } private fun updateButtonText(e: ActionEvent?) { with(e?.source as JButton) { - text = testsModel.getActionText() - testsModel.project.service().runGeneratedTestsWithCoverage = - testsModel.runGeneratedTestsWithCoverage + text = this@GenerateTestsDialogWindow.model.getActionText() + this@GenerateTestsDialogWindow.model.project.service().runGeneratedTestsWithCoverage = + this@GenerateTestsDialogWindow.model.runGeneratedTestsWithCoverage repaint() } } @@ -473,15 +655,41 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } override fun getOptions(): Array { - if (testsModel.runGeneratedTestsWithCoverage) return arrayOf(generateAndRunAction, generateAction) + if (model.runGeneratedTestsWithCoverage) return arrayOf(generateAndRunAction, generateAction) return arrayOf(generateAction, generateAndRunAction) } + + override fun setEnabled(enabled: Boolean) { + super.setEnabled(enabled && isSdkSupported()) + } } - private val okOptionAction: OKOptionAction get() = OKOptionAction(model, super.getOKAction()) - override fun getOKAction() = okOptionAction + private var okOptionAction: OKOptionAction? = null + override fun getOKAction(): Action { + if (okOptionAction == null) { + okOptionAction = OKOptionAction(super.getOKAction()) + } + return okOptionAction!! + } override fun doOKAction() { + if (isSpringConfigSelected() + && springTestType.selectedItem == INTEGRATION_TEST + && Messages.showYesNoDialog( + model.project, + "Generating \"Integration tests\" may lead to corrupting user data or inflicting other harm.\n" + + "Please use a test configuration or profile.", + "Warning", + "Proceed", + "Go Back", + Messages.getWarningIcon() + ) != Messages.YES) { + return; + } + fun now() = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")) + + logger.info { "Tests generation instantiation phase started at ${now()}" } + model.testPackageName = if (testPackageField.text != SAME_PACKAGE_LABEL) testPackageField.text else "" @@ -500,30 +708,68 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m model.mockFramework = MOCKITO model.staticsMocking = if (staticsMocking.isSelected) MockitoStaticMocking else NoStaticMocking - model.codegenLanguage = model.project.service().codegenLanguage try { timeoutSpinner.commitEdit() } catch (ignored: ParseException) { } model.timeout = TimeUnit.SECONDS.toMillis(timeoutSpinner.number.toLong()) + model.testSourceRoot?.apply { model.updateSourceRootHistory(this.toNioPath().toString()) } + + model.springSettings = + when (springConfig.item) { + NO_SPRING_CONFIGURATION_OPTION -> AbsentSpringSettings + else -> { + val shortConfigName = springConfig.item.toString() + val config = + if (isXmlSpringConfigUsed()) { + val absolutePath = xmlConfigurationHelper.restoreFullName(shortConfigName) + SpringConfiguration.XMLConfiguration(absolutePath) + } else { + val classBinaryName = javaConfigurationHelper.restoreFullName(shortConfigName) + + val springBootConfigs = model.getSortedSpringBootApplicationClasses() + if (springBootConfigs.contains(classBinaryName)) { + SpringConfiguration.SpringBootConfiguration( + configBinaryName = classBinaryName, + isDefinitelyUnique = springBootConfigs.size == 1, + ) + } else { + SpringConfiguration.JavaConfiguration(classBinaryName) + } + } + + PresentSpringSettings( + configuration = config, + profiles = parseProfileExpression(springProfileNames.text, SpringProfileNames.defaultItem).toList() + ) + } + } + + model.springTestType = springTestType.item + model.springConfig = springConfig.item.toString() + model.springProfileNames = springProfileNames.text val settings = model.project.service() with(settings) { model.runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour model.hangingTestsTimeout = hangingTestsTimeout + model.useTaintAnalysis = useTaintAnalysis + model.runInspectionAfterTestGeneration = runInspectionAfterTestGeneration model.forceStaticMocking = forceStaticMocking model.chosenClassesToMockAlways = chosenClassesToMockAlways() model.fuzzingValue = fuzzingValue model.commentStyle = javaDocCommentStyle + model.summariesGenerationType = state.summariesGenerationType UtSettings.treatOverflowAsError = treatOverflowAsError == TreatOverflowAsError.AS_ERROR + UtSettings.useTaintAnalysis = model.useTaintAnalysis } - // firstly save settings - settings.loadStateFromModel(model) - // then process force static mocking case + // Firstly, save settings + loadStateFromModel(settings, model) + // Then, process force static mocking case model.generateWarningsForStaticMocking = model.staticsMocking is NoStaticMocking if (model.forceStaticMocking == ForceStaticMocking.FORCE) { - // we need mock framework extension to mock statics, no user provided => choose default + // We need mock framework extension to mock statics, no user provided => choose default if (model.staticsMocking is NoStaticMocking) { model.staticsMocking = StaticsMocking.defaultItem } @@ -537,15 +783,14 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } } catch (e: IncorrectOperationException) { println(e.message) - } - configureJvmTargetIfRequired() configureTestFrameworkIfRequired() configureMockFrameworkIfRequired() configureStaticMockingIfRequired() configureParametrizedTestsIfRequired() + logger.info { "Tests generation instantiation phase finished at ${now()}" } super.doOKAction() } @@ -600,19 +845,9 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m private fun showTestRootAbsenceErrorMessage() = Messages.showErrorDialog( "Test source root is not configured or is located out of content entry!", - "Generation error" + "Generation Error" ) - 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, false) } - } - private fun getOrCreateTestRoot(testSourceRoot: VirtualFile): Boolean { val modifiableModel = ModuleRootManager.getInstance(model.testModule).modifiableModel try { @@ -637,31 +872,66 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m private fun trimPackageName(name: String?): String = name?.trim() ?: "" + private fun isSpringConfigSelected(): Boolean = springConfig.item != NO_SPRING_CONFIGURATION_OPTION + private fun isXmlSpringConfigUsed(): Boolean = springConfig.item.toString().endsWith(".xml") + private fun initDefaultValues() { testPackageField.isEnabled = false cbSpecifyTestPackage.isEnabled = model.srcClasses.all { cl -> cl.packageName.isNotEmpty() } val settings = model.project.service() - mockStrategies.item = settings.mockStrategy - staticsMocking.isSelected = settings.staticsMocking == MockitoStaticMocking - parametrizedTestSources.isSelected = settings.parametrizedTestSource == ParametrizedTestSource.PARAMETRIZE - val areMocksSupported = settings.parametrizedTestSource == ParametrizedTestSource.DO_NOT_PARAMETRIZE - mockStrategies.isEnabled = areMocksSupported - staticsMocking.isEnabled = areMocksSupported && mockStrategies.item != MockStrategyApi.NO_MOCKS + when (model.projectType) { + ProjectType.Spring -> { + if (!settings.isSpringHandled) { + settings.isSpringHandled = true + settings.fuzzingValue = + if (settings.fuzzingValue == 0.0) 0.0 + else settings.fuzzingValue.coerceAtLeast(0.3) + } + springConfig.item = settings.springConfig + } + else -> {} + } + + mockStrategies.item = when (model.projectType) { + ProjectType.Spring -> + if (isSpringConfigSelected()) MockStrategyApi.springDefaultItem else settings.mockStrategy + else -> settings.mockStrategy + } + staticsMocking.isSelected = settings.staticsMocking == MockitoStaticMocking + parametrizedTestSources.isSelected = (settings.parametrizedTestSource == ParametrizedTestSource.PARAMETRIZE + && model.projectType == ProjectType.PureJvm) codegenLanguages.item = model.codegenLanguage val installedTestFramework = TestFramework.allItems.singleOrNull { it.isInstalled } + val testFramework = JavaTestFrameworkMapper.handleUnknown(settings.testFramework) currentFrameworkItem = when (parametrizedTestSources.isSelected) { - false -> installedTestFramework ?: settings.testFramework + false -> installedTestFramework ?: testFramework true -> installedTestFramework - ?: if (settings.testFramework != Junit4) settings.testFramework else TestFramework.parametrizedDefaultItem + ?: if (testFramework != Junit4) testFramework else TestFramework.parametrizedDefaultItem } - updateTestFrameworksList(settings.parametrizedTestSource) - updateParametrizationEnabled(currentFrameworkItem) + when (model.projectType) { + ProjectType.PureJvm -> { + updateTestFrameworksList(settings.parametrizedTestSource) + updateParametrizationEnabled() + } + ProjectType.Spring -> { + springProfileNames.text = settings.springProfileNames + springTestType.item = + if (isSpringConfigSelected()) settings.springTestType else SpringTestType.defaultItem + updateMockStrategy(springTestType.item) + updateSpringSettings() + updateTestFrameworksList(springTestType.item) + } + ProjectType.Python, + ProjectType.JavaScript -> { } + } + mockStrategies.isEnabled = !isSpringConfigSelected() + updateStaticMockEnabled() updateMockStrategyList() itemsToHelpTooltip.forEach { (box, tooltip) -> @@ -680,8 +950,6 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m * We need to notify the user about potential problems and to give * him a chance to install missing frameworks into his application. */ - //region configure frameworks - private fun configureTestFrameworkIfRequired() { val testFramework = testFrameworks.item if (!testFramework.isInstalled) { @@ -694,6 +962,8 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } model.conflictTriggers[Conflict.TestFrameworkConflict] = TestFramework.allItems.count { it.isInstalled } > 1 + + configureSpringTestFrameworkIfRequired() } private fun configureMockFrameworkIfRequired() { @@ -714,17 +984,30 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } } + private fun configureSpringTestFrameworkIfRequired() { + if (isSpringConfigSelected()) { + + SpringModule.installedItems + .forEach { configureSpringTestDependency(it) } + } + } + private fun configureTestFramework() { val selectedTestFramework = testFrameworks.item val libraryInProject = - findFrameworkLibrary(model.project, model.testModule, selectedTestFramework, LibrarySearchScope.Project) + findFrameworkLibrary(model.testModule, selectedTestFramework, LibrarySearchScope.Project) val versionInProject = libraryInProject?.libraryName?.parseVersion() + val sdkVersion = findSdkVersion(model.srcModule).feature val libraryDescriptor = when (selectedTestFramework) { Junit4 -> jUnit4LibraryDescriptor(versionInProject) Junit5 -> jUnit5LibraryDescriptor(versionInProject) - TestNg -> testNgLibraryDescriptor(versionInProject) + TestNg -> when (sdkVersion) { + minSupportedSdkVersion -> testNgOldLibraryDescriptor() + else -> testNgNewLibraryDescriptor(versionInProject) + } + else -> throw UnsupportedOperationException() } selectedTestFramework.isInstalled = true @@ -732,11 +1015,36 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m .onError { selectedTestFramework.isInstalled = false } } + private fun configureSpringTestDependency(springModule: SpringModule) { + val frameworkLibrary = + findDependencyInjectionLibrary(model.srcModule, springModule, LibrarySearchScope.Project) + val frameworkTestLibrary = + findDependencyInjectionTestLibrary(model.testModule, springModule, LibrarySearchScope.Project) + + val frameworkVersionInProject = frameworkLibrary?.libraryName?.parseVersion() + ?: error("Trying to install Spring test framework, but Spring framework is not found in module ${model.srcModule.name}") + val frameworkTestVersionInProject = frameworkTestLibrary?.libraryName?.parseVersion() + + if (frameworkTestVersionInProject == null || + !frameworkTestVersionInProject.isCompatibleWith(frameworkVersionInProject) +) { + val libraryDescriptor = when (springModule) { + SPRING_BOOT -> springBootTestLibraryDescriptor(frameworkVersionInProject) + SPRING_BEANS -> springTestLibraryDescriptor(frameworkVersionInProject) + SPRING_SECURITY -> springSecurityLibraryDescriptor(frameworkVersionInProject) + } + + model.preClasspathCollectionPromises += addDependency(model.testModule, libraryDescriptor) + } + + springModule.testFrameworkInstalled = true + } + private fun configureMockFramework() { val selectedMockFramework = MOCKITO val libraryInProject = - findFrameworkLibrary(model.project, model.testModule, selectedMockFramework, LibrarySearchScope.Project) + findFrameworkLibrary(model.testModule, selectedMockFramework, LibrarySearchScope.Project) val versionInProject = libraryInProject?.libraryName?.parseVersion() selectedMockFramework.isInstalled = true @@ -762,10 +1070,10 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m val mockitoExtensionsPath = "$testResourcesPath/$MOCKITO_EXTENSIONS_FOLDER".toPath() val mockitoMockMakerPath = "$mockitoExtensionsPath/$MOCKITO_MOCKMAKER_FILE_NAME".toPath() - if (!testResourcesPath.exists()) Files.createDirectory(testResourcesPath) - if (!mockitoExtensionsPath.exists()) Files.createDirectory(mockitoExtensionsPath) + if (testResourcesPath.notExists()) Files.createDirectories(testResourcesPath) + if (mockitoExtensionsPath.notExists()) Files.createDirectories(mockitoExtensionsPath) - if (!mockitoMockMakerPath.exists()) { + if (mockitoMockMakerPath.notExists()) { Files.createFile(mockitoMockMakerPath) Files.write(mockitoMockMakerPath, listOf(MOCKITO_EXTENSIONS_FILE_CONTENT)) } @@ -775,14 +1083,14 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m // TODO: currently first three declarations are copy-pasted from configureTestFramework(), maybe fix this somehow? val selectedTestFramework = testFrameworks.item - val libraryInProject = - findFrameworkLibrary(model.project, model.testModule, selectedTestFramework, LibrarySearchScope.Project) + val libraryInProject = findFrameworkLibrary(model.testModule, selectedTestFramework, LibrarySearchScope.Project) val versionInProject = libraryInProject?.libraryName?.parseVersion() val libraryDescriptor: ExternalLibraryDescriptor? = when (selectedTestFramework) { Junit4 -> error("Parametrized tests are not supported for JUnit 4") Junit5 -> jUnit5ParametrizedTestsLibraryDescriptor(versionInProject) TestNg -> null // Parametrized tests come with TestNG by default + else -> throw UnsupportedOperationException() } selectedTestFramework.isParametrizedTestsConfigured = true @@ -798,18 +1106,18 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m * 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(module: Module, libraryDescriptor: ExternalLibraryDescriptor): Promise { + private fun addDependency(module: Module, libraryDescriptor: ExternalLibraryDescriptor): Promise { val promise = 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) - promise.thenRun { + + return promise.thenRun { module.allLibraries() .lastOrNull { library -> library.presentableName.contains(libraryDescriptor.id) }?.let { ModuleRootModificationUtil.updateModel(module) { model -> placeEntryToCorrectPlace(model, it) } } } - return promise } /** @@ -830,57 +1138,6 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m //endregion - /** - * Configures JVM Target if required. - * - * We need to notify the user about potential problems and to give - * him a chance to change JVM targets in his application. - * - * Note that now we need it for Kotlin plugin only. - */ - private fun configureJvmTargetIfRequired() { - if (codegenLanguages.item == CodegenLanguage.KOTLIN - && parametrizedTestSources.isSelected - && createKotlinJvmTargetNotificationDialog() == Messages.YES - ) { - configureKotlinJvmTarget() - } - } - - /** - * Checks if JVM target for Kotlin plugin if configured appropriately - * and allows user to configure it via ProjectStructure tab if not. - * - * For Kotlin plugin until version 1.5 default JVM target is 1.6. - * Sometimes (i.e. in parametrized tests) we use some annotations - * and statements that are supported since JVM version 1.8 only. - */ - private fun configureKotlinJvmTarget() { - val activeKotlinJvmTarget = model.srcModule.kotlinTargetPlatform().description - if (activeKotlinJvmTarget == actualKotlinJvmTarget) { - return - } - - ShowSettingsUtil.getInstance().editConfigurable( - model.project, - ProjectStructureConfigurable.getInstance(Objects.requireNonNull(model.project)) - ) - } - - private fun createKotlinJvmTargetNotificationDialog() = Messages.showYesNoDialog( - """Your current JVM target is 1.6. Some Kotlin features may not be supported. - |Would you like to update current target to $actualKotlinJvmTarget?""".trimMargin(), - title, - "Yes", - "No", - Messages.getQuestionIcon(), - ProjectNewWindowDoNotAskOption(), - ) - - //language features we use to generate parametrized tests - // (i.e. @JvmStatic attribute or JUnit5 arguments) are supported since JVM target 1.8 - private val actualKotlinJvmTarget = "1.8" - private fun setListeners() { itemsToHelpTooltip.forEach { (box, tooltip) -> if (box is ComboBox<*> && tooltip != null) { box.setHelpTooltipTextChanger(tooltip) @@ -890,19 +1147,15 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m with((event.source as JComboBox<*>).selectedItem) { if (this is VirtualFile) { model.setSourceRootAndFindTestModule(this@with) - } - else { + } else { model.setSourceRootAndFindTestModule(null) } } } - mockStrategies.addActionListener { event -> - val comboBox = event.source as ComboBox<*> - val item = comboBox.item as MockStrategyApi - - staticsMocking.isEnabled = item != MockStrategyApi.NO_MOCKS - if (!staticsMocking.isEnabled) { + mockStrategies.addActionListener { _ -> + updateControlsEnabledStatus() + if (mockStrategies.item == MockStrategyApi.NO_MOCKS) { staticsMocking.isSelected = false } } @@ -912,28 +1165,61 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m val item = comboBox.item as TestFramework currentFrameworkItem = item - updateParametrizationEnabled(currentFrameworkItem) + + updateControlsEnabledStatus() + } + + codegenLanguages.addActionListener { _ -> + updateControlsEnabledStatus() } - parametrizedTestSources.addActionListener { event -> + parametrizedTestSources.addActionListener { _ -> val parametrizedTestSource = if (parametrizedTestSources.isSelected) { ParametrizedTestSource.PARAMETRIZE } else { ParametrizedTestSource.DO_NOT_PARAMETRIZE } - val areMocksSupported = parametrizedTestSource == ParametrizedTestSource.DO_NOT_PARAMETRIZE + updateTestFrameworksList(parametrizedTestSource) + updateControlsEnabledStatus() + } + + springConfig.addActionListener { _ -> + if (isSpringConfigSelected()) { + if (isXmlSpringConfigUsed()) { + springTestType.item = SpringTestType.defaultItem + } - mockStrategies.isEnabled = areMocksSupported - staticsMocking.isEnabled = areMocksSupported && mockStrategies.item != MockStrategyApi.NO_MOCKS - if (!mockStrategies.isEnabled) { - mockStrategies.item = MockStrategyApi.NO_MOCKS + if (springTestType.item == UNIT_TEST) { + mockStrategies.item = MockStrategyApi.springDefaultItem + } + } else { + mockStrategies.item = when (model.projectType) { + ProjectType.Spring -> MockStrategyApi.springDefaultItem + else -> MockStrategyApi.defaultItem + } + + springTestType.item = SpringTestType.defaultItem + + springProfileNames.text = "" } - if (!staticsMocking.isEnabled) { - staticsMocking.isSelected = false + + if (isSpringConfigSelected() && springTestType.item == UNIT_TEST) { + staticsMocking.isSelected = true } - updateTestFrameworksList(parametrizedTestSource) + updateMockStrategyList() + updateControlsEnabledStatus() + } + + springTestType.addActionListener { event -> + val comboBox = event.source as ComboBox<*> + val item = comboBox.item as SpringTestType + + updateTestFrameworksList(item) + updateMockStrategy(item) + updateMockStrategyList() + updateControlsEnabledStatus() } cbSpecifyTestPackage.addActionListener { @@ -945,63 +1231,118 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } } + private fun updateMockStrategy(springTestType: SpringTestType){ + when (springTestType) { + UNIT_TEST -> { + if(isSpringConfigSelected()){ + mockStrategies.item = MockStrategyApi.springDefaultItem + staticsMocking.isSelected = true + } + } + INTEGRATION_TEST -> { + mockStrategies.item = MockStrategyApi.springIntegrationTestItem + staticsMocking.isSelected = false + } + } + } + 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 - var enabledTestFrameworks = when (parametrizedTestSource) { + // We do not support parameterized tests for JUnit4 + val enabledTestFrameworks = when (parametrizedTestSource) { ParametrizedTestSource.DO_NOT_PARAMETRIZE -> TestFramework.allItems ParametrizedTestSource.PARAMETRIZE -> TestFramework.allItems.filterNot { it == Junit4 } } - //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 } - } - var defaultItem = when (parametrizedTestSource) { ParametrizedTestSource.DO_NOT_PARAMETRIZE -> TestFramework.defaultItem ParametrizedTestSource.PARAMETRIZE -> TestFramework.parametrizedDefaultItem } enabledTestFrameworks.forEach { if (it.isInstalled && !defaultItem.isInstalled) defaultItem = it } + updateTestFrameworksList(enabledTestFrameworks, defaultItem) + } + + private fun updateTestFrameworksList(springTestType: SpringTestType) { + // We do not support Spring integration tests for TestNg + val enabledTestFrameworks = when (springTestType) { + UNIT_TEST -> TestFramework.allItems + INTEGRATION_TEST -> TestFramework.allItems.filterNot { it == TestNg } + } + + updateTestFrameworksList(enabledTestFrameworks) + } + + private fun updateTestFrameworksList( + enabledTestFrameworks: List, + defaultItem: TestFramework = TestFramework.defaultItem, + ) { testFrameworks.model = DefaultComboBoxModel(enabledTestFrameworks.toTypedArray()) testFrameworks.item = if (currentFrameworkItem in enabledTestFrameworks) currentFrameworkItem else defaultItem - testFrameworks.renderer = object : ColoredListCellRenderer() { - override fun customizeCellRenderer( - list: JList, value: TestFramework, - index: Int, selected: Boolean, hasFocus: Boolean - ) { - this.append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES) - if (!value.isInstalled) { - this.append(WILL_BE_INSTALLED_LABEL, SimpleTextAttributes.ERROR_ATTRIBUTES) - } - } - } + testFrameworks.renderer = createTestFrameworksRenderer(WILL_BE_INSTALLED_LABEL) currentFrameworkItem = testFrameworks.item } - //We would like to disable parametrization options for JUnit4 - private fun updateParametrizationEnabled(testFramework: TestFramework) { - when (testFramework) { - Junit4 -> parametrizedTestSources.isEnabled = false - Junit5, - TestNg -> parametrizedTestSources.isEnabled = true + private fun updateParametrizationEnabled() { + val languageIsSupported = codegenLanguages.item == CodegenLanguage.JAVA + val frameworkIsSupported = currentFrameworkItem == Junit5 + || currentFrameworkItem == TestNg && findSdkVersion(model.srcModule).feature > minSupportedSdkVersion + val mockStrategyIsSupported = mockStrategies.item == MockStrategyApi.NO_MOCKS + + // We do not support PUT in Spring projects + val isSupportedProjectType = model.projectType == ProjectType.PureJvm + parametrizedTestSources.isEnabled = + isSupportedProjectType && languageIsSupported && frameworkIsSupported && mockStrategyIsSupported + + if (!parametrizedTestSources.isEnabled) { + parametrizedTestSources.isSelected = false } } + private fun updateStaticMockEnabled() { + val mockStrategyIsSupported = mockStrategies.item != MockStrategyApi.NO_MOCKS + staticsMocking.isEnabled = mockStrategyIsSupported && !isSpringConfigSelected() + } + private fun updateMockStrategyList() { mockStrategies.renderer = object : ColoredListCellRenderer() { override fun customizeCellRenderer( list: JList, value: MockStrategyApi, index: Int, selected: Boolean, hasFocus: Boolean + ) { + if(mockStrategies.item == MockStrategyApi.springDefaultItem && isSpringConfigSelected()) { + this.append("Mock using Spring configuration", SimpleTextAttributes.REGULAR_ATTRIBUTES) + } + else{ + this.append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES) + if (value != MockStrategyApi.NO_MOCKS && !MOCKITO.isInstalled) { + this.append(WILL_BE_INSTALLED_LABEL, SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } + } + } + + private fun updateSpringSettings() { + // We check for > 1 because there is already extra-dummy NO_SPRING_CONFIGURATION_OPTION option + springConfig.isEnabled = model.projectType == ProjectType.Spring && springConfig.itemCount > 1 + + springTestType.renderer = object : ColoredListCellRenderer() { + override fun customizeCellRenderer( + list: JList, value: SpringTestType, + index: Int, selected: Boolean, hasFocus: Boolean ) { this.append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES) - if (value != MockStrategyApi.NO_MOCKS && !MOCKITO.isInstalled) { - this.append(WILL_BE_INSTALLED_LABEL, SimpleTextAttributes.ERROR_ATTRIBUTES) + if (isSpringConfigSelected()) { + SpringModule.installedItems + // only first missing test framework is shown to avoid overflowing ComboBox + .firstOrNull { !it.testFrameworkInstalled } + ?.let { diFramework -> + val additionalText = " (${diFramework.testFrameworkDisplayName} will be installed)" + this.append(additionalText, SimpleTextAttributes.ERROR_ATTRIBUTES) + } } } } @@ -1039,6 +1380,31 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } } + + private fun updateControlsEnabledStatus() { + mockStrategies.isEnabled = true + + updateParametrizationEnabled() + updateStaticMockEnabled() + + if (model.projectType == ProjectType.Spring) { + updateSpringControlsEnabledStatus() + } + } + + private fun updateSpringControlsEnabledStatus() { + // Parametrized tests are not supported for Spring + parametrizedTestSources.isEnabled = false + + if (isSpringConfigSelected()) { + mockStrategies.isEnabled = false + springProfileNames.isEnabled = true + springTestType.isEnabled = !isXmlSpringConfigUsed() + } else { + springProfileNames.isEnabled = false + springTestType.isEnabled = false + } + } } fun GenerateTestsModel.getActionText() : String = @@ -1052,4 +1418,4 @@ private fun ComboBox<*>.setHelpTooltipTextChanger(helpLabel: JBLabel) { helpLabel.toolTipText = item.description } } -} \ No newline at end of file +} 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 deleted file mode 100644 index 87a9f36317..0000000000 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt +++ /dev/null @@ -1,198 +0,0 @@ -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.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 - protected abstract val displayId: String - protected abstract fun content(project: Project?, module: Module?, info: String): String - - open fun notify(info: String, project: Project? = null, module: Module? = null) { - notificationGroup - .createNotification(content(project, module, info), notificationType) - .notify(project) - } - - protected open val notificationDisplayType = NotificationDisplayType.BALLOON - - protected val notificationGroup: NotificationGroup - get() = NotificationGroup(displayId, notificationDisplayType) -} - -abstract class WarningNotifier : Notifier() { - override val notificationType: NotificationType = NotificationType.WARNING - final override fun notify(info: String, project: Project?, module: Module?) { - super.notify(info, project, module) - } -} - -abstract class ErrorNotifier : Notifier() { - final override val notificationType: NotificationType = NotificationType.ERROR - - final override fun notify(info: String, project: Project?, module: Module?) { - super.notify(info, project, module) - error(content(project, module, info)) - } -} - -object CommonErrorNotifier : ErrorNotifier() { - override val displayId: String = "UTBot plugin errors" - override fun content(project: Project?, module: Module?, info: String): String = info -} - -object UnsupportedJdkNotifier : ErrorNotifier() { - override val displayId: String = "Unsupported JDK" - override fun content(project: Project?, module: Module?, info: String): String = - "JDK versions older than 8 are not supported. This project's JDK version is $info" -} - -object InvalidClassNotifier : WarningNotifier() { - override val displayId: String = "Invalid class" - override fun content(project: Project?, module: Module?, info: String): String = - "Generate tests with UtBot for the $info is not supported." -} - -object MissingLibrariesNotifier : WarningNotifier() { - override val displayId: String = "Missing libraries" - override fun content(project: Project?, module: Module?, info: String): String = - "Library $info missing on the test classpath of module ${module?.name}" -} - -@Suppress("unused") -object UnsupportedTestFrameworkNotifier : ErrorNotifier() { - override val displayId: String = "Unsupported test framework" - override fun content(project: Project?, module: Module?, info: String): String = - "Test framework $info is not supported yet" -} - -abstract class UrlNotifier : Notifier() { - - protected abstract val titleText: String - protected abstract val urlOpeningListener: NotificationListener - - override fun notify(info: String, project: Project?, module: Module?) { - notificationGroup.createNotification(content(project, module, info), notificationType) - .setTitle(titleText).setListener(urlOpeningListener).notify(project) - } -} - -abstract class InformationUrlNotifier : UrlNotifier() { - override val notificationType: NotificationType = NotificationType.INFORMATION -} - -abstract class WarningUrlNotifier : UrlNotifier() { - override val notificationType: NotificationType = NotificationType.WARNING -} - -abstract class EventLogNotifier : InformationUrlNotifier() { - override val notificationDisplayType = NotificationDisplayType.NONE -} - -object SarifReportNotifier : EventLogNotifier() { - - override val displayId: String = "SARIF report" - - override val titleText: String = "" // no title - - override val urlOpeningListener: NotificationListener = NotificationListener.UrlOpeningListener(false) - - override fun content(project: Project?, module: Module?, info: String): String = info -} - -object TestsReportNotifier : InformationUrlNotifier() { - override val displayId: String = "Generated unit tests report" - - override val titleText: String = "UTBot: unit tests generated successfully" - - public override val urlOpeningListener: TestReportUrlOpeningListener = TestReportUrlOpeningListener - - override fun content(project: Project?, module: Module?, info: String): String = info -} - -// TODO replace inheritance with decorators -object WarningTestsReportNotifier : WarningUrlNotifier() { - override val displayId: String = "Generated unit tests report" - - override val titleText: String = "UTBot: unit tests generated with warnings" - - public override val urlOpeningListener: TestReportUrlOpeningListener = TestReportUrlOpeningListener - - override fun content(project: Project?, module: Module?, info: String): String = info -} - -object DetailsTestsReportNotifier : EventLogNotifier() { - override val displayId: String = "Test report details" - - override val titleText: String = "Test report details of the unit tests generation via UtBot" - - public override val urlOpeningListener: TestReportUrlOpeningListener = TestReportUrlOpeningListener - - override fun content(project: Project?, module: Module?, info: String): String = info -} - -/** - * Listener that handles URLs starting with [prefix], like "#utbot/configure-mockito". - */ -object TestReportUrlOpeningListener: NotificationListener.Adapter() { - const val prefix = "#utbot/" - const val mockitoSuffix = "configure-mockito" - const val mockitoInlineSuffix = "mockito-inline" - const val eventLogSuffix = "event-log" - - val callbacks: Map Unit>> = hashMapOf( - Pair(mockitoSuffix, mutableListOf()), - Pair(mockitoInlineSuffix, mutableListOf()), - Pair(eventLogSuffix, mutableListOf()), - ) - - private val defaultListener = NotificationListener.UrlOpeningListener(false) - - 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) = - callbacks[descriptionSuffix]?.map { it() } ?: 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/actions/GenerateTestsAction.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt deleted file mode 100644 index d6e05ee4db..0000000000 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt +++ /dev/null @@ -1,233 +0,0 @@ -package org.utbot.intellij.plugin.ui.actions - -import com.intellij.openapi.actionSystem.ActionPlaces -import org.utbot.intellij.plugin.generator.UtTestsDialogProcessor -import org.utbot.intellij.plugin.ui.utils.PsiElementHandler -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.UpdateInBackground -import com.intellij.openapi.actionSystem.PlatformDataKeys -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.module.ModuleUtil -import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ModuleRootManager -import com.intellij.openapi.roots.ProjectFileIndex -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.* -import com.intellij.psi.util.PsiTreeUtil -import com.intellij.refactoring.util.classMembers.MemberInfo -import org.jetbrains.kotlin.idea.core.getPackage -import org.jetbrains.kotlin.idea.core.util.toPsiDirectory -import org.jetbrains.kotlin.idea.core.util.toPsiFile -import org.utbot.intellij.plugin.util.extractFirstLevelMembers -import org.utbot.intellij.plugin.util.isVisible -import java.util.* -import org.jetbrains.kotlin.j2k.getContainingClass -import org.jetbrains.kotlin.utils.addIfNotNull -import org.utbot.intellij.plugin.models.packageName -import org.utbot.intellij.plugin.ui.InvalidClassNotifier -import org.utbot.intellij.plugin.util.isAbstract - -class GenerateTestsAction : AnAction(), UpdateInBackground { - override fun actionPerformed(e: AnActionEvent) { - val project = e.project ?: return - - val (srcClasses, focusedMethods, extractMembersFromSrcClasses) = getPsiTargets(e) ?: return - val validatedSrcClasses = validateSrcClasses(srcClasses) ?: return - - UtTestsDialogProcessor.createDialogAndGenerateTests(project, validatedSrcClasses, extractMembersFromSrcClasses, focusedMethods) - } - - override fun update(e: AnActionEvent) { - if (e.place == ActionPlaces.POPUP) { - e.presentation.text = "Tests with UnitTestBot..." - } - e.presentation.isEnabled = getPsiTargets(e) != null - } - - private fun getPsiTargets(e: AnActionEvent): Triple, Set, Boolean>? { - val project = e.project ?: return null - val editor = e.getData(CommonDataKeys.EDITOR) - if (editor != null) { - //The action is being called from editor - val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null - val element = findPsiElement(file, editor) ?: return null - - val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) - - if (psiElementHandler.isCreateTestActionAvailable(element)) { - val srcClass = psiElementHandler.containingClass(element) ?: return null - val srcSourceRoot = srcClass.getSourceRoot() ?: return null - val srcMembers = srcClass.extractFirstLevelMembers(false) - val focusedMethod = focusedMethodOrNull(element, srcMembers, psiElementHandler) - - val module = ModuleUtil.findModuleForFile(srcSourceRoot, project) ?: return null - val matchingRoot = ModuleRootManager.getInstance(module).contentEntries - .flatMap { entry -> entry.sourceFolders.toList() } - .firstOrNull { folder -> folder.file == srcSourceRoot } - if (srcMembers.isEmpty() || matchingRoot == null || matchingRoot.rootType.isForTests) { - return null - } - - return Triple(setOf(srcClass), if (focusedMethod != null) setOf(focusedMethod) else emptySet(), true) - } - } else { - // The action is being called from 'Project' tool window - val srcClasses = mutableSetOf() - val selectedMethods = mutableSetOf() - var extractMembersFromSrcClasses = false - val element = e.getData(CommonDataKeys.PSI_ELEMENT) - if (element is PsiFileSystemItem) { - e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { - srcClasses += getAllClasses(project, it) - } - } else if (element is PsiElement){ - val file = element.containingFile ?: return null - val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) - - if (psiElementHandler.isCreateTestActionAvailable(element)) { - psiElementHandler.containingClass(element)?.let { - srcClasses += setOf(it) - extractMembersFromSrcClasses = true - val memberInfoList = runReadAction> { - it.extractFirstLevelMembers(false) - } - if (memberInfoList.isNullOrEmpty()) - return null - } - - if (element is PsiMethod) { - selectedMethods.add(MemberInfo(element)) - } - } - } else { - val someSelection = e.getData(PlatformDataKeys.SELECTED_ITEMS)?: return null - someSelection.forEach { - when(it) { - is PsiFileSystemItem -> srcClasses += getAllClasses(project, arrayOf(it.virtualFile)) - is PsiClass -> srcClasses.add(it) - is PsiElement -> { - srcClasses.addIfNotNull(it.getContainingClass()) - if (it is PsiMethod) { - selectedMethods.add(MemberInfo(it)) - extractMembersFromSrcClasses = true - } - } - } - } - } - - if (srcClasses.size > 1) { - extractMembersFromSrcClasses = false - } - var commonSourceRoot = null as VirtualFile? - for (srcClass in srcClasses) { - if (commonSourceRoot == null) { - commonSourceRoot = srcClass.getSourceRoot()?: return null - } else if (commonSourceRoot != srcClass.getSourceRoot()) return null - } - if (commonSourceRoot == null) return null - val module = ModuleUtil.findModuleForFile(commonSourceRoot, project)?: return null - - if (!Arrays.stream(ModuleRootManager.getInstance(module).contentEntries) - .flatMap { entry -> Arrays.stream(entry.sourceFolders) } - .filter { folder -> !folder.rootType.isForTests && folder.file == commonSourceRoot} - .findAny().isPresent ) return null - - return Triple(srcClasses.toSet(), selectedMethods.toSet(), extractMembersFromSrcClasses) - } - return null - } - - /** - * Validates that a set of source classes matches some requirements from [isInvalid]. - * If no one of them matches, shows a warning about the first mismatch reason. - */ - private fun validateSrcClasses(srcClasses: Set): Set? { - val filteredClasses = srcClasses - .filterNot { it.isInvalid(withWarnings = false) } - .toSet() - - if (filteredClasses.isEmpty()) { - srcClasses.first().isInvalid(withWarnings = true) - return null - } - - return filteredClasses - } - - private fun PsiClass.isInvalid(withWarnings: Boolean): Boolean { - val isAbstractOrInterface = this.isInterface || this.isAbstract - if (isAbstractOrInterface) { - if (withWarnings) InvalidClassNotifier.notify("abstract class or interface ${this.name}") - return true - } - - val isInvisible = !this.isVisible - if (isInvisible) { - if (withWarnings) InvalidClassNotifier.notify("private or protected class ${this.name}") - return true - } - - val packageIsIncorrect = this.packageName.startsWith("java") - if (packageIsIncorrect) { - if (withWarnings) InvalidClassNotifier.notify("class ${this.name} located in java.* package") - return true - } - - return false - } - - private fun PsiElement?.getSourceRoot() : VirtualFile? { - val project = this?.project?: return null - val virtualFile = this.containingFile?.originalFile?.virtualFile?: return null - return ProjectFileIndex.getInstance(project).getSourceRootForFile(virtualFile) - } - - private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { - val offset = editor.caretModel.offset - var element = file.findElementAt(offset) - if (element == null && offset == file.textLength) { - element = file.findElementAt(offset - 1) - } - - return element - } - - private fun focusedMethodOrNull(element: PsiElement, methods: List, psiElementHandler: PsiElementHandler): MemberInfo? { - // getParentOfType might return element which does not correspond to the standard Psi hierarchy. - // Thus, make transition to the Psi if it is required. - val currentMethod = PsiTreeUtil.getParentOfType(element, psiElementHandler.methodClass) - ?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) } - - return methods.singleOrNull { it.member == currentMethod } - } - - private fun getAllClasses(directory: PsiDirectory): Set { - val allClasses = directory.files.flatMap { getClassesFromFile(it) }.toMutableSet() - for (subDir in directory.subdirectories) allClasses += getAllClasses(subDir) - return allClasses - } - - private fun getAllClasses(project: Project, virtualFiles: Array): Set { - val psiFiles = virtualFiles.mapNotNull { it.toPsiFile(project) } - val psiDirectories = virtualFiles.mapNotNull { it.toPsiDirectory(project) } - val dirsArePackages = psiDirectories.all { it.getPackage()?.qualifiedName?.isNotEmpty() == true } - - if (!dirsArePackages) { - return emptySet() - } - val allClasses = psiFiles.flatMap { getClassesFromFile(it) }.toMutableSet() - for (psiDir in psiDirectories) allClasses += getAllClasses(psiDir) - - return allClasses - } - - private fun getClassesFromFile(psiFile: PsiFile): List { - val psiElementHandler = PsiElementHandler.makePsiElementHandler(psiFile) - return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, psiElementHandler.classClass) - .map { psiElementHandler.toPsi(it, PsiClass::class.java) } - } -} \ No newline at end of file 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 c223176153..14ce8db2e0 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 @@ -3,6 +3,7 @@ package org.utbot.intellij.plugin.ui.components import com.intellij.openapi.application.ReadAction import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComponentWithBrowseButton @@ -13,21 +14,21 @@ import com.intellij.ui.ColoredListCellRenderer import com.intellij.ui.SimpleTextAttributes import com.intellij.util.ArrayUtil import com.intellij.util.ui.UIUtil -import java.io.File -import javax.swing.DefaultComboBoxModel -import javax.swing.JList -import org.jetbrains.kotlin.idea.util.projectStructure.allModules import org.utbot.common.PathUtil +import org.utbot.intellij.plugin.models.BaseTestsModel import org.utbot.intellij.plugin.models.GenerateTestsModel -import org.utbot.intellij.plugin.ui.utils.TestSourceRoot +import org.utbot.intellij.plugin.ui.utils.ITestSourceRoot import org.utbot.intellij.plugin.ui.utils.addDedicatedTestRoot import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle -import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots +import java.io.File +import javax.swing.DefaultComboBoxModel +import javax.swing.JList + +private const val SET_TEST_FOLDER = "set test folder" class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : ComponentWithBrowseButton>(ComboBox(), null) { - private val SET_TEST_FOLDER = "set test folder" init { if (model.project.isBuildWithGradle) { @@ -56,20 +57,7 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : } } - val suggestedModules = - if (model.project.isBuildWithGradle) model.project.allModules() else model.potentialTestModules - - val testRoots = suggestedModules.flatMap { - it.suitableTestSourceRoots() - }.sortedWith( - compareByDescending { - // Heuristics: Dirs with language == codegenLanguage should go first - it.expectedLanguage == model.codegenLanguage - }.thenBy { - // Heuristics: User is more likely to choose the shorter path - it.dir.path.length - } - ).toMutableList() + val testRoots = model.getSortedTestRoots() // this method is blocked for Gradle, where multiple test modules can exist model.testModule.addDedicatedTestRoot(testRoots, model.codegenLanguage) @@ -81,7 +69,7 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : } addActionListener { - val testSourceRoot = createNewTestSourceRoot(model) + val testSourceRoot = chooseTestRoot(model) testSourceRoot?.let { model.setSourceRootAndFindTestModule(it) @@ -90,35 +78,42 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : } else { //Prepend and select newly added test root val testRootItems = linkedSetOf(it) - testRootItems += (0 until childComponent.itemCount).map { i -> childComponent.getItemAt(i) as VirtualFile} + testRootItems += (0 until childComponent.itemCount).map { i -> childComponent.getItemAt(i) as VirtualFile } newItemList(testRootItems) } } } } - private fun createNewTestSourceRoot(model: GenerateTestsModel): VirtualFile? = + private fun chooseTestRoot(model: BaseTestsModel): VirtualFile? = ReadAction.compute { - val desc = FileChooserDescriptor(false, true, false, false, false, false) + val desc = object : FileChooserDescriptor(false, true, false, false, false, false) { + override fun isFileSelectable(file: VirtualFile?): Boolean { + return file != null && ModuleUtil.findModuleForFile( + file, + model.project + ) != null && super.isFileSelectable(file) + } + } val initialFile = model.project.guessProjectDir() val files = FileChooser.chooseFiles(desc, model.project, initialFile) files.singleOrNull() } - private fun configureRootsCombo(testRoots: List) { + private fun configureRootsCombo(testRoots: List) { val selectedRoot = testRoots.first() // do not update model.testModule here, because fake test source root could have been chosen model.testSourceRoot = selectedRoot.dir - newItemList(testRoots.map { it.dir }.toSet()) + newItemList(testRoots.mapNotNull { it.dir }.toSet()) } private fun newItemList(comboItems: Set) { childComponent.model = DefaultComboBoxModel(ArrayUtil.toObjectArray(comboItems)) } - private fun formatUrl(virtualFile: VirtualFile, model: GenerateTestsModel): String { + private fun formatUrl(virtualFile: VirtualFile, model: BaseTestsModel): String { var directoryUrl = if (virtualFile is FakeVirtualFile) { virtualFile.parent.presentableUrl + File.separatorChar + virtualFile.name } else { diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt deleted file mode 100644 index 948bb9344a..0000000000 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.utbot.intellij.plugin.ui.utils - -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.Messages - -fun showErrorDialogLater(project: Project, message: String, title: String) { - invokeLater { - Messages.showErrorDialog(project, message, title) - } -} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/KotlinPsiElementHandler.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/KotlinPsiElementHandler.kt index 0beca8f9a1..1ea7d3e110 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/KotlinPsiElementHandler.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/KotlinPsiElementHandler.kt @@ -2,6 +2,9 @@ package org.utbot.intellij.plugin.ui.utils import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.util.findParentOfType +import org.jetbrains.kotlin.asJava.findFacadeClass import org.jetbrains.kotlin.idea.testIntegration.KotlinCreateTestIntention import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtClassOrObject @@ -24,13 +27,27 @@ class KotlinPsiElementHandler( return element.toUElement()?.javaPsi as? T ?: error("Could not cast $element to $clazz") } - override fun isCreateTestActionAvailable(element: PsiElement): Boolean = - getTarget(element)?.let { KotlinCreateTestIntention().applicabilityRange(it) != null } ?: false + override fun getClassesFromFile(psiFile: PsiFile): List { + return listOfNotNull((psiFile as? KtFile)?.findFacadeClass()) + super.getClassesFromFile(psiFile) + } + + override fun isCreateTestActionAvailable(element: PsiElement): Boolean { + getTarget(element)?.let { + return KotlinCreateTestIntention().applicabilityRange(it) != null + } + return (element.containingFile as? KtFile)?.findFacadeClass() != null + } private fun getTarget(element: PsiElement?): KtNamedDeclaration? = element?.parentsWithSelf ?.firstOrNull { it is KtClassOrObject || it is KtNamedDeclaration && it.parent is KtFile } as? KtNamedDeclaration - override fun containingClass(element: PsiElement): PsiClass? = - element.parentsWithSelf.firstOrNull { it is KtClassOrObject }?.let { toPsi(it, PsiClass::class.java) } + override fun containingClass(element: PsiElement): PsiClass? { + element.findParentOfType(strict=false)?.let { + return toPsi(it, PsiClass::class.java) + } + return element.findParentOfType(strict=false)?.findFacadeClass()?.let { + toPsi(it, PsiClass::class.java) + } + } } \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryMatcher.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryMatcher.kt index e6ae80afdc..8666a3c4d8 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryMatcher.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryMatcher.kt @@ -1,47 +1,55 @@ package org.utbot.intellij.plugin.ui.utils -import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.model.util.patterns +import org.utbot.framework.codegen.domain.TestFramework import org.utbot.framework.plugin.api.MockFramework import com.intellij.openapi.module.Module -import com.intellij.openapi.project.Project import com.intellij.openapi.roots.LibraryOrderEntry -import org.utbot.framework.codegen.model.util.parametrizedTestsPatterns -import org.utbot.framework.codegen.model.util.Patterns +import org.utbot.framework.codegen.domain.SpringModule +import org.utbot.framework.plugin.api.utils.Patterns +import org.utbot.framework.plugin.api.utils.parametrizedTestsPatterns +import org.utbot.framework.plugin.api.utils.patterns +import org.utbot.framework.plugin.api.utils.testPatterns fun findFrameworkLibrary( - project: Project, - testModule: Module, + module: Module, testFramework: TestFramework, scope: LibrarySearchScope = LibrarySearchScope.Module, -): LibraryOrderEntry? { - return findMatchingLibrary(project, testModule, testFramework.patterns(), scope) -} +): LibraryOrderEntry? = findMatchingLibraryOrNull(module, testFramework.patterns(), scope) fun findFrameworkLibrary( - project: Project, - testModule: Module, + module: Module, mockFramework: MockFramework, scope: LibrarySearchScope = LibrarySearchScope.Module, -): LibraryOrderEntry? = findMatchingLibrary(project, testModule, mockFramework.patterns(), scope) +): LibraryOrderEntry? = findMatchingLibraryOrNull(module, mockFramework.patterns(), scope) fun findParametrizedTestsLibrary( - project: Project, - testModule: Module, + module: Module, testFramework: TestFramework, scope: LibrarySearchScope = LibrarySearchScope.Module, -): LibraryOrderEntry? = findMatchingLibrary(project, testModule, testFramework.parametrizedTestsPatterns(), scope) +): LibraryOrderEntry? = findMatchingLibraryOrNull(module, testFramework.parametrizedTestsPatterns(), scope) + +fun findDependencyInjectionLibrary( + module: Module, + springModule: SpringModule, + scope: LibrarySearchScope = LibrarySearchScope.Module +): LibraryOrderEntry? = findMatchingLibraryOrNull(module, springModule.patterns(), scope) -private fun findMatchingLibrary( - project: Project, - testModule: Module, +fun findDependencyInjectionTestLibrary( + module: Module, + springModule: SpringModule, + scope: LibrarySearchScope = LibrarySearchScope.Module +): LibraryOrderEntry? = findMatchingLibraryOrNull(module, springModule.testPatterns(), scope) + +private fun findMatchingLibraryOrNull( + module: Module, patterns: Patterns, scope: LibrarySearchScope, ): LibraryOrderEntry? { val installedLibraries = when (scope) { - LibrarySearchScope.Module -> testModule.allLibraries() - LibrarySearchScope.Project -> project.allLibraries() + LibrarySearchScope.Module -> module.allLibraries() + LibrarySearchScope.Project -> module.project.allLibraries() } + return installedLibraries .matchesFrameworkPatterns(patterns.moduleLibraryPatterns, patterns.libraryPatterns) } diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryUtils.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryUtils.kt index 4ba0d35148..13242c2c5f 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryUtils.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/LibraryUtils.kt @@ -25,20 +25,6 @@ fun Module.allLibraries(): List { val moduleRootManager = ModuleRootManager.getInstance(this) return allLibraries(moduleRootManager.orderEntries()) } - -fun String.parseVersion(): String? { - val lastSemicolon = lastIndexOf(':') - val version = substring(lastSemicolon + 1) - - if (lastSemicolon == -1 || - version.split('.').any { it.toIntOrNull() == null } - ) { - return null - } - - return version -} - fun List.matchesAnyOf(patterns: List): LibraryOrderEntry? = firstOrNull { entry -> patterns.any { pattern -> 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 eb371dc4e8..5f18c2a0cb 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 @@ -4,8 +4,6 @@ import org.utbot.common.PathUtil.toPath import org.utbot.common.WorkaroundReason import org.utbot.common.workaround import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.intellij.plugin.ui.CommonErrorNotifier -import org.utbot.intellij.plugin.ui.UnsupportedJdkNotifier import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.externalSystem.model.ProjectSystemId import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil @@ -14,9 +12,6 @@ import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleUtilCore import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessModuleDir -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 @@ -29,7 +24,7 @@ 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.java.JavaResourceRootType import org.jetbrains.jps.model.module.JpsModuleSourceRootType import org.jetbrains.kotlin.config.KotlinFacetSettingsProvider import org.jetbrains.kotlin.config.TestResourceKotlinRootType @@ -37,18 +32,23 @@ import org.jetbrains.kotlin.platform.TargetPlatformVersion private val logger = KotlinLogging.logger {} -data class TestSourceRoot( - val dir: VirtualFile, - val expectedLanguage: CodegenLanguage -) +interface ITestSourceRoot { + val dirPath: String + val dirName: String + val dir: VirtualFile? + val expectedLanguage : CodegenLanguage +} -/** - * @return jdk version of the module - */ -fun Module.jdkVersion(): JavaSdkVersion { - val moduleRootManager = ModuleRootManager.getInstance(this) - val sdk = moduleRootManager.sdk - return jdkVersionBy(sdk) +class TestSourceRoot(override val dir: VirtualFile, override val expectedLanguage: CodegenLanguage) : ITestSourceRoot { + override val dirPath: String = dir.toNioPath().toString() + override val dirName: String = dir.name + + override fun toString() = dirPath + + override fun equals(other: Any?) = + other is TestSourceRoot && dir == other.dir && expectedLanguage == other.expectedLanguage + + override fun hashCode() = 31 * dir.hashCode() + expectedLanguage.hashCode() } /** @@ -65,6 +65,18 @@ fun Module.kotlinTargetPlatform(): TargetPlatformVersion { ?.singleOrNull() ?: error("Can't determine target platform for module $this") } +/** + * Gets paths to project resources source roots. + * + * E.g. src/main/resources + */ +fun Module.getResourcesPaths(): List = + ModuleRootManager.getInstance(this) + .contentEntries + .flatMap { it.sourceFolders.toList() } + .filter { it.rootType is JavaResourceRootType && !it.isTestSource } + .mapNotNull { it.file?.toNioPath() } + /** * Gets a path to test resources source root. * @@ -107,11 +119,11 @@ private fun findPotentialModulesForTests(project: Project, srcModule: Module): L if (modules.isNotEmpty()) return modules if (srcModule.suitableTestSourceFolders().isEmpty()) { - val modules = mutableSetOf() - ModuleUtilCore.collectModulesDependsOn(srcModule, modules) - modules.remove(srcModule) + val modulesWithTestRoot = mutableSetOf().also { + ModuleUtilCore.collectModulesDependsOn(srcModule, it) + it.remove(srcModule) + }.filter { it.suitableTestSourceFolders().isNotEmpty() } - val modulesWithTestRoot = modules.filter { it.suitableTestSourceFolders().isNotEmpty() } if (modulesWithTestRoot.size == 1) return modulesWithTestRoot } return listOf(srcModule) @@ -136,7 +148,7 @@ fun Module.suitableTestSourceRoots(): List { .mapNotNull { it.testSourceRoot } } -private val SourceFolder.testSourceRoot:TestSourceRoot? +private val SourceFolder.testSourceRoot: TestSourceRoot? get() { val file = file val expectedLanguage = expectedLanguageForTests @@ -159,21 +171,24 @@ private fun Module.suitableTestSourceFolders(): List { private val GRADLE_SYSTEM_ID = ProjectSystemId("GRADLE") val Project.isBuildWithGradle get() = - ModuleManager.getInstance(this).modules.any { - ExternalSystemApiUtil.isExternalSystemAwareModule(GRADLE_SYSTEM_ID, it) - } + ModuleManager.getInstance(this).modules.any { + ExternalSystemApiUtil.isExternalSystemAwareModule(GRADLE_SYSTEM_ID, it) + } -private const val dedicatedTestSourceRootName = "utbot_tests" +const val dedicatedTestSourceRootName = "utbot_tests" -fun Module.addDedicatedTestRoot(testSourceRoots: MutableList, language: CodegenLanguage): VirtualFile? { - // Don't suggest new test source roots for Gradle project where 'unexpected' test roots won't work +fun Module.addDedicatedTestRoot(testSourceRoots: MutableList, language: CodegenLanguage): VirtualFile? { + // Don't suggest new test source roots for a Gradle project where 'unexpected' test roots won't work if (project.isBuildWithGradle) return null // Dedicated test root already exists - if (testSourceRoots.any { root -> root.dir.name == dedicatedTestSourceRootName }) return null + if (testSourceRoots.any { root -> root.dir?.name == dedicatedTestSourceRootName }) return null val moduleInstance = ModuleRootManager.getInstance(this) - val testFolder = moduleInstance.contentEntries.flatMap { it.sourceFolders.toList() } + val testFolder = moduleInstance.contentEntries + .flatMap { it.sourceFolders.toList() } + .filterNot { it.isForGeneratedSources() } .firstOrNull { it.rootType in testSourceRootTypes } + (testFolder?.let { testFolder.file?.parent } ?: testFolder?.contentEntry?.file ?: this.guessModuleDir())?.let { val file = FakeVirtualFile(it, dedicatedTestSourceRootName) @@ -231,7 +246,8 @@ private fun getOrCreateTestResourcesUrl(module: Module, testSourceRoot: VirtualF if (!rootModel.isDisposed && rootModel.isWritable) rootModel.dispose() } } -fun SourceFolder.getModifiableContentEntry() : ContentEntry? { + +private fun SourceFolder.getModifiableContentEntry() : ContentEntry? { return ModuleRootManager.getInstance(contentEntry.rootModel.module).modifiableModel.contentEntries.find { entry -> entry.url == url } } @@ -256,34 +272,6 @@ fun ContentEntry.addSourceRootIfAbsent( } } -/** - * Obtain JDK version and make sure that it is JDK8 or JDK11 - */ -private fun jdkVersionBy(sdk: Sdk?): JavaSdkVersion { - if (sdk == null) { - CommonErrorNotifier.notify("Failed to obtain JDK version of the project") - } - requireNotNull(sdk) - - val jdkVersion = when (sdk.sdkType) { - is JavaSdk -> { - (sdk.sdkType as JavaSdk).getVersion(sdk) - } - is AndroidSdkType -> { - ((sdk.sdkType as AndroidSdkType).dependencyType as JavaSdk).getVersion(sdk) - } - else -> null - } - if (jdkVersion == null) { - CommonErrorNotifier.notify("Failed to obtain JDK version of the project") - } - requireNotNull(jdkVersion) - if (!jdkVersion.isAtLeast(JavaSdkVersion.JDK_1_8)) { - UnsupportedJdkNotifier.notify(jdkVersion.description) - } - return jdkVersion -} - private val SourceFolder.expectedLanguageForTests: CodegenLanguage? get() { // unfortunately, Gradle creates Kotlin test source root with Java source root type, so type is misleading, diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/PsiElementHandler.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/PsiElementHandler.kt index 6455cbbf55..eecfda5945 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/PsiElementHandler.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/PsiElementHandler.kt @@ -3,6 +3,7 @@ package org.utbot.intellij.plugin.ui.utils import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.kotlin.psi.KtFile /** @@ -36,6 +37,14 @@ interface PsiElementHandler { */ fun toPsi(element: PsiElement, clazz: Class): T + /** + * Returns all classes that are declared in the [psiFile] + */ + fun getClassesFromFile(psiFile: PsiFile): List { + return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, classClass) + .map { toPsi(it, PsiClass::class.java) } + } + /** * Get java class of the Class in the corresponding syntax tree (PsiClass, KtClass, e.t.c). */ 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 4d7d19c28f..ecba364675 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 @@ -1,7 +1,8 @@ package org.utbot.intellij.plugin.ui.utils -import org.utbot.framework.plugin.api.CodegenLanguage import com.intellij.openapi.roots.SourceFolder +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil import org.jetbrains.jps.model.java.JavaResourceRootProperties import org.jetbrains.jps.model.java.JavaResourceRootType import org.jetbrains.jps.model.java.JavaSourceRootProperties @@ -11,6 +12,7 @@ import org.jetbrains.kotlin.config.ResourceKotlinRootType import org.jetbrains.kotlin.config.SourceKotlinRootType import org.jetbrains.kotlin.config.TestResourceKotlinRootType import org.jetbrains.kotlin.config.TestSourceKotlinRootType +import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.intellij.plugin.util.IntelliJApiHelper val sourceRootTypes: Set> = setOf(JavaSourceRootType.SOURCE, SourceKotlinRootType) @@ -25,6 +27,7 @@ fun CodegenLanguage.testRootType(): JpsModuleSourceRootType JavaSourceRootType.TEST_SOURCE CodegenLanguage.KOTLIN -> TestSourceKotlinRootType + else -> TestSourceKotlinRootType } /** @@ -34,6 +37,7 @@ fun CodegenLanguage.testResourcesRootType(): JpsModuleSourceRootType JavaResourceRootType.TEST_RESOURCE CodegenLanguage.KOTLIN -> TestResourceKotlinRootType + else -> TestResourceKotlinRootType } /** @@ -47,9 +51,63 @@ fun SourceFolder.isForGeneratedSources(): Boolean { val resourceProperties = jpsElement.getProperties(resourceRootTypes + testResourceRootTypes) val markedGeneratedSources = - properties?.isForGeneratedSources == true && resourceProperties?.isForGeneratedSources == true + properties?.isForGeneratedSources == true || resourceProperties?.isForGeneratedSources == true val androidStudioGeneratedSources = IntelliJApiHelper.isAndroidStudio() && this.file?.path?.contains("build/generated") == true return markedGeneratedSources || androidStudioGeneratedSources } + +const val SRC_MAIN = "src/main/" + +/** + * Sorting test roots, the main idea is to place 'the best' + * test source root the first and to provide readability in general + * @param allTestRoots are all test roots of a project to be sorted + * @param moduleSourcePaths is list of source roots for the module for which we're going to generate tests. + * The first test source root in the resulting list is expected + * to be the closest one to the module based on module source roots. + * @param codegenLanguage is target generation language + */ +fun getSortedTestRoots( + allTestRoots: MutableList, + sourceRootHistory: List, + moduleSourcePaths: List, + codegenLanguage: CodegenLanguage +): MutableList { + var commonModuleSourceDirectory = FileUtil.toSystemIndependentName(moduleSourcePaths.getCommonPrefix()) + //Remove standard suffix that may prevent exact module path matching + commonModuleSourceDirectory = StringUtil.trimEnd(commonModuleSourceDirectory, SRC_MAIN) + + return allTestRoots.distinct().toMutableList().sortedWith( + compareByDescending { + // Heuristics: Dirs with proper code language should go first + it.expectedLanguage == codegenLanguage + }.thenByDescending { + // Heuristics: Dirs from within module 'common' directory should go first + FileUtil.toSystemIndependentName(it.dirPath).startsWith(commonModuleSourceDirectory) + }.thenByDescending { + // Heuristics: dedicated test source root named 'utbot_tests' should go first + it.dirName == dedicatedTestSourceRootName + }.thenByDescending { + // Recent used root should be handy too + sourceRootHistory.indexOf(it.dirPath) + }.thenBy { + // ABC-sorting + it.dirPath + } + ).toMutableList() +} + + +fun List.getCommonPrefix() : String { + var result = "" + for ((i, s) in withIndex()) { + result = if (i == 0) { + s + } else { + StringUtil.commonPrefix(result, s) + } + } + return result +} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/Version.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/Version.kt new file mode 100644 index 0000000000..080f52e590 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/Version.kt @@ -0,0 +1,52 @@ +package org.utbot.intellij.plugin.ui.utils + +/** + * Describes the version of a library. + * Contains three standard components: major, minor and patch. + * + * Major and minor components are always numbers, while patch + * may contain a number with some optional postfix like `-RELEASE` or `.RELEASE`. + * + * Sometimes patch is empty, e.g. for TestNg 7.5 version. + * + * @param plainText is optional and represents whole version as text. + */ +data class Version( + val major: Int, + val minor: Int, + val patch: String, + val plainText: String? = null, +) { + fun isCompatibleWith(another: Version): Boolean { + // Non-numeric versions can't be compared to each other, + // so we cannot be sure that current is compatible unless it's the exact match + if (!hasNumericOrEmptyPatch() || !hasNumericOrEmptyPatch()) { + return plainText == another.plainText + } + + return major > another.major || + major == another.major && minor > another.minor || + major == another.major && minor == another.minor && + (another.patch.isEmpty() || patch.isNotEmpty() && patch.toInt() >= another.patch.toInt()) + } + + fun hasNumericOrEmptyPatch(): Boolean = patch.isEmpty() || patch.toIntOrNull() != null +} + +fun String.parseVersion(): Version? { + val lastSemicolon = lastIndexOf(':') + val versionText = substring(lastSemicolon + 1) + + // Components must be: major, minor and (optional) patch + val versionComponents = versionText.split('.', limit = 3) + + if (versionComponents.size < 2) { + return null + } + + val major = versionComponents[0].toIntOrNull() ?: return null + val minor = versionComponents[1].toIntOrNull() ?: return null + val patch = if (versionComponents.size == 3) versionComponents[2] else "" + + return Version(major, minor, patch, versionText) +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/IdeaThreadingUtil.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/IdeaThreadingUtil.kt new file mode 100644 index 0000000000..526dde9bc6 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/IdeaThreadingUtil.kt @@ -0,0 +1,27 @@ +package org.utbot.intellij.plugin.util + +import com.intellij.openapi.application.ApplicationManager + +fun assertIsDispatchThread() { + ApplicationManager.getApplication().assertIsDispatchThread() +} + +fun assertIsWriteThread() { + ApplicationManager.getApplication().isWriteThread() +} + +fun assertReadAccessAllowed() { + ApplicationManager.getApplication().assertReadAccessAllowed() +} + +fun assertWriteAccessAllowed() { + ApplicationManager.getApplication().assertWriteAccessAllowed() +} + +fun assertIsNonDispatchThread() { + ApplicationManager.getApplication().assertIsNonDispatchThread() +} + +fun assertReadAccessNotAllowed() { + ApplicationManager.getApplication().assertReadAccessNotAllowed() +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt deleted file mode 100644 index 68d50c3079..0000000000 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.utbot.intellij.plugin.util - -import com.intellij.ide.plugins.PluginManagerCore -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.application.runWriteAction -import com.intellij.openapi.extensions.PluginId -import com.intellij.openapi.project.Project -import com.intellij.util.PlatformUtils -import com.intellij.util.ReflectionUtil -import com.intellij.util.concurrency.AppExecutorUtil -import org.jetbrains.kotlin.idea.util.application.invokeLater - -/** - * This object is required to encapsulate Android API usage and grant safe access to it. - */ -object IntelliJApiHelper { - - enum class Target { THREAD_POOL, READ_ACTION, WRITE_ACTION, EDT_LATER } - - fun run(target: Target, runnable: Runnable) { - when (target) { - Target.THREAD_POOL -> AppExecutorUtil.getAppExecutorService().submit { - runnable.run() - } - - Target.READ_ACTION -> runReadAction { runnable.run() } - Target.WRITE_ACTION -> runWriteAction { runnable.run() } - Target.EDT_LATER -> invokeLater { runnable.run() } - } - } - - private val isAndroidPluginAvailable: Boolean = - !PluginManagerCore.isDisabled(PluginId.getId("org.jetbrains.android")) - - fun isAndroidStudio(): Boolean = - isAndroidPluginAvailable && ("AndroidStudio" == PlatformUtils.getPlatformPrefix()) - - fun androidGradleSDK(project: Project): String? { - if (!isAndroidPluginAvailable) return null - try { - val finderClass = Class.forName("com.android.tools.idea.gradle.util.GradleProjectSettingsFinder") - var method = ReflectionUtil.getMethod(finderClass, "findGradleProjectSettings", Project::class.java) ?: return null - val gradleProjectSettings = method.invoke(project) ?: return null - method = ReflectionUtil.getMethod(gradleProjectSettings.javaClass, "getGradleJvm") ?: return null - val gradleJvm = method.invoke(gradleProjectSettings) - return if (gradleJvm is String) gradleJvm else null - } catch (e: Exception) { - return null - } - } -} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/MethodDescriptionHelper.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/MethodDescriptionHelper.kt new file mode 100644 index 0000000000..9110efe995 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/MethodDescriptionHelper.kt @@ -0,0 +1,16 @@ +package org.utbot.intellij.plugin.util + +import com.intellij.psi.PsiMethod +import com.intellij.refactoring.util.classMembers.MemberInfo +import org.utbot.framework.plugin.api.MethodDescription + +fun MemberInfo.methodDescription(): MethodDescription = + (this.member as PsiMethod).methodDescription() + +// Note that rules for obtaining signature here should correlate with KFunction<*>.signature() +private fun PsiMethod.methodDescription() = + MethodDescription(this.name, this.containingClass?.qualifiedName, this.parameterList.parameters.map { + it.type.canonicalText + .replace("...", "[]") //for PsiEllipsisType + .replace(",", ", ") // to fix cases like Pair -> Pair + }) \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt index 5b1f30e098..26cd770797 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/PsiClassHelper.kt @@ -2,30 +2,79 @@ package org.utbot.intellij.plugin.util import com.intellij.psi.PsiClass import com.intellij.psi.PsiMember -import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiMethod import com.intellij.psi.SyntheticElement +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiModifier +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.openapi.project.Project import com.intellij.refactoring.util.classMembers.MemberInfo import com.intellij.testIntegration.TestIntegrationUtils +import org.jetbrains.kotlin.asJava.elements.KtLightMember import org.jetbrains.kotlin.asJava.elements.KtLightMethod import org.jetbrains.kotlin.asJava.elements.isGetter import org.jetbrains.kotlin.asJava.elements.isSetter +import org.jetbrains.kotlin.psi.KtClass import org.utbot.common.filterWhen import org.utbot.framework.UtSettings +import org.utbot.intellij.plugin.models.packageName + +/** + * Used to build binary name from canonical name + * in a similar form which could be obtained by [java.lang.Class.getName] method. + * + * E.g. ```org.example.OuterClass.InnerClass.InnerInnerClass``` -> ```org.example.OuterClass$InnerClass$InnerInnerClass``` + */ +val PsiClass.binaryName: String + get() = + if (packageName.isEmpty()) { + qualifiedName?.replace(".", "$") ?: "" + } else { + val name = + qualifiedName + ?.substringAfter("$packageName.") + ?.replace(".", "$") + ?: error("Binary name construction failed: unable to get qualified name of $this") + "$packageName.$name" + } val PsiMember.isAbstract: Boolean get() = modifierList?.hasModifierProperty(PsiModifier.ABSTRACT)?: false +val PsiMember.isStatic: Boolean + get() = modifierList?.hasModifierProperty(PsiModifier.STATIC)?: false + private val PsiMember.isKotlinGetterOrSetter: Boolean get() { if (this !is KtLightMethod) return false - return isGetter || isSetter + return this.isGetter || this.isSetter + } + +private val PsiMember.isKotlinAndProtected: Boolean + get() = this is KtLightMember<*> && this.hasModifierProperty(PsiModifier.PROTECTED) + +// By now, we think that method in Kotlin is autogenerated iff navigation to its declaration leads to its declaring class +// rather than the method itself (because such methods don't have bodies that we can navigate to) +private val PsiMember.isKotlinAutogeneratedMethod: Boolean + get() = this is KtLightMethod && navigationElement is KtClass + +private val PsiMethod.canBeCalledStatically: Boolean + get() = isStatic || containingClass?.let { it.isStatic && !it.isInterface && !it.isAbstract } ?: throw IllegalStateException("No containing class found for method $this") + +private val PsiMethod.isUntestableMethodOfAbstractOrInterface: Boolean + get() { + val hasAbstractContext = generateSequence(containingClass) { it.containingClass }.any { it.isAbstract || it.isInterface } + return hasAbstractContext && !canBeCalledStatically } private fun Iterable.filterTestableMethods(): List = this - .filterWhen(UtSettings.skipTestGenerationForSyntheticMethods) { it.member !is SyntheticElement } - .filterNot { it.member.isAbstract } + .filterWhen(UtSettings.skipTestGenerationForSyntheticAndImplicitlyDeclaredMethods) { + it.member !is SyntheticElement && !it.member.isKotlinAutogeneratedMethod + } + .filterNot { (it.member as PsiMethod).isUntestableMethodOfAbstractOrInterface } .filterNot { it.member.isKotlinGetterOrSetter } + .filterNot { it.member.isKotlinAndProtected } private val PsiClass.isPrivateOrProtected: Boolean get() = this.modifierList?.let { @@ -57,4 +106,19 @@ fun PsiClass.extractFirstLevelMembers(includeInherited: Boolean): List { o1, o2 -> + val p1 = o1.configuration.isPatternBased() + val p2 = o2.configuration.isPatternBased() + if (p1 xor p2) { + return@Comparator if (p1) -1 else 1 + } + ConfigurationFromContext.COMPARATOR.compare(o1, o2) + } + fun runTestsWithCoverage( model: GenerateTestsModel, testFilesPointers: MutableList>, @@ -84,12 +105,14 @@ class RunConfigurationHelper { ConfigurationContext.SHARED_CONTEXT, myConfigurationContext ) - run(IntelliJApiHelper.Target.THREAD_POOL) { + run(IntelliJApiHelper.Target.THREAD_POOL, indicator = null, "Get run configurations from all producers") { val configurations = ApplicationManager.getApplication().runReadAction(Computable { - myConfigurationContext.configurationsFromContext + return@Computable RunConfigurationProducer.getProducers(model.project) + .mapNotNull { it.findOrCreateConfigurationFromContext(myConfigurationContext) } + .toMutableList().sortedWith(rcComparator) }) - val settings = if (configurations.isNullOrEmpty()) null else configurations[0].configurationSettings + val settings = if (configurations.isEmpty()) null else configurations[0].configurationSettings if (settings != null) { val executor = if (ProgramRunner.getRunner(CoverageExecutor.EXECUTOR_ID, settings.configuration) != null) { ExecutorRegistry.getInstance().getExecutorById(CoverageExecutor.EXECUTOR_ID) ?: DefaultRunExecutor.getRunExecutorInstance() @@ -97,7 +120,11 @@ class RunConfigurationHelper { //Fallback in case 'Code Coverage for Java' plugin is not enabled DefaultRunExecutor.getRunExecutorInstance() } - ApplicationManager.getApplication().invokeLater { + run(IntelliJApiHelper.Target.EDT_LATER, null, "Start run configuration with coverage") { + val configuration = settings.configuration + if (configuration is ConfigurationWithCommandLineShortener) { + configuration.shortenCommandLine = ShortenCommandLine.MANIFEST + } ExecutionUtil.runConfiguration(settings, executor) with(RunManagerEx.getInstanceEx(model.project)) { if (findSettings(settings.configuration) == null) { @@ -113,6 +140,5 @@ class RunConfigurationHelper { } } } - } } \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/SdkUtils.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/SdkUtils.kt new file mode 100644 index 0000000000..21061688bc --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/SdkUtils.kt @@ -0,0 +1,15 @@ +package org.utbot.intellij.plugin.util + +import com.intellij.openapi.module.Module +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.util.lang.JavaVersion + +fun findSdkVersion(module:Module): JavaVersion = + findSdkVersionOrNull(module) ?: error("Cannot define sdk version in module $module") + +fun findSdkVersionOrNull(module: Module): JavaVersion? { + val moduleSdk = ModuleRootManager.getInstance(module).sdk + return JavaVersion.tryParse(moduleSdk?.versionString) +} + + diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/SignaturesHelper.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/SignaturesHelper.kt deleted file mode 100644 index 8cf49a8af5..0000000000 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/SignaturesHelper.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.utbot.intellij.plugin.util - -import com.intellij.psi.PsiMethod -import com.intellij.refactoring.util.classMembers.MemberInfo -import org.utbot.framework.plugin.api.Signature - -fun MemberInfo.signature(): Signature = - (this.member as PsiMethod).signature() - -private fun PsiMethod.signature() = - Signature(this.name, this.parameterList.parameters.map { - it.type.canonicalText - .replace("...", "[]") //for PsiEllipsisType - .replace(",", ", ") // to fix cases like Pair -> Pair - }) \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/SpringConfigurationsHelper.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/SpringConfigurationsHelper.kt new file mode 100644 index 0000000000..1c21adfd16 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/SpringConfigurationsHelper.kt @@ -0,0 +1,126 @@ +package org.utbot.intellij.plugin.util + +import java.io.File + +/** + * This class is a converter between full Spring configuration names and shortened versions. + * + * Shortened versions are represented on UI. + * Full names are used in further analysis in utbot-spring-analyzer. + * + * The idea of this implementation is to append parent directories to the file name until all names become unique. + * + * Example: + * - [["config.web.WebConfig", "config.web2.WebConfig", "config.web.AnotherConfig"]] + * -> + * [["web.WebConfig", "web2.WebConfig", "AnotherConfig"]] + */ +class SpringConfigurationsHelper(val configType: SpringConfigurationType) { + + private val nameToInfo = mutableMapOf() + + inner class NameInfo(val fullName: String) { + val shortenedName: String + get() = innerShortName + + private val pathFragments: MutableList = fullName.split(*configType.separatorsToSplitBy).toMutableList() + private var innerShortName = pathFragments.removeLast() + + fun enlargeShortName(): Boolean { + if (pathFragments.isEmpty()) { + return false + } + + val lastElement = pathFragments.removeLast() + innerShortName = "${lastElement}${configType.separatorToConcatenateBy}$innerShortName" + return true + } + } + + fun restoreFullName(shortenedName: String): String = + nameToInfo + .values + .singleOrNull { it.shortenedName == shortenedName } + ?.fullName + ?: error("Full name of configuration file cannot be restored from shortened name $shortenedName") + + fun shortenSpringConfigNames(fullNames: Set): Map { + fullNames.forEach { nameToInfo[it] = NameInfo(it) } + var nameInfoCollection = nameToInfo.values + + // this cycle continues until all shortenedNames become unique + while (nameInfoCollection.size != nameInfoCollection.distinctBy { it.shortenedName }.size) { + nameInfoCollection = nameInfoCollection.sortedBy { it.shortenedName }.toMutableList() + + var index = 0 + while (index < nameInfoCollection.size) { + val curShortenedPath = nameInfoCollection[index].shortenedName + + // here we search a block of shortened paths that are equivalent + // and must be enlarged with new fragment so on. + var maxIndexWithSamePath = index + while (maxIndexWithSamePath < nameInfoCollection.size) { + if (nameInfoCollection[maxIndexWithSamePath].shortenedName == curShortenedPath) { + maxIndexWithSamePath++ + } else { + break + } + } + + // if the size of this block is one, we should not enlarge it + if (index == maxIndexWithSamePath - 1) { + index++ + continue + } + + // otherwise, enlarge the block of shortened names with one new fragment + for (i in index until maxIndexWithSamePath) { + if (!nameInfoCollection[i].enlargeShortName()) { + return collectShortenedNames() + } + } + + // after enlarging the block, we proceed to search for the next block + index = maxIndexWithSamePath + } + } + + return collectShortenedNames() + } + + private fun collectShortenedNames() = nameToInfo.values.associate { it.fullName to it.shortenedName } + +} + +/* + * Transforms active profile information + * from the form of user input to a list of active profiles. + * + * NOTICE: Current user input form is comma-separated values, but it may be changed later. + */ +fun parseProfileExpression(profileExpression: String?, default: String): Array { + if (profileExpression.isNullOrEmpty()) { + return arrayOf(default) + } + + return profileExpression + .filter { !it.isWhitespace() } + .split(',') + .toTypedArray() +} + +@Deprecated("To be deleted") +enum class SpringConfigurationType( + val separatorsToSplitBy: Array, + val separatorToConcatenateBy: String, +) { + ClassConfiguration( + separatorsToSplitBy = arrayOf("."), + separatorToConcatenateBy = ".", + ), + + FileConfiguration( + separatorsToSplitBy = arrayOf(File.separator), + separatorToConcatenateBy = File.separator, + ), +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtIdeaProjectModelModifier.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtIdeaProjectModelModifier.kt new file mode 100644 index 0000000000..f33eb79230 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtIdeaProjectModelModifier.kt @@ -0,0 +1,95 @@ +package org.utbot.intellij.plugin.util + +import com.intellij.codeInsight.daemon.impl.quickfix.LocateLibraryDialog +import com.intellij.codeInsight.daemon.impl.quickfix.OrderEntryFix +import com.intellij.jarRepository.JarRepositoryManager +import com.intellij.openapi.application.WriteAction +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.ModuleRootModificationUtil +import com.intellij.openapi.roots.OrderRootType +import com.intellij.openapi.roots.impl.IdeaProjectModelModifier +import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar +import com.intellij.openapi.roots.libraries.LibraryUtil +import com.intellij.util.PathUtil +import com.intellij.util.containers.ContainerUtil +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.resolvedPromise +import org.jetbrains.idea.maven.project.MavenProjectsManager +import org.jetbrains.idea.maven.utils.library.RepositoryLibraryProperties +import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor +import org.utbot.intellij.plugin.models.mavenCoordinates +import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle + +class UtIdeaProjectModelModifier(val project: Project) : IdeaProjectModelModifier(project) { + override fun addExternalLibraryDependency( + modules: Collection, + descriptor: ExternalLibraryDescriptor, + scope: DependencyScope + ): Promise? { + if (project.isBuildWithGradle) { + return null + } + for (module in modules) { + if (MavenProjectsManager.getInstance(project).isMavenizedModule(module)) { + return null + } + } + + val defaultRoots = descriptor.libraryClassesRoots + val firstModule = ContainerUtil.getFirstItem(modules) ?: return null + val classesRoots = if (defaultRoots.isNotEmpty()) { + LocateLibraryDialog( + firstModule, + defaultRoots, + descriptor.presentableName + ).showAndGetResult() + } else { + val roots = JarRepositoryManager.loadDependenciesModal( + project, + RepositoryLibraryProperties(JpsMavenRepositoryLibraryDescriptor(descriptor.mavenCoordinates)), + /* loadSources = */ false, + /* loadJavadoc = */ false, + /* copyTo = */ null, + /* repositories = */ null + ) + if (roots.isEmpty()) { + return null + } + roots.filter { orderRoot -> orderRoot.type === OrderRootType.CLASSES } + .map { PathUtil.getLocalPath(it.file) }.toList() + } + if (classesRoots.isNotEmpty()) { + val urls = OrderEntryFix.refreshAndConvertToUrls(classesRoots) + if (canLoadModuleLibrary(modules)) { + ModuleRootModificationUtil.addModuleLibrary( + firstModule, + if (classesRoots.size > 1) descriptor.presentableName else null, + urls, + emptyList(), + scope + ) + } else { + WriteAction.run { + LibraryUtil.createLibrary( + LibraryTablesRegistrar.getInstance().getLibraryTable(project), + descriptor.presentableName + ).let { + val model = it.modifiableModel + urls.forEach { url -> model.addRoot(url, OrderRootType.CLASSES) } + model.commit() + modules.forEach { module -> + ModuleRootModificationUtil.addDependency(module, it, scope, false) + } + } + } + } + } + return resolvedPromise() + } + + private fun canLoadModuleLibrary(modules: Collection) = + modules.size == 1 && !ContainerUtil.getFirstItem(modules).project.isBuildWithGradle +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtMavenProjectModelModifier.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtMavenProjectModelModifier.kt new file mode 100644 index 0000000000..f9ab071b2e --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtMavenProjectModelModifier.kt @@ -0,0 +1,119 @@ +package org.utbot.intellij.plugin.util + +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.fileEditor.FileDocumentManager +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.JavaProjectModelModifier +import com.intellij.openapi.util.Trinity +import com.intellij.openapi.util.text.StringUtil +import com.intellij.pom.java.LanguageLevel +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.util.PsiUtilCore +import com.intellij.psi.xml.XmlFile +import com.intellij.util.ThrowableRunnable +import com.intellij.util.xml.DomUtil +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.rejectedPromise +import org.jetbrains.idea.maven.dom.MavenDomBundle +import org.jetbrains.idea.maven.dom.MavenDomUtil +import org.jetbrains.idea.maven.dom.converters.MavenDependencyCompletionUtil +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.model.MavenConstants +import org.jetbrains.idea.maven.model.MavenId +import org.jetbrains.idea.maven.project.MavenProject +import org.jetbrains.idea.maven.project.MavenProjectsManager + +class UtMavenProjectModelModifier(val project: Project): JavaProjectModelModifier() { + + private val mavenProjectsManager = MavenProjectsManager.getInstance(project) + + override fun addExternalLibraryDependency( + modules: Collection, + descriptor: ExternalLibraryDescriptor, + scope: DependencyScope + ): Promise? { + for (module in modules) { + if (!mavenProjectsManager.isMavenizedModule(module)) { + return null + } + } + + val mavenId = MavenId(descriptor.libraryGroupId, descriptor.libraryArtifactId, descriptor.preferredVersion) + return addDependency(modules, mavenId, descriptor.preferredVersion, scope) + } + + override fun changeLanguageLevel(module: Module, level: LanguageLevel): Promise = rejectedPromise() + + private fun addDependency( + fromModules: Collection, + mavenId: MavenId, + preferredVersion: String?, + scope: DependencyScope, + ): Promise? { + val models: MutableList> = ArrayList(fromModules.size) + val files: MutableList = ArrayList(fromModules.size) + val projectToUpdate: MutableList = ArrayList(fromModules.size) + val mavenScope = getMavenScope(scope) + + for (from in fromModules) { + if (!mavenProjectsManager.isMavenizedModule(from)) return null + val fromProject: MavenProject = mavenProjectsManager.findProject(from) ?: return null + val model = MavenDomUtil.getMavenDomProjectModel(project, fromProject.file) ?: return null + var scopeToSet: String? = null + var version: String? = null + if (mavenId.groupId != null && mavenId.artifactId != null) { + val managedDependency = MavenDependencyCompletionUtil.findManagedDependency( + model, project, + mavenId.groupId!!, + mavenId.artifactId!! + ) + if (managedDependency != null) { + val managedScope = StringUtil.nullize(managedDependency.scope.stringValue, true) + scopeToSet = if (managedScope == null && MavenConstants.SCOPE_COMPILE == mavenScope || + StringUtil.equals(managedScope, mavenScope) + ) null else mavenScope + } + if (managedDependency == null || StringUtil.isEmpty(managedDependency.version.stringValue)) { + version = preferredVersion + scopeToSet = mavenScope + } + } + models.add(Trinity.create(model, MavenId(mavenId.groupId, mavenId.artifactId, version), scopeToSet)) + files.add(DomUtil.getFile(model)) + projectToUpdate.add(fromProject) + } + + WriteCommandAction.writeCommandAction(project, *PsiUtilCore.toPsiFileArray(files)) + .withName(MavenDomBundle.message("fix.add.dependency")).run( + ThrowableRunnable { + val pdm = PsiDocumentManager.getInstance(project) + for (trinity in models) { + val model = trinity.first + val dependency = MavenDomUtil.createDomDependency(model, null, trinity.second) + val ms = trinity.third + if (ms != null) { + dependency.scope.stringValue = ms + } + val document = + pdm.getDocument(DomUtil.getFile(model)) + if (document != null) { + pdm.doPostponedOperationsAndUnblockDocument(document) + FileDocumentManager.getInstance().saveDocument(document) + } + } + }) + + return mavenProjectsManager.forceUpdateProjects(projectToUpdate) + } + + private fun getMavenScope(scope: DependencyScope): String? = when (scope) { + DependencyScope.RUNTIME -> MavenConstants.SCOPE_RUNTIME + DependencyScope.COMPILE -> MavenConstants.SCOPE_COMPILE + DependencyScope.TEST -> MavenConstants.SCOPE_TEST + DependencyScope.PROVIDED -> MavenConstants.SCOPE_PROVIDED + else -> throw IllegalArgumentException(scope.toString()) + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtProjectModelModifier.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtProjectModelModifier.kt deleted file mode 100644 index 9adf33d7e4..0000000000 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/UtProjectModelModifier.kt +++ /dev/null @@ -1,81 +0,0 @@ -package org.utbot.intellij.plugin.util - -import com.intellij.codeInsight.daemon.impl.quickfix.LocateLibraryDialog -import com.intellij.codeInsight.daemon.impl.quickfix.OrderEntryFix -import com.intellij.jarRepository.JarRepositoryManager -import com.intellij.openapi.application.WriteAction -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.ModuleRootModificationUtil -import com.intellij.openapi.roots.OrderRootType -import com.intellij.openapi.roots.impl.IdeaProjectModelModifier -import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar -import com.intellij.openapi.roots.libraries.LibraryUtil -import com.intellij.util.PathUtil -import com.intellij.util.containers.ContainerUtil -import org.jetbrains.concurrency.Promise -import org.jetbrains.concurrency.resolvedPromise -import org.jetbrains.idea.maven.utils.library.RepositoryLibraryProperties -import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor -import org.utbot.intellij.plugin.models.mavenCoordinates - -class UtProjectModelModifier(val project: Project) : IdeaProjectModelModifier(project) { - override fun addExternalLibraryDependency( - modules: Collection, - descriptor: ExternalLibraryDescriptor, - scope: DependencyScope - ): Promise? { - val defaultRoots = descriptor.libraryClassesRoots - val firstModule = ContainerUtil.getFirstItem(modules) ?: return null - val classesRoots = if (defaultRoots.isNotEmpty()) { - LocateLibraryDialog( - firstModule, - defaultRoots, - descriptor.presentableName - ).showAndGetResult() - } else { - val roots = JarRepositoryManager.loadDependenciesModal( - project, - RepositoryLibraryProperties(JpsMavenRepositoryLibraryDescriptor(descriptor.mavenCoordinates)), - /* loadSources = */ false, - /* loadJavadoc = */ false, - /* copyTo = */ null, - /* repositories = */ null - ) - if (roots.isEmpty()) { - return null - } - roots.filter { orderRoot -> orderRoot.type === OrderRootType.CLASSES } - .map { PathUtil.getLocalPath(it.file) }.toList() - } - if (classesRoots.isNotEmpty()) { - val urls = OrderEntryFix.refreshAndConvertToUrls(classesRoots) - if (modules.size == 1) { - ModuleRootModificationUtil.addModuleLibrary( - firstModule, - if (classesRoots.size > 1) descriptor.presentableName else null, - urls, - emptyList(), - scope - ) - } else { - WriteAction.run { - LibraryUtil.createLibrary( - LibraryTablesRegistrar.getInstance().getLibraryTable(project), - descriptor.presentableName - ).let { - val model = it.modifiableModel - urls.forEach { url -> model.addRoot(url, OrderRootType.CLASSES) } - model.commit() - modules.forEach { module -> - ModuleRootModificationUtil.addDependency(module, it, scope, false) - } - } - } - } - } - return resolvedPromise() - } -} \ 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 deleted file mode 100644 index 81f3409165..0000000000 --- a/utbot-intellij/src/main/resources/META-INF/plugin.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - org.utbot.intellij.plugin.id - UnitTestBot - utbot.org - com.intellij.modules.platform - com.intellij.modules.java - org.jetbrains.kotlin - - - org.jetbrains.android - - - - - - - - - - - - - - - - - - - - - - - - - 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. - ]]> -
    - - -
  • Java 11 support.
  • -
  • Smart Fuzzer significantly improves test generation results.
  • -
  • Generated tests have become even more human-readable and user-friendly.
  • -
  • We have enabled Mac OS X platform, give it a try.
  • -
  • The UnitTestBot engine generates SARIF reports.
  • -
  • We have polished plugin UX.
  • -
  • Mocking support is enhanced.
  • -
  • Java Streams, better Java Optional support, Java String support is improved, package-private constructors now are used for the test generation.
  • - - Discover everything mentioned above and much more in this release. - ]]> -
    - -
    diff --git a/utbot-intellij/src/main/resources/META-INF/pluginIcon.svg b/utbot-intellij/src/main/resources/META-INF/pluginIcon.svg deleted file mode 100644 index 08d64eb8d1..0000000000 --- a/utbot-intellij/src/main/resources/META-INF/pluginIcon.svg +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - diff --git a/utbot-intellij/src/main/resources/META-INF/pluginIcon_dark.svg b/utbot-intellij/src/main/resources/META-INF/pluginIcon_dark.svg deleted file mode 100644 index 3cf0a92370..0000000000 --- a/utbot-intellij/src/main/resources/META-INF/pluginIcon_dark.svg +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - diff --git a/utbot-intellij/src/main/resources/log4j2.xml b/utbot-intellij/src/main/resources/log4j2.xml deleted file mode 100644 index 47234d7c78..0000000000 --- a/utbot-intellij/src/main/resources/log4j2.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/data/IdeaBuildSystem.kt b/utbot-intellij/src/test/kotlin/org/utbot/data/IdeaBuildSystem.kt new file mode 100644 index 0000000000..0e602fbff7 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/data/IdeaBuildSystem.kt @@ -0,0 +1,9 @@ +package org.utbot.data + +enum class IdeaBuildSystem (val system: String) { + + INTELLIJ("IntelliJ"), + GRADLE("Gradle"), + MAVEN("Maven") + +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/data/JDKVersion.kt b/utbot-intellij/src/test/kotlin/org/utbot/data/JDKVersion.kt new file mode 100644 index 0000000000..ea4b872573 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/data/JDKVersion.kt @@ -0,0 +1,10 @@ +package org.utbot.data + +enum class JDKVersion (val namePart: String, val number: Int, val supported: Boolean) { + + JDK_1_8(namePart = "1.8", 8, true), + JDK_11(namePart = "11", 11, true), + JDK_17(namePart = "17", 17, true), + JDK_19(namePart = "19", 19, false); + override fun toString() = namePart +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/data/RunInfo.kt b/utbot-intellij/src/test/kotlin/org/utbot/data/RunInfo.kt new file mode 100644 index 0000000000..4e89fba84e --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/data/RunInfo.kt @@ -0,0 +1,20 @@ +package org.utbot.data + +import org.utbot.common.utBotTempDirectory +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Random + +val TEST_RUN_NUMBER: String = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))!! + +val tempDirectoryPath: String = utBotTempDirectory.toAbsolutePath().toString() +const val DEFAULT_DIRECTORY_NAME = "Autotests" +val DEFAULT_DIRECTORY_FULL_PATH = tempDirectoryPath + File.separator + DEFAULT_DIRECTORY_NAME +val CURRENT_RUN_DIRECTORY_FULL_PATH = DEFAULT_DIRECTORY_FULL_PATH + File.separator + TEST_RUN_NUMBER +val CURRENT_RUN_DIRECTORY_END = DEFAULT_DIRECTORY_NAME + File.separator + TEST_RUN_NUMBER + +const val SPRING_PROJECT_NAME = "spring-petclinic" +const val SPRING_PROJECT_URL = "https://github.com/spring-projects/spring-petclinic.git" + +val random: Random = Random() diff --git a/utbot-intellij/src/test/kotlin/org/utbot/dialogs/AddFileToGitDialogFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/AddFileToGitDialogFixture.kt new file mode 100644 index 0000000000..32c0f25842 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/AddFileToGitDialogFixture.kt @@ -0,0 +1,20 @@ +package org.utbot.dialogs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.waitFor +import org.utbot.data.IdeaBuildSystem +import java.time.Duration + +@FixtureName("Add File to Git Dialog") +@DefaultXpath("Dialog type", "//*[@title.key='vfs.listener.add.single.title']") +class AddFileToGitDialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent) : DialogFixture(remoteRobot, remoteComponent) { + + val cancelButton + get() = button( + byXpath("//div[@text.key='button.cancel']")) +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/dialogs/DialogFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/DialogFixture.kt new file mode 100644 index 0000000000..ca643c84cc --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/DialogFixture.kt @@ -0,0 +1,36 @@ +package org.utbot.dialogs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import java.time.Duration + +fun ContainerFixture.dialog( + title: String, + timeout: Duration = Duration.ofSeconds(20), + function: DialogFixture.() -> Unit = {}): DialogFixture = step("Search for dialog with title $title") { + find(DialogFixture.byTitle(title), timeout).apply(function) +} + +@FixtureName("Dialog") +open class DialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) { + + companion object { + @JvmStatic + fun byTitle(title: String) = byXpath("title $title", "//div[@title='$title' and @class='MyDialog']") + } + + val title: String + get() = callJs("component.getTitle();") + + val closeButton + get() = button( + byXpath("//div[@class='DialogRootPane']//div[@class='JButton']")) + +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/dialogs/GetFromVersionControlDialogFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/GetFromVersionControlDialogFixture.kt new file mode 100644 index 0000000000..21509c10ec --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/GetFromVersionControlDialogFixture.kt @@ -0,0 +1,44 @@ +package org.utbot.dialogs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.keyboard +import org.utbot.data.IdeaBuildSystem +import java.awt.event.KeyEvent +import java.io.File + +@FixtureName("Get from Version Control Dialog") +@DefaultXpath("Dialog type", "//*[@title.key='get.from.version.control']") +class GetFromVersionControlDialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent) : DialogFixture(remoteRobot, remoteComponent) { + + val urlInput + get() = textField( + byXpath("//div[@class='BorderlessTextField']")) + + val directoryInput + get() = textField( + byXpath("//div[@class='ExtendableTextField']")) + + val cloneButton + get() = button( + byXpath("//div[@text.key='clone.dialog.clone.button']")) + + val cancelButton + get() = button( + byXpath("//div[@text.key='button.cancel']")) + + fun fillDialog(url: String, location: String = "") { + urlInput.keyboard { enterText(url) } + if (directoryInput.hasText(location).not()) { // firstly change directory, otherwise it won't be updated with project name + directoryInput.click() + keyboard{ + hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_A) + enterText(location.replace(File.separator, File.separator + File.separator)) + } + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/dialogs/NewProjectDialogFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/NewProjectDialogFixture.kt new file mode 100644 index 0000000000..277b033e20 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/NewProjectDialogFixture.kt @@ -0,0 +1,100 @@ +package org.utbot.dialogs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.Keyboard +import com.intellij.remoterobot.utils.keyboard +import com.intellij.remoterobot.utils.waitForIgnoringError +import org.utbot.data.IdeaBuildSystem +import org.utbot.data.JDKVersion +import java.awt.event.KeyEvent +import java.io.File +import java.time.Duration +import java.time.Duration.ofSeconds + +@FixtureName("NewProjectDialog") +@DefaultXpath("type", "//*[contains(@title.key, 'title.new.project')]") +class NewProjectDialogFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) + : DialogFixture(remoteRobot, remoteComponent) { + val keyboard: Keyboard = Keyboard(remoteRobot) + + val wizardsList + get() = jList( + byXpath("//div[@class='JBList']")) + + val nameInput + get() = textField( + byXpath("//div[@class='JBTextField']")) + + val locationInput + get() = textField( + byXpath("//div[@class='ExtendableTextField']")) + + val addSampleCodeCheckbox + get() = checkBox( + byXpath("//div[@text.key='label.project.wizard.new.project.add.sample.code']")) + + val jdkComboBox + get() = comboBox( + byXpath("//div[@class='JdkComboBox']"), + Duration.ofSeconds(10)) + + val jdkList + get() = heavyWeightWindow().itemsList + + val createButton + get() = button( + byXpath("//div[@text.key='button.create']")) + + val cancelButton + get() = button( + byXpath("//div[@text.key='button.cancel']")) + + fun selectWizard(wizardName: String) { + if (title != wizardName) { + wizardsList.findText(wizardName).click() + } + } + + fun selectJDK(jdkVersion: String) { + step("Select JDK: $jdkVersion") { + jdkComboBox.click() + var jdkMatching = jdkVersion + waitForIgnoringError(ofSeconds(20)) { + findAll(byXpath("//*[@text.key='progress.title.detecting.sdks']")).isEmpty() + jdkMatching = jdkList.collectItems().first { it.contains(jdkVersion) } + jdkMatching.isEmpty().not() + } + jdkList.clickItem(jdkMatching) + } + } + + fun fillDialog(projectName: String, + location: String = "", locationPart: String = "", + language: String = "Java", + buildSystem: IdeaBuildSystem = IdeaBuildSystem.INTELLIJ, + jdkVersion: JDKVersion, + addSampleCode: Boolean = true) { + step("Fill New Project dialog") { + nameInput.doubleClick() + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_A) + keyboard.enterText(projectName) + if (!locationInput.hasText{it.text.contains(locationPart)}) { + locationInput.click() + keyboard{ + hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_A) + enterText(location.replace(File.separator, File.separator + File.separator)) + } + } + this.findText(language).click() + this.findText(buildSystem.system).click() + addSampleCodeCheckbox.setValue(addSampleCode) + if (!jdkComboBox.selectedText().contains(jdkVersion.namePart)) { + selectJDK(jdkVersion.namePart) + } + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/dialogs/OpenOrImportProjectDialogFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/OpenOrImportProjectDialogFixture.kt new file mode 100644 index 0000000000..c96e3e3dde --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/OpenOrImportProjectDialogFixture.kt @@ -0,0 +1,38 @@ +package org.utbot.dialogs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.waitFor +import org.utbot.data.IdeaBuildSystem +import java.time.Duration + +@FixtureName("Open or Import Project Dialog") +@DefaultXpath("Dialog type", "//*[@title.key='project.open.select.from.multiple.processors.dialog.title']") +class OpenOrImportProjectDialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent) : DialogFixture(remoteRobot, remoteComponent) { + + val openAsRadioButtons + get() = radioButtons( + byXpath("//div[@class='JBRadioButton']")) + + val okButton + get() = button( + byXpath("//div[@text.key='button.ok']")) + + val cancelButton + get() = button( + byXpath("//div[@text.key='button.cancel']")) + + fun selectBuildSystem(buildSystem: IdeaBuildSystem) { + waitFor { + openAsRadioButtons.isNotEmpty() + } + openAsRadioButtons.filter { + it.text.contains(buildSystem.system) + }[0].click() + okButton.click() + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/dialogs/OpenProjectDialogFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/OpenProjectDialogFixture.kt new file mode 100644 index 0000000000..ec2cf4939e --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/OpenProjectDialogFixture.kt @@ -0,0 +1,21 @@ +package org.utbot.dialogs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath + +@FixtureName("OpenProjectDialog") +@DefaultXpath("Dialog type", "//*[@title.key='title.open.file.or.project']") +class OpenProjectDialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent) : DialogFixture(remoteRobot, remoteComponent) { + + val pathInput + get() = textField( + byXpath("//div[@class='BorderlessTextField']")) + + val okButton + get() = button( + byXpath("//div[@text.key='button.ok']")) +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/dialogs/ProjectStructureDialogFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/ProjectStructureDialogFixture.kt new file mode 100644 index 0000000000..3df95ce70d --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/ProjectStructureDialogFixture.kt @@ -0,0 +1,42 @@ +package org.utbot.dialogs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.keyboard +import com.intellij.remoterobot.utils.waitForIgnoringError +import org.utbot.data.JDKVersion +import java.time.Duration + +@FixtureName("Project Structure Dialog") +@DefaultXpath("Dialog type", "//*[@title.key='project.settings.display.name']") +class ProjectStructureDialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent) : DialogFixture(remoteRobot, remoteComponent) { + + val projectJdkCombobox + get() = comboBox( + byXpath("//div[@class='JdkComboBox']")) + + val moduleSdkCombobox + get() = comboBox( + byXpath("//div[@text.key='module.libraries.target.jdk.module.radio']/../div[@class='JdkComboBox']")) + + val okButton + get() = button( + byXpath("//div[@text.key='button.ok']")) + + fun setProjectSdk(jdkVersion: JDKVersion) { + findText("Project").click() + projectJdkCombobox.click() + waitForIgnoringError(Duration.ofSeconds(5)) { + heavyWeightWindow().itemsList.isShowing + } + keyboard { + enterText(jdkVersion.namePart) + enter() + } + } + +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/dialogs/UnitTestBotDialogFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/UnitTestBotDialogFixture.kt new file mode 100644 index 0000000000..2d256cff99 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/UnitTestBotDialogFixture.kt @@ -0,0 +1,117 @@ +package org.utbot.dialogs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.Keyboard +import java.time.Duration + +@FixtureName("UnitTestBotDialog") +@DefaultXpath("Dialog type", "//*[contains(@title, 'UnitTestBot')]") +class UnitTestBotDialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent) : DialogFixture(remoteRobot, remoteComponent) { + val keyboard: Keyboard = Keyboard(remoteRobot) + + val sdkNotificationLabel + get() = jLabel( + byXpath("//div[@class='SdkNotificationPanel']//div[@defaulticon='fatalError.svg']")) + + val setupSdkLink + get() = actionLink( + byXpath("//div[@class='SdkNotificationPanel']//div[@class='HyperlinkLabel']")) + + val testSourcesRootLabel + get() = jLabel( + byXpath("//div[@text='Test sources root:']")) + + val testSourcesRootComboBox + get() = comboBox( + byXpath("//div[@class='TestFolderComboWithBrowseButton']/div[@class='ComboBox']")) + + val testingFrameworkLabel + get() = jLabel( + byXpath("//div[@text='Testing framework:']")) + + val testingFrameworkComboBox + get() = comboBox( + byXpath("//div[@accessiblename='Testing framework:' and @class='ComboBox']")) + + val mockingStrategyLabel + get() = jLabel( + byXpath("//div[@text='Mocking strategy:']")) + + val mockingStrategyComboBox + get() = comboBox( + byXpath("//div[@accessiblename='Mocking strategy:' and @class='ComboBox']")) + + val mockStaticMethodsCheckbox + get() = checkBox( + byXpath("//div[@text='Mock static methods']")) + + val parameterizedTestsCheckbox + get() = checkBox( + byXpath("//div[@text='Parameterized tests']")) + + val testGenerationTimeoutLabel + get() = jLabel( + byXpath("//div[@text='Test generation timeout:']")) + + val testGenerationTimeoutTextField + get() = textField( + byXpath("//div[@class='JFormattedTextField']")) + + val timeoutSecondsPerClassLabel + get() = jLabel( + byXpath("//div[@text='seconds per class']")) + + val generateTestsForLabel + get() = jLabel( + byXpath("//div[@text='Generate tests for:']")) + + val memberListTable + get() = remoteRobot.find(byXpath("//div[@class='MemberSelectionTable']"), + Duration.ofSeconds(5) + ) + + val generateTestsButton + get() = button( + byXpath("//div[@class='MainButton']")) + + val arrowOnGenerateTestsButton + get() = button( + byXpath("//div[@class='JBOptionButton' and @text='Generate Tests']//div[@class='ArrowButton']")) + + val buttonsList + get() = heavyWeightWindow().itemsList + + + // Spring-specific elements + val springConfigurationLabel + get() = jLabel( + byXpath("//div[@text='Spring configuration:']")) + + val springConfigurationComboBox + get() = comboBox( + byXpath("//div[@accessiblename='Spring configuration:' and @class='ComboBox']")) + + val springTestsTypeLabel + get() = jLabel( + byXpath("//div[@text='Test type:']")) + + val springTestsTypeComboBox + get() = comboBox( + byXpath("//div[@accessiblename='Test type:' and @class='ComboBox']")) + + val springActiveProfilesLabel + get() = jLabel( + byXpath("//div[@text='Active profile(s):']")) + + val springActiveProfilesTextField + get() = textField( + byXpath("//div[@accessiblename='Active profile(s):' and @class='JBTextField']")) + + val integrationTestsWarningDialog: WarningDialogFixture + get() = remoteRobot.find(byXpath( "//div[@title='Warning']")) +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/dialogs/WarningDialogFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/WarningDialogFixture.kt new file mode 100644 index 0000000000..210d33e687 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/dialogs/WarningDialogFixture.kt @@ -0,0 +1,32 @@ +package org.utbot.dialogs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.Keyboard + +@FixtureName("MyDialog") +@DefaultXpath("type", "//div[@class='DialogRootPane']") +class WarningDialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent) : DialogFixture(remoteRobot, remoteComponent) { + val keyboard: Keyboard = Keyboard(remoteRobot) + + val terminateButton + get() = button( + byXpath("//div[@text.key='button.terminate']")) + + val cancelButton + get() = button( + byXpath("//div[@text.key='button.cancel']")) + + val proceedButton + get() = button( + byXpath("//div[@text='Proceed']")) + + val goBackButton + get() = button( + byXpath("//div[@text='Go Back']")) + +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/elements/ActionMenuFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/elements/ActionMenuFixture.kt new file mode 100644 index 0000000000..6ec17bc84c --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/elements/ActionMenuFixture.kt @@ -0,0 +1,30 @@ +package org.utbot.pages + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.waitFor + +fun RemoteRobot.actionMenu(text: String): ActionMenuFixture { + val xpath = byXpath("text '$text'", "//div[@class='ActionMenu' and @text='$text']") + waitFor { + findAll(xpath).isNotEmpty() + } + return findAll(xpath).first() +} + +fun RemoteRobot.actionMenuItem(text: String): ActionMenuItemFixture { + val xpath = byXpath("text '$text'", "//div[@class='ActionMenuItem' and @text='$text']") + waitFor { + findAll(xpath).isNotEmpty() + } + return findAll(xpath).first() +} + +@FixtureName("ActionMenu") +class ActionMenuFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : ComponentFixture(remoteRobot, remoteComponent) + +@FixtureName("ActionMenuItem") +class ActionMenuItemFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : ComponentFixture(remoteRobot, remoteComponent) diff --git a/utbot-intellij/src/test/kotlin/org/utbot/elements/NotificationFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/elements/NotificationFixture.kt new file mode 100644 index 0000000000..1ba3205e88 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/elements/NotificationFixture.kt @@ -0,0 +1,35 @@ +package org.utbot.elements + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import java.time.Duration.ofSeconds + +@FixtureName("Notification Center Panel") +@DefaultXpath("NotificationCenterPanel type", "//div[@class='NotificationCenterPanel']") +class NotificationFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) { + + val title + get() = jLabel(byXpath("//div[@class='JLabel']"), + ofSeconds(5)) + + val body + get() = remoteRobot.find(byXpath("//div[@class='JEditorPane']"), + ofSeconds(5)) + + val projectLoadButton + get() = button(byXpath("//div[@text.key='unlinked.project.notification.load.action']")) + + // For add file to Git notification + val alwaysAddButton + get() = button(byXpath("//div[contains(@text.key, 'external.files.add.notification.action.add')]")) + + val dontAskAgainButton + get() = button(byXpath("//div[contains(@text.key, 'external.files.add.notification.action.mute')]")) + +} + diff --git a/utbot-intellij/src/test/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtilsTest.kt b/utbot-intellij/src/test/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtilsTest.kt new file mode 100644 index 0000000000..cbc295104f --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtilsTest.kt @@ -0,0 +1,68 @@ +package org.utbot.intellij.plugin.ui.utils + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.CodegenLanguage + +internal class RootUtilsTest { + internal class MockTestSourceRoot(override val dirPath: String) : ITestSourceRoot { + override val dir = null + override val dirName = dirPath.substring(dirPath.lastIndexOf("/") + 1) + override val expectedLanguage = if (dirName == "java") CodegenLanguage.JAVA else CodegenLanguage.KOTLIN + override fun toString()= dirPath + } + + @Test + fun testCommonPrefix() { + val commonPrefix = listOf( + "/UTBotJavaTest/utbot-framework/src/main/java", + "/UTBotJavaTest/utbot-framework/src/main/kotlin", + "/UTBotJavaTest/utbot-framework/src/main/resources" + ).getCommonPrefix() + Assertions.assertEquals("/UTBotJavaTest/utbot-framework/src/main/", commonPrefix) + Assertions.assertTrue(commonPrefix.endsWith(SRC_MAIN)) + } + + @Test + fun testRootSorting() { + val allTestRoots = mutableListOf( + MockTestSourceRoot("/UTBotJavaTest/utbot-analytics/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-analytics/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-analytics-torch/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-cli/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework-api/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework-api/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework-test/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework-test/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-java-fuzzing/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-java-fuzzing/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-gradle/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation-tests/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation-tests/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-intellij/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-maven/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-sample/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-summary/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-summary-tests/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-core/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-core/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-rd/src/test/kotlin"), + ) + val moduleSourcePaths = listOf( + "/UTBotJavaTest/utbot-framework/src/main/java", + "/UTBotJavaTest/utbot-framework/src/main/kotlin", + "/UTBotJavaTest/utbot-framework/src/main/resources", + ) + val sortedTestRoots = getSortedTestRoots( + allTestRoots, + listOf("/UTBotJavaTest/utbot-core/src/test/java"), + moduleSourcePaths, + CodegenLanguage.JAVA + ) + Assertions.assertEquals("/UTBotJavaTest/utbot-framework/src/test/java", sortedTestRoots.first().toString()) + Assertions.assertEquals("/UTBotJavaTest/utbot-core/src/test/java", sortedTestRoots[1].toString()) + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/pages/IdeaFrame.kt b/utbot-intellij/src/test/kotlin/org/utbot/pages/IdeaFrame.kt new file mode 100644 index 0000000000..ee569961bb --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/pages/IdeaFrame.kt @@ -0,0 +1,211 @@ +package org.utbot.pages + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.keyboard +import com.intellij.remoterobot.utils.waitFor +import com.intellij.remoterobot.utils.waitForIgnoringError +import org.assertj.swing.core.MouseButton +import org.utbot.data.IdeaBuildSystem +import org.utbot.dialogs.AddFileToGitDialogFixture +import org.utbot.dialogs.ProjectStructureDialogFixture +import org.utbot.dialogs.UnitTestBotDialogFixture +import org.utbot.dialogs.WarningDialogFixture +import org.utbot.elements.NotificationFixture +import org.utbot.tabs.InspectionViewFixture +import java.awt.event.KeyEvent +import java.time.Duration +import java.time.Duration.ofSeconds + +fun RemoteRobot.idea(function: IdeaFrame.() -> Unit) { + find(timeout = ofSeconds(5)).apply(function) +} + +@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']") +open class IdeaFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) { + + open val buildSystemToUse: IdeaBuildSystem = IdeaBuildSystem.INTELLIJ + + val projectViewTree + get() = find(byXpath("ProjectViewTree", "//div[@class='ProjectViewTree']"), + ofSeconds(10)) + + val projectName + get() = step("Get project name") { return@step callJs("component.getProject().getName()") } + + val menuBar: JMenuBarFixture + get() = step("Menu...") { + return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType()) + } + + val inlineProgressTextPanel + get() = remoteRobot.find(byXpath("//div[@class='InlineProgressPanel']//div[@class='TextPanel']"), + ofSeconds(5)) + + val statusTextPanel + get() = remoteRobot.find(byXpath("//div[@class='StatusPanel']//div[@class='TextPanel']"), + ofSeconds(10)) + + val buildResultInEditor + get() = remoteRobot.find(byXpath("//div[@class='TrafficLightButton']"), + ofSeconds(20)) + + val buildResult + get() = textField(byXpath("//div[contains(@accessiblename.key, 'editor.accessible.name')]"), + ofSeconds(20)) + + // Notifications + val ideError + get() = remoteRobot.find(byXpath( "//div[@class='NotificationCenterPanel'][.//div[@accessiblename.key='error.new.notification.title']]"), + ofSeconds(10)) + + val utbotNotification + get() = remoteRobot.find(byXpath( "//div[@class='NotificationCenterPanel' and contains(.,'UnitTestBot')]"), + ofSeconds(10)) + + val loadProjectNotification + get() = remoteRobot.find(byXpath( "//div[@class='NotificationActionPanel' and contains(.,'Load')]"), + ofSeconds(20)) + + val addToGitNotification + get() = remoteRobot.find(byXpath( "//div[@class='NotificationCenterPanel' and contains(.,'Git')]"), + ofSeconds(10)) + + val inspectionsView + get() = remoteRobot.find(InspectionViewFixture::class.java) + + val problemsTabButton + get() = button( byXpath("//div[contains(@text.key, 'toolwindow.stripe.Problems_View')]")) + + // Dialogs + val unitTestBotDialog + get() = remoteRobot.find(UnitTestBotDialogFixture::class.java) + + val projectStructureDialog + get() = remoteRobot.find(ProjectStructureDialogFixture::class.java) + + val addFileToGitDialog + get() = remoteRobot.find(AddFileToGitDialogFixture::class.java) + + @JvmOverloads + fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) { + step("Wait for smart mode") { + waitFor(duration = timeout, interval = ofSeconds(5)) { + runCatching { isDumbMode().not() }.getOrDefault(false) + } + function() + step("..wait for smart mode again") { + waitFor(duration = timeout, interval = ofSeconds(5)) { + isDumbMode().not() + } + } + } + } + + fun isDumbMode(): Boolean { + return callJs(""" + const frameHelper = com.intellij.openapi.wm.impl.ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + project ? com.intellij.openapi.project.DumbService.isDumb(project) : true + } else { + true + } + """, true) + } + + fun closeProject() { + if (remoteRobot.isMac()) { + keyboard { + hotKey(KeyEvent.VK_SHIFT, KeyEvent.VK_META, KeyEvent.VK_A) + enterText("Close Project") + enter() + } + } else { + menuBar.select("File", "Close Project") + } + try { + remoteRobot.find(WarningDialogFixture::class.java, ofSeconds(1)) + .terminateButton.click() + } catch (ignore: Throwable) {} + } + + fun openUTBotDialogFromProjectViewForClass(classname: String, packageName: String = "") { + step("Call UnitTestBot action") { + waitFor(ofSeconds(200)) { !isDumbMode() } + with(projectViewTree) { + if (hasText(classname).not() && packageName != "") { + findText{it.text.endsWith(packageName)}.doubleClick() + } + findText(classname).click(MouseButton.RIGHT_BUTTON) + } + remoteRobot.actionMenuItem("Generate Tests with UnitTestBot...").click() + } + } + + open fun waitProjectIsBuilt() { + projectViewTree.click() + keyboard { key(KeyEvent.VK_PAGE_UP) } + waitForIgnoringError(ofSeconds(30)) { + projectViewTree.hasText(projectName) + } + waitFor(Duration.ofSeconds(90)) { + !isDumbMode() + } + } + + open fun expandProjectTree() { + with(projectViewTree) { + if (hasText("src").not()) { + findText(projectName).doubleClick() + waitForIgnoringError{ + hasText("src").and(hasText(".idea")) + } + } + } + } + + open fun createNewPackage(packageName: String) { + with(projectViewTree) { + if (hasText("src").not()) { + findText(projectName).doubleClick() + waitFor { hasText("src") } + } + findText("src").click(MouseButton.RIGHT_BUTTON) + } + remoteRobot.actionMenu("New").click() + remoteRobot.actionMenuItem("Package").click() + keyboard { + enterText(packageName) + enter() + } + } + + fun createNewJavaClass(newClassname: String = "Example", + textToClickOn: String = "Main") { + with(projectViewTree) { + findText(textToClickOn).click(MouseButton.RIGHT_BUTTON) + } + remoteRobot.actionMenu("New").click() + remoteRobot.actionMenuItem("Java Class").click() + remoteRobot.keyboard { + enterText(newClassname) + enter() + } + } + + fun openProjectStructureDialog() { + if (remoteRobot.isMac()) { + keyboard { + hotKey(KeyEvent.VK_SHIFT, KeyEvent.VK_META, KeyEvent.VK_A) + enterText("Project Structure...") + enter() + } + } else { + menuBar.select("File", "Project Structure...") + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/pages/IdeaGradleFrame.kt b/utbot-intellij/src/test/kotlin/org/utbot/pages/IdeaGradleFrame.kt new file mode 100644 index 0000000000..11fe5fdce0 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/pages/IdeaGradleFrame.kt @@ -0,0 +1,53 @@ +package org.utbot.pages + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.utils.waitFor +import com.intellij.remoterobot.utils.waitForIgnoringError +import org.utbot.data.IdeaBuildSystem +import java.time.Duration.ofSeconds + +@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']") +class IdeaGradleFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : IdeaFrame(remoteRobot, remoteComponent) { + + override val buildSystemToUse = IdeaBuildSystem.GRADLE + + override fun waitProjectIsBuilt() { + super.waitProjectIsBuilt() + try { + waitFor (ofSeconds(90)) { + inlineProgressTextPanel.isShowing.not() + } + } catch (ignore: Throwable) {} + } + + override fun expandProjectTree() { + with(projectViewTree) { + waitForIgnoringError(ofSeconds(10)) { + hasText(projectName) + } + if (hasText("src").not()) { + findText(projectName).doubleClick() + } + waitForIgnoringError{ + hasText("src") + } + if (hasText("main").not()) { + findText("src").doubleClick() + } + waitForIgnoringError{ + hasText("src").and(hasText("main")) + } + if (hasText("java").not()) { + findText("main").doubleClick() + } + waitForIgnoringError{ + hasText("src").and(hasText("main")).and(hasText("java")) + } + if (hasText {it.text.startsWith("org") || it.text.startsWith("com")}.not()) { + findText("java").doubleClick() + } + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/pages/IdeaMavenFrame.kt b/utbot-intellij/src/test/kotlin/org/utbot/pages/IdeaMavenFrame.kt new file mode 100644 index 0000000000..c555880aa5 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/pages/IdeaMavenFrame.kt @@ -0,0 +1,22 @@ +package org.utbot.pages + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.utils.waitForIgnoringError +import org.utbot.data.IdeaBuildSystem +import java.time.Duration.ofSeconds + +@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']") +class IdeaMavenFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : IdeaFrame(remoteRobot, remoteComponent) { + + override val buildSystemToUse = IdeaBuildSystem.MAVEN + + override fun waitProjectIsBuilt() { + super.waitProjectIsBuilt() + waitForIgnoringError (ofSeconds(60)) { + projectViewTree.hasText("Main.java").not() + projectViewTree.hasText("Main") + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/pages/WelcomeFrame.kt b/utbot-intellij/src/test/kotlin/org/utbot/pages/WelcomeFrame.kt new file mode 100644 index 0000000000..987085e6ae --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/pages/WelcomeFrame.kt @@ -0,0 +1,80 @@ +package org.utbot.pages + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.Keyboard +import org.utbot.data.IdeaBuildSystem +import org.utbot.data.JDKVersion +import org.utbot.dialogs.GetFromVersionControlDialogFixture +import org.utbot.dialogs.NewProjectDialogFixture +import org.utbot.dialogs.OpenOrImportProjectDialogFixture +import org.utbot.dialogs.OpenProjectDialogFixture +import java.io.File +import java.time.Duration + +fun RemoteRobot.welcomeFrame(function: WelcomeFrame.()-> Unit) { + find(WelcomeFrame::class.java, Duration.ofSeconds(10)).apply(function) +} + +@FixtureName("Welcome Frame") +@DefaultXpath("Welcome Frame type", "//div[@class='FlatWelcomeFrame']") +class WelcomeFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) { + val keyboard: Keyboard = Keyboard(remoteRobot) + val ideaVersion //ToDO good locator + get() = jLabel(byXpath("//div[@class='TabbedWelcomeScreen']//div[@class='JLabel' and contains(@text,'20')]")) + + val newProjectLink + get() = actionLink(byXpath("New Project","//div[(@class='MainButton' and @text='New Project') or (@accessiblename='New Project' and @class='JButton')]")) + val openProjectLink + get() = actionLink(byXpath("Open","//div[(@class='MainButton' and @text='Open') or (@accessiblename.key='action.WelcomeScreen.OpenProject.text')]")) + val getFromVSCLink + get() = actionLink(byXpath("Get from VCS","//div[(@class='MainButton' and @text='Get from VCS')]")) + val moreActions + get() = button(byXpath("More Action", "//div[@accessiblename='More Actions']")) + + val recentProjectLinks + get() = jTree(byXpath("//div[@class='CardLayoutPanel']//div[@class='Tree']")) + + val newProjectDialog + get() = remoteRobot.find(NewProjectDialogFixture::class.java) + + val openProjectDialog + get() = remoteRobot.find(OpenProjectDialogFixture::class.java) + + val getFromVersionControlDialog + get() = remoteRobot.find(GetFromVersionControlDialogFixture::class.java) + + val openOrImportProjectDialog + get() = remoteRobot.find(OpenOrImportProjectDialogFixture::class.java, + Duration.ofSeconds(120)) + + fun openProjectByPath(location: String, projectName: String = "") { + val separator = File.separator + val localPath = location + separator + projectName + openProjectLink.click() + keyboard.enterText(localPath.replace(separator, separator + separator)) + openProjectDialog.okButton.click() + } + + fun createNewProject(projectName: String, location: String = "", locationPart: String = "", + language: String = "Java", buildSystem: IdeaBuildSystem = IdeaBuildSystem.INTELLIJ, + jdkVersion: JDKVersion, addSampleCode: Boolean = true) { + newProjectLink.click() + newProjectDialog.selectWizard("New Project") + newProjectDialog.fillDialog( + projectName, location, locationPart, language, + buildSystem, jdkVersion, addSampleCode + ) + newProjectDialog.createButton.click() + } + + fun cloneProjectFromVC(url: String, location: String = "", + buildSystem: IdeaBuildSystem) { + getFromVSCLink.click() + getFromVersionControlDialog.fillDialog(url, location) + getFromVersionControlDialog.cloneButton.click() + openOrImportProjectDialog.selectBuildSystem(buildSystem) + } +} diff --git a/utbot-intellij/src/test/kotlin/org/utbot/samples/CodeSamples.kt b/utbot-intellij/src/test/kotlin/org/utbot/samples/CodeSamples.kt new file mode 100644 index 0000000000..5fe1cc06a1 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/samples/CodeSamples.kt @@ -0,0 +1,40 @@ +package org.utbot.samples + +import com.intellij.remoterobot.fixtures.TextEditorFixture +import com.intellij.remoterobot.utils.keyboard +import org.utbot.data.TEST_RUN_NUMBER +import java.awt.event.KeyEvent + +fun TextEditorFixture.typeAdditionFunction(className: String): String { + editor.selectText(className) + keyboard { + key(KeyEvent.VK_END) + enter() + enterText("public int addition(") + enterText("int a, int b") + key(KeyEvent.VK_END) + enterText(" {") + enter() + enterText("// Test run ${TEST_RUN_NUMBER}") + enter() + enterText("return a + b;") + } + return "@utbot.returnsFrom {@code return a + b;}" +} + +fun TextEditorFixture.typeDivisionFunction(className: String) : String { + editor.selectText(className) + keyboard { + key(KeyEvent.VK_END) + enter() + enterText("public int division(") + enterText("int a, int b") + key(KeyEvent.VK_END) + enterText(" {") + enter() + enterText("// Test run ${TEST_RUN_NUMBER}") + enter() + enterText("return a / b;") + } + return "@utbot.returnsFrom {@code return a / b;}" +} diff --git a/utbot-intellij/src/test/kotlin/org/utbot/steps/IDESteps.kt b/utbot-intellij/src/test/kotlin/org/utbot/steps/IDESteps.kt new file mode 100644 index 0000000000..7119df5ebf --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/steps/IDESteps.kt @@ -0,0 +1,60 @@ +package org.utbot.steps + +import com.intellij.remoterobot.fixtures.JButtonFixture +import com.intellij.remoterobot.fixtures.TextEditorFixture +import com.intellij.remoterobot.fixtures.dataExtractor.contains +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.keyboard +import com.intellij.remoterobot.utils.waitFor +import org.utbot.dialogs.DialogFixture +import org.utbot.dialogs.DialogFixture.Companion.byTitle +import org.utbot.pages.IdeaFrame +import java.awt.event.KeyEvent +import java.time.Duration.ofSeconds + +fun IdeaFrame.autocomplete(text: String) { + step("Autocomplete '" + text + "'") { + keyboard { + enterText(text) + } + heavyWeightWindow(ofSeconds(5)) + .findText(contains(text)) + .click() + keyboard { + enter() + } + } +} + +fun TextEditorFixture.goToLineAndColumn(row: Int, column: Int) { + keyboard { + if (remoteRobot.isMac()) { + hotKey(KeyEvent.VK_META, KeyEvent.VK_L) + } else { + hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_G) + } + enterText("$row:$column") + enter() + } +} + +fun TextEditorFixture.closeAllTabs() { + val closeTabButtons = remoteRobot.findAll(byXpath("//div[@class='EditorTabs']//div[@myicon='close.svg']")) + closeTabButtons.forEach { + it.click() + } +} + +fun IdeaFrame.closeTipOfTheDay() { + step("Close Tip of the Day if it appears") { + waitFor(ofSeconds(20)) { + remoteRobot.findAll(byXpath("//div[@class='MyDialog'][.//div[@text='Running startup activities...']]")) + .isEmpty() + } + try { + find(byTitle ("Tip of the Day")) + .button("Close").click() + } catch (ignore: Throwable) {} + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/tabs/InspectionViewFixture.kt b/utbot-intellij/src/test/kotlin/org/utbot/tabs/InspectionViewFixture.kt new file mode 100644 index 0000000000..b6fc97b370 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/tabs/InspectionViewFixture.kt @@ -0,0 +1,16 @@ +package org.utbot.tabs + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import java.time.Duration.ofSeconds + +@FixtureName("Inspection Results View") +@DefaultXpath("InspectionResultsView type", "//div[@class='InspectionResultsView']") +class InspectionViewFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) { + + val inspectionTree // not all problems, but only inspections tab + get() = find(byXpath("//div[@class='InspectionTree']"), + ofSeconds(10)) +} diff --git a/utbot-intellij/src/test/kotlin/org/utbot/tests/BaseTest.kt b/utbot-intellij/src/test/kotlin/org/utbot/tests/BaseTest.kt new file mode 100644 index 0000000000..becf8db9eb --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/tests/BaseTest.kt @@ -0,0 +1,92 @@ +package org.utbot.tests + +import com.intellij.remoterobot.RemoteRobot +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.provider.Arguments +import org.utbot.data.IdeaBuildSystem +import org.utbot.data.JDKVersion +import org.utbot.pages.* +import org.utbot.utils.RemoteRobotExtension +import org.utbot.utils.StepsLogger +import java.time.Duration.ofSeconds + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ExtendWith(RemoteRobotExtension::class) +open class BaseTest { + fun getIdeaFrameForBuildSystem(remoteRobot: RemoteRobot, ideaBuildSystem: IdeaBuildSystem): IdeaFrame { + return when (ideaBuildSystem) { + IdeaBuildSystem.INTELLIJ -> remoteRobot.find(IdeaFrame::class.java, ofSeconds(10)) + IdeaBuildSystem.GRADLE -> remoteRobot.find(IdeaGradleFrame::class.java, ofSeconds(10)) + IdeaBuildSystem.MAVEN -> remoteRobot.find(IdeaMavenFrame::class.java, ofSeconds(10)) + } + } + + @BeforeEach + fun `Close each project before test`(remoteRobot: RemoteRobot): Unit = with(remoteRobot) { + try { + idea { + closeProject() + } + } catch (ignore: Throwable) {} + } + + @AfterEach + fun `Close each project after test`(remoteRobot: RemoteRobot): Unit = with(remoteRobot) { + idea { + closeProject() + } + } + + companion object { + @BeforeAll + @JvmStatic + fun init(remoteRobot: RemoteRobot) { + StepsLogger.init() + } + + internal val supportedProjectsList: List = + addPairsToList(true) + private val unsupportedProjectsList: List = + addPairsToList(false) + + @JvmStatic + fun supportedProjectsProvider(): List { + return supportedProjectsList + } + + @JvmStatic + fun unsupportedProjectsProvider(): List { + return unsupportedProjectsList + } + + @JvmStatic + fun allProjectsProvider(): List { + return supportedProjectsList + unsupportedProjectsList + } + + @JvmStatic + private fun addPairsToList(supported: Boolean): List { + val ideaBuildSystems = IdeaBuildSystem.values() + ideaBuildSystems.shuffle() + var j = 0 + + val listOfArguments: MutableList = mutableListOf() + JDKVersion.values().toMutableList().filter { + it.supported == supported + }.forEach { + listOfArguments.add( + Arguments.of(ideaBuildSystems[j], it) //each (un)supported JDK with a random build system + ) + j++ + if (j >= ideaBuildSystems.size) { + j = 0 + } + } + return listOfArguments + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/tests/CreateProjects.kt b/utbot-intellij/src/test/kotlin/org/utbot/tests/CreateProjects.kt new file mode 100644 index 0000000000..82d34b8dbe --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/tests/CreateProjects.kt @@ -0,0 +1,68 @@ +package org.utbot.tests + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.utils.waitFor +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import org.utbot.data.* +import org.utbot.pages.welcomeFrame +import java.io.File +import java.time.Duration + +@Order(1) +class CreateProjects : BaseTest() { + @ParameterizedTest(name = "Create {0} project with JDK {1}") + @Tags(Tag("Setup"), Tag("Java"), Tag("UTBot")) + @MethodSource("allProjectsProvider") + fun createProjectWithJDK( + ideaBuildSystem: IdeaBuildSystem, jdkVersion: JDKVersion, + remoteRobot: RemoteRobot + ) { + val newProjectName = ideaBuildSystem.system + jdkVersion.number + remoteRobot.welcomeFrame { + createNewProject( + projectName = newProjectName, + buildSystem = ideaBuildSystem, + jdkVersion = jdkVersion, + location = CURRENT_RUN_DIRECTORY_FULL_PATH, + locationPart = CURRENT_RUN_DIRECTORY_END + ) + } + val ideaFrame = getIdeaFrameForBuildSystem(remoteRobot, ideaBuildSystem) + return with(ideaFrame) { + waitProjectIsBuilt() + waitFor(Duration.ofSeconds(90)) { + !isDumbMode() + } + } + } + + @Test + @DisplayName("Clone Spring project") + @Tags(Tag("Setup"), Tag("Java"), Tag("Spring"), Tag("UTBot")) + fun cloneSpringProject(remoteRobot: RemoteRobot): Unit = with(remoteRobot) { + welcomeFrame { + cloneProjectFromVC( + SPRING_PROJECT_URL, + CURRENT_RUN_DIRECTORY_FULL_PATH + File.separator + SPRING_PROJECT_NAME, + IdeaBuildSystem.MAVEN + ) + } + with (getIdeaFrameForBuildSystem(remoteRobot, IdeaBuildSystem.GRADLE)) { + try { + loadProjectNotification.projectLoadButton.click() + waitProjectIsBuilt() + } catch (ignore: Throwable) {} + openProjectStructureDialog() + projectStructureDialog.setProjectSdk(JDKVersion.JDK_17) + projectStructureDialog.okButton.click() + waitProjectIsBuilt() + expandProjectTree() + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/tests/SpringUTBotActionTest.kt b/utbot-intellij/src/test/kotlin/org/utbot/tests/SpringUTBotActionTest.kt new file mode 100644 index 0000000000..c38b97b00e --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/tests/SpringUTBotActionTest.kt @@ -0,0 +1,222 @@ +package org.utbot.tests + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.utils.waitForIgnoringError +import org.assertj.core.api.SoftAssertions +import org.junit.jupiter.api.* +import org.utbot.data.* +import org.utbot.pages.idea +import org.utbot.pages.welcomeFrame +import java.io.File +import java.time.Duration + +class SpringUTBotActionTest : BaseTest() { + + val EXISTING_PACKAGE_NAME = "vet" + val EXISTING_CLASS_NAME = "VetController" + + @BeforeEach + fun openSpringProject(remoteRobot: RemoteRobot): Unit = with(remoteRobot) { + welcomeFrame { + findText { + it.text.endsWith(CURRENT_RUN_DIRECTORY_END + File.separator + SPRING_PROJECT_NAME) + }.click() + } + with (getIdeaFrameForBuildSystem(remoteRobot, IdeaBuildSystem.GRADLE)) { + waitProjectIsBuilt() + try { + loadProjectNotification.projectLoadButton.click() + waitProjectIsBuilt() + } catch (ignore: Throwable) {} + expandProjectTree() + openUTBotDialogFromProjectViewForClass(EXISTING_CLASS_NAME, EXISTING_PACKAGE_NAME) + } + } + + @Test + @DisplayName("Check action dialog UI default state in a Spring project") + @Tags(Tag("Spring"), Tag("Java"), Tag("UnitTestBot"), Tag("UI")) + fun checkSpringDefaultActionDialog(remoteRobot: RemoteRobot) { + with (getIdeaFrameForBuildSystem(remoteRobot, IdeaBuildSystem.GRADLE)) { + with (unitTestBotDialog) { + val softly = SoftAssertions() + softly.assertThat(springConfigurationLabel.isVisible()) + softly.assertThat(springConfigurationComboBox.isShowing) + softly.assertThat(springConfigurationComboBox.selectedText()== "No Configuration") + softly.assertThat(springConfigurationComboBox.listValues() + .containsAll(listOf("No Configuration", "PetClinicApplication", "CacheConfiguration"))) + softly.assertThat(springTestsTypeLabel.isVisible()) + softly.assertThat(springTestsTypeComboBox.isShowing) + softly.assertThat(springTestsTypeComboBox.selectedText() == "Unit tests") + softly.assertThat(springActiveProfilesLabel.isShowing) + softly.assertThat(springActiveProfilesTextField.text =="default") + softly.assertThat(mockingStrategyLabel.isVisible()) + softly.assertThat(mockingStrategyComboBox.selectedText() == "Mock everything outside the class") + softly.assertThat(mockingStrategyComboBox.listValues() + .containsAll(listOf("Do not mock", "Mock everything outside the package", "Mock everything outside the class"))) + softly.assertThat(mockStaticMethodsCheckbox.isShowing) + softly.assertThat(parameterizedTestsCheckbox.isShowing) + softly.assertThat(parameterizedTestsCheckbox.isSelected().not()) + softly.assertThat(testGenerationTimeoutLabel.isShowing) + softly.assertThat(testGenerationTimeoutTextField.text.isNotEmpty()) + softly.assertThat(memberListTable.isShowing) + softly.assertThat(memberListTable.columnCount == 1) + softly.assertThat(memberListTable.rowCount == 2) + softly.assertThat(memberListTable.data.getAll().map { it.text } + .containsAll(listOf("showResourcesVetList():Vets", "showVetList(page:int, model:Model):String"))) + softly.assertThat(generateTestsButton.isEnabled()) + softly.assertThat(arrowOnGenerateTestsButton.isShowing) + arrowOnGenerateTestsButton.click() + softly.assertThat(buttonsList.isShowing) + softly.assertThat(buttonsList.collectItems().containsAll(listOf("Generate Tests", "Generate and Run"))) + softly.assertAll() + } + } + } + + @Test + @DisplayName("Check action dialog UI when Spring configuration is selected") + @Tags(Tag("Spring"), Tag("Java"), Tag("UnitTestBot"), Tag("UI")) + fun checkActionDialogWithSpringConfiguration(remoteRobot: RemoteRobot) { + with (getIdeaFrameForBuildSystem(remoteRobot, IdeaBuildSystem.GRADLE)) { + with (unitTestBotDialog) { + springConfigurationComboBox.click() /* ComboBoxFixture::selectItem doesn't work with heavyWeightWindow */ + heavyWeightWindow().itemsList.clickItem("PetClinicApplication") + val softly = SoftAssertions() + softly.assertThat(springConfigurationComboBox.selectedText()== "PetClinicApplication") + softly.assertThat(springConfigurationComboBox.listValues() + .containsAll(listOf("No Configuration", "PetClinicApplication", "CacheConfiguration"))) + softly.assertThat(springTestsTypeComboBox.selectedText() == "Unit tests") + softly.assertThat(springActiveProfilesTextField.text =="default") + softly.assertThat(generateTestsButton.isEnabled()) + softly.assertAll() + } + } + } + + @Test + @DisplayName("Check action dialog UI when Integration tests are selected") + @Tags(Tag("Spring"), Tag("Java"), Tag("UnitTestBot"), Tag("UI")) + fun checkActionDialogWithIntegrationTests(remoteRobot: RemoteRobot) { + with (getIdeaFrameForBuildSystem(remoteRobot, IdeaBuildSystem.GRADLE)) { + with (unitTestBotDialog) { + springConfigurationComboBox.click() /* ComboBoxFixture::selectItem doesn't work with heavyWeightWindow */ + heavyWeightWindow().itemsList.clickItem("PetClinicApplication") + springTestsTypeComboBox.selectItem("Integration tests") + val softly = SoftAssertions() + softly.assertThat(springConfigurationComboBox.selectedText()== "PetClinicApplication") + softly.assertThat(springConfigurationComboBox.listValues() + .containsAll(listOf("No Configuration", "PetClinicApplication", "CacheConfiguration"))) + softly.assertThat(springTestsTypeComboBox.selectedText() == "Unit tests") + softly.assertThat(springActiveProfilesTextField.text =="default") + softly.assertThat(generateTestsButton.isEnabled()) + softly.assertAll() + } + } + } + + @Order(1) // to close git notification + @Test + @DisplayName("Check Spring Unit tests generation") + @Tags(Tag("Spring"), Tag("Java"), Tag("UnitTestBot"), Tag("Unit tests"), Tag("Generate tests")) + fun checkSpringUnitTestsGeneration(remoteRobot: RemoteRobot) { + with (getIdeaFrameForBuildSystem(remoteRobot, IdeaBuildSystem.GRADLE)) { + with (unitTestBotDialog) { + springConfigurationComboBox.click() /* ComboBoxFixture::selectItem doesn't work with heavyWeightWindow */ + heavyWeightWindow().itemsList.clickItem("PetClinicApplication") + unitTestBotDialog.generateTestsButton.click() + } + waitForIgnoringError (Duration.ofSeconds(10)){ + inlineProgressTextPanel.isShowing + } + waitForIgnoringError (Duration.ofSeconds(60)){ + inlineProgressTextPanel.hasText("Generate test cases for class $EXISTING_CLASS_NAME") + } + waitForIgnoringError (Duration.ofSeconds(90)){ + addToGitNotification.isShowing + } + addToGitNotification.alwaysAddButton.click() // otherwise prompt dialog will be shown for each created test file + waitForIgnoringError(Duration.ofSeconds(90)) { + utbotNotification.title.hasText("UnitTestBot: unit tests generated with warnings") + // because project has several test frameworks + } + val softly = SoftAssertions() + softly.assertThat(utbotNotification.body.hasText("Target: org.springframework.samples.petclinic.vet.VetController Overall test methods: 7")) + softly.assertThat(textEditor().editor.text).contains("class ${EXISTING_CLASS_NAME}Test") + softly.assertThat(textEditor().editor.text).contains("@Test\n") + softly.assertThat(textEditor().editor.text).contains("@InjectMocks\n\tprivate VetController vetController;") + softly.assertThat(textEditor().editor.text).contains("@Mock\n\tprivate VetRepository vetRepositoryMock;") + softly.assertThat(textEditor().editor.text).contains("@utbot.classUnderTest {@link ${EXISTING_CLASS_NAME}}") + softly.assertThat(textEditor().editor.text).contains("@utbot.methodUnderTest {@link ${EXISTING_CLASS_NAME}#showResourcesVetList") + softly.assertThat(textEditor().editor.text).contains("@utbot.methodUnderTest {@link ${EXISTING_CLASS_NAME}#showVetList") + softly.assertThat(inspectionsView.inspectionTree.isShowing) + softly.assertThat(inspectionsView.inspectionTree.hasText("Errors detected by UnitTestBot")) + softly.assertThat(inspectionsView.inspectionTree.hasText("${EXISTING_CLASS_NAME}.java")) + buildResultInEditor.rightClick() // to check test class is compilable + softly.assertThat(heavyWeightWindow().data.getAll().toString().contains("error").not()) + problemsTabButton.click() //to close problems view + softly.assertAll() + } + } + + @Test + @DisplayName("Check Spring Integration tests generation") + @Tags(Tag("Spring"), Tag("Java"), Tag("UnitTestBot"), Tag("Integration tests"), Tag("Generate tests")) + fun checkSpringIntegrationTestsGeneration(remoteRobot: RemoteRobot) { + with (getIdeaFrameForBuildSystem(remoteRobot, IdeaBuildSystem.GRADLE)) { + with (unitTestBotDialog) { + springConfigurationComboBox.click() /* ComboBoxFixture::selectItem doesn't work with heavyWeightWindow */ + heavyWeightWindow().itemsList.clickItem("PetClinicApplication") + springTestsTypeComboBox.selectItem("Integration tests") + unitTestBotDialog.generateTestsButton.click() + unitTestBotDialog.integrationTestsWarningDialog.proceedButton.click() + } + waitForIgnoringError (Duration.ofSeconds(10)){ + inlineProgressTextPanel.isShowing + } + waitForIgnoringError (Duration.ofSeconds(60)){ + inlineProgressTextPanel.hasText("Generate test cases for class $EXISTING_CLASS_NAME") + } + waitForIgnoringError(Duration.ofSeconds(90)) { + utbotNotification.title.hasText("UnitTestBot: unit tests generated with warnings") + // because project has several test frameworks + } + val softly = SoftAssertions() + softly.assertThat(utbotNotification.body.hasText("Target: org.springframework.samples.petclinic.vet.VetController Overall test methods: ")) + softly.assertThat(textEditor().editor.text).contains("@SpringBootTest\n") + softly.assertThat(textEditor().editor.text).contains("@BootstrapWith(SpringBootTestContextBootstrapper.class)\n") + softly.assertThat(textEditor().editor.text).contains("@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)\n") + softly.assertThat(textEditor().editor.text).contains("@Transactional\n") + softly.assertThat(textEditor().editor.text).contains("@AutoConfigureTestDatabase\n") + softly.assertThat(textEditor().editor.text).contains("@AutoConfigureMockMvc\n") + softly.assertThat(textEditor().editor.text).contains("class ${EXISTING_CLASS_NAME}Test") + softly.assertThat(textEditor().editor.text).contains("@Test\n") + softly.assertThat(textEditor().editor.text).contains(CONTEXT_LOADS_TEST_TEXT) + softly.assertThat(textEditor().editor.text).contains("///region FUZZER: SUCCESSFUL EXECUTIONS for method showResourcesVetList()") + softly.assertThat(inspectionsView.inspectionTree.isShowing) + softly.assertThat(inspectionsView.inspectionTree.hasText("Errors detected by UnitTestBot")) + softly.assertThat(inspectionsView.inspectionTree.hasText("${EXISTING_CLASS_NAME}.java")) + buildResultInEditor.rightClick() // to check test class is compilable + softly.assertThat(heavyWeightWindow().data.getAll().toString().contains("error").not()) + problemsTabButton.click() //to close problems view + softly.assertAll() + } + } + + @AfterEach + fun closeDialogIfNotClosed (remoteRobot: RemoteRobot): Unit = with(remoteRobot){ + idea { + try { + unitTestBotDialog.closeButton.click() + } catch (ignore: Throwable) {} + } + } + + val CONTEXT_LOADS_TEST_TEXT = """ /** + * This sanity check test fails if the application context cannot start. + */ + @Test + public void contextLoads() { + }""" + +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/tests/UTBotActionTest.kt b/utbot-intellij/src/test/kotlin/org/utbot/tests/UTBotActionTest.kt new file mode 100644 index 0000000000..497d6911fe --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/tests/UTBotActionTest.kt @@ -0,0 +1,83 @@ +package org.utbot.tests + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.utils.waitForIgnoringError +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.SoftAssertions +import org.jetbrains.kotlin.konan.file.File +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import org.utbot.data.* +import org.utbot.pages.* +import org.utbot.samples.typeAdditionFunction +import org.utbot.samples.typeDivisionFunction +import java.time.Duration.ofSeconds + +class UTBotActionTest : BaseTest() { + @ParameterizedTest(name = "Generate tests in {0} project with JDK {1}") + @MethodSource("supportedProjectsProvider") + @Tags(Tag("Java"), Tag("UnitTestBot"), Tag("Positive"), Tag("Generate tests")) + fun checkBasicTestGeneration(ideaBuildSystem: IdeaBuildSystem, jdkVersion: JDKVersion, + remoteRobot: RemoteRobot) { + val createdProjectName = ideaBuildSystem.system + jdkVersion.number + remoteRobot.welcomeFrame { + findText { + it.text.endsWith(CURRENT_RUN_DIRECTORY_END + File.separator + createdProjectName) + }.click() + } + return with (getIdeaFrameForBuildSystem(remoteRobot, ideaBuildSystem)) { + waitProjectIsBuilt() + expandProjectTree() + val newClassName = "Arithmetic" + createNewJavaClass(newClassName, "Main") + val returnsFromTagBody = textEditor().typeDivisionFunction(newClassName) + openUTBotDialogFromProjectViewForClass(newClassName) + unitTestBotDialog.generateTestsButton.click() + waitForIgnoringError (ofSeconds(5)){ + inlineProgressTextPanel.isShowing + } + waitForIgnoringError (ofSeconds(90)){ + inlineProgressTextPanel.hasText("Generate test cases for class $newClassName") + } + waitForIgnoringError(ofSeconds(30)) { + utbotNotification.title.hasText("UnitTestBot: unit tests generated successfully") + } + val softly = SoftAssertions() + softly.assertThat(textEditor().editor.text).contains("class ${newClassName}Test") + softly.assertThat(textEditor().editor.text).contains("@Test\n") + softly.assertThat(textEditor().editor.text).contains("assertEquals(") + softly.assertThat(textEditor().editor.text).contains("@utbot.classUnderTest {@link ${newClassName}}") + softly.assertThat(textEditor().editor.text).contains("@utbot.methodUnderTest {@link ${newClassName}#") + softly.assertThat(textEditor().editor.text).contains(returnsFromTagBody) + //ToDo verify how many tests are generated + //ToDo verify Problems view and Arithmetic exception on it + softly.assertAll() + } + } + + @ParameterizedTest(name = "Check Generate tests button is disabled in {0} project with unsupported JDK {1}") + @MethodSource("unsupportedProjectsProvider") + @Tags(Tag("Java"), Tag("UnitTestBot"), Tag("Negative"), Tag("UI")) + fun checkProjectWithUnsupportedJDK(ideaBuildSystem: IdeaBuildSystem, jdkVersion: JDKVersion, + remoteRobot: RemoteRobot) { + val createdProjectName = ideaBuildSystem.system + jdkVersion.number + remoteRobot.welcomeFrame { + findText { + it.text.endsWith(CURRENT_RUN_DIRECTORY_END + File.separator + createdProjectName) + }.click() + } + return with (getIdeaFrameForBuildSystem(remoteRobot, ideaBuildSystem)) { + waitProjectIsBuilt() + expandProjectTree() + val newClassName = "Arithmetic" + createNewJavaClass(newClassName, "Main") + textEditor().typeAdditionFunction(newClassName) + openUTBotDialogFromProjectViewForClass(newClassName) + assertThat(unitTestBotDialog.generateTestsButton.isEnabled().not()) + assertThat(unitTestBotDialog.sdkNotificationLabel.hasText("SDK version ${jdkVersion.number} is not supported, use 1.8, 11 or 17 instead.")) + assertThat(unitTestBotDialog.setupSdkLink.isShowing) + unitTestBotDialog.closeButton.click() + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/utils/RemoteRobotExtension.kt b/utbot-intellij/src/test/kotlin/org/utbot/utils/RemoteRobotExtension.kt new file mode 100644 index 0000000000..650542c1f2 --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/utils/RemoteRobotExtension.kt @@ -0,0 +1,135 @@ +package org.utbot.utils + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.search.locators.byXpath +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import org.junit.jupiter.api.extension.AfterTestExecutionCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.ParameterContext +import org.junit.jupiter.api.extension.ParameterResolver +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.File +import java.lang.IllegalStateException +import java.lang.reflect.Method +import javax.imageio.ImageIO + +class RemoteRobotExtension : AfterTestExecutionCallback, ParameterResolver { + private val url: String = System.getProperty("remote-robot-url") ?: "http://127.0.0.1:8082" + private val remoteRobot: RemoteRobot = if (System.getProperty("debug-retrofit")?.equals("enable") == true) { + val interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor().apply { + this.level = HttpLoggingInterceptor.Level.BODY + } + val client = OkHttpClient.Builder().apply { + this.addInterceptor(interceptor) + }.build() + RemoteRobot(url, client) + } else { + RemoteRobot(url) + } + private val client = OkHttpClient() + + override fun supportsParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Boolean { + return parameterContext?.parameter?.type?.equals(RemoteRobot::class.java) ?: false + } + + override fun resolveParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Any { + return remoteRobot + } + + override fun afterTestExecution(context: ExtensionContext?) { + val testMethod: Method = context?.requiredTestMethod ?: throw IllegalStateException("test method is null") + val testMethodName = testMethod.name + val testFailed: Boolean = context.executionException?.isPresent ?: false + if (testFailed) { + saveScreenshot(testMethodName) + saveIdeaFrames(testMethodName) + saveHierarchy(testMethodName) + } + } + + private fun saveScreenshot(testName: String) { + fetchScreenShot().save(testName) + } + + private fun saveHierarchy(testName: String) { + val hierarchySnapshot = + saveFile(url, "build/reports", "hierarchy-$testName.html") + if (File("build/reports/styles.css").exists().not()) { + saveFile("$url/styles.css", "build/reports", "styles.css") + } + println("Hierarchy snapshot: ${hierarchySnapshot.absolutePath}") + } + + private fun saveFile(url: String, folder: String, name: String): File { + val response = client.newCall(Request.Builder().url(url).build()).execute() + return File(folder).apply { + mkdirs() + }.resolve(name).apply { + writeText(response.body?.string() ?: "") + } + } + + private fun BufferedImage.save(name: String) { + val bytes = ByteArrayOutputStream().use { b -> + ImageIO.write(this, "png", b) + b.toByteArray() + } + File("build/reports").apply { mkdirs() }.resolve("$name.png").writeBytes(bytes) + } + + private fun saveIdeaFrames(testName: String) { + remoteRobot.findAll(byXpath("//div[@class='IdeFrameImpl']")).forEachIndexed { n, frame -> + val pic = try { + frame.callJs( + """ + importPackage(java.io) + importPackage(javax.imageio) + importPackage(java.awt.image) + const screenShot = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB); + component.paint(screenShot.getGraphics()) + let pictureBytes; + const baos = new ByteArrayOutputStream(); + try { + ImageIO.write(screenShot, "png", baos); + pictureBytes = baos.toByteArray(); + } finally { + baos.close(); + } + pictureBytes; + """, true + ) + } catch (e: Throwable) { + e.printStackTrace() + throw e + } + pic.inputStream().use { + ImageIO.read(it) + }.save(testName + "_" + n) + } + } + + private fun fetchScreenShot(): BufferedImage { + return remoteRobot.callJs( + """ + importPackage(java.io) + importPackage(javax.imageio) + const screenShot = new java.awt.Robot().createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); + let pictureBytes; + const baos = new ByteArrayOutputStream(); + try { + ImageIO.write(screenShot, "png", baos); + pictureBytes = baos.toByteArray(); + } finally { + baos.close(); + } + pictureBytes; + """ + ).inputStream().use { + ImageIO.read(it) + } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/test/kotlin/org/utbot/utils/StepsLogger.kt b/utbot-intellij/src/test/kotlin/org/utbot/utils/StepsLogger.kt new file mode 100644 index 0000000000..a97ddfb70b --- /dev/null +++ b/utbot-intellij/src/test/kotlin/org/utbot/utils/StepsLogger.kt @@ -0,0 +1,15 @@ +package org.utbot.utils + +import com.intellij.remoterobot.stepsProcessing.StepLogger +import com.intellij.remoterobot.stepsProcessing.StepWorker + +object StepsLogger { + private var initialized = false + @JvmStatic + fun init() { + if (initialized.not()) { + StepWorker.registerProcessor(StepLogger()) + initialized = true + } + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/build.gradle.kts b/utbot-java-fuzzing/build.gradle.kts new file mode 100644 index 0000000000..eadcf81b91 --- /dev/null +++ b/utbot-java-fuzzing/build.gradle.kts @@ -0,0 +1,17 @@ +val sootVersion: String by rootProject +val kotlinLoggingVersion: String by rootProject +val rgxgenVersion: String by rootProject +val guavaVersion: String by rootProject + +dependencies { + implementation(project(":utbot-framework-api")) + api(project(":utbot-fuzzing")) + api(project(":utbot-modificators-analyzer")) + + implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { + exclude(group="com.google.guava", module="guava") + } + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation(group = "com.github.curious-odd-man", name = "rgxgen", version = rgxgenVersion) + implementation(group = "com.google.guava", name = "guava", version = guavaVersion) +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedConcreteValue.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedConcreteValue.kt similarity index 100% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedConcreteValue.kt rename to utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedConcreteValue.kt diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedContext.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedContext.kt similarity index 100% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedContext.kt rename to utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedContext.kt diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt similarity index 82% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt rename to utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt index 054a564e1a..e2061ab214 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt @@ -13,13 +13,12 @@ import org.utbot.framework.plugin.api.ExecutableId * @param concreteValues any concrete values to be processed by fuzzer * */ -class FuzzedMethodDescription( +open class FuzzedMethodDescription( val name: String, val returnType: ClassId, val parameters: List, val concreteValues: Collection = emptyList() ) { - /** * Name that can be used to generate test names */ @@ -35,6 +34,21 @@ class FuzzedMethodDescription( */ var packageName: String? = null + /** + * Canonical name. + */ + var canonicalName: String? = null + + /** + * True, if class is nested. + */ + var isNested: Boolean = false + + /** + * True, if method is static, false if it's not and null if it is not defined. + */ + var isStatic: Boolean? = null + /** * Returns parameter name by its index in the signature */ @@ -52,6 +66,11 @@ class FuzzedMethodDescription( */ var fuzzerType: (Int) -> FuzzedType? = { null } + /** + * Returns true if class should be mocked. + */ + var shouldMock: (ClassId) -> Boolean = { false } + /** * Map class id to indices of this class in parameters list. */ diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt new file mode 100644 index 0000000000..1fe47a142c --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt @@ -0,0 +1,20 @@ +package org.utbot.fuzzer + +import org.utbot.framework.plugin.api.ClassId + +/** + * Contains information about classId and generic types, that should be applied. + * + * Note that this class can be replaced by the API mechanism for collecting parametrized types, + * but at the moment it doesn't fully support all necessary operations. + * + * @see ClassId.typeParameters + */ +open class FuzzedType( + val classId: ClassId, + val generics: List = emptyList(), +) { + override fun toString(): String { + return "FuzzedType(classId=$classId, generics=${generics.map { it.classId }})" + } +} diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt new file mode 100644 index 0000000000..fc10480079 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt @@ -0,0 +1,24 @@ +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. + * + * [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 + */ +open class FuzzedValue( + val model: UtModel, + var summary: String? = null, +) + +fun UtModel.fuzzed(block: FuzzedValue.() -> Unit = {}): FuzzedValue = FuzzedValue(this).apply(block) \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt new file mode 100644 index 0000000000..0eb097513a --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/TypeUtils.kt @@ -0,0 +1,108 @@ +package org.utbot.fuzzer + +import com.google.common.reflect.TypeResolver +import com.google.common.reflect.TypeToken +import mu.KotlinLogging +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.util.constructor +import org.utbot.framework.plugin.api.util.executable +import org.utbot.framework.plugin.api.util.isArray +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.method +import java.lang.reflect.GenericArrayType +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.util.Optional + +private val logger = KotlinLogging.logger {} +private val loggedUnresolvedExecutables = mutableSetOf() + +val Type.typeToken: TypeToken<*> get() = TypeToken.of(this) +inline fun typeTokenOf(): TypeToken = object : TypeToken() {} +inline fun jTypeOf(): Type = typeTokenOf().type + +val FuzzedType.jType: Type get() = toType(cache = mutableMapOf()) + +private fun FuzzedType.toType(cache: MutableMap): Type = cache.getOrPut(this) { + when { + generics.isEmpty() -> classId.jClass + classId.isArray && generics.size == 1 -> GenericArrayType { generics.single().toType(cache) } + else -> object : ParameterizedType { + override fun getActualTypeArguments(): Array = + generics.map { it.toType(cache) }.toTypedArray() + + override fun getRawType(): Type = + classId.jClass + + override fun getOwnerType(): Type? = null + } + } +} + +/** + * Returns fully parameterized type, e.g. for `Map` class + * `Map` type is returned, where `K` and `V` are type variables. + */ +fun Class<*>.toTypeParametrizedByTypeVariables(): Type = + if (typeParameters.isEmpty()) this + else object : ParameterizedType { + override fun getActualTypeArguments(): Array = + typeParameters.toList().toTypedArray() + + override fun getRawType(): Type = + this@toTypeParametrizedByTypeVariables + + override fun getOwnerType(): Type? = + declaringClass?.toTypeParametrizedByTypeVariables() + } + +/** + * Returns types of arguments that should be passed to [executableId] for it to return [neededType] and `null` iff + * [executableId] can't return [neededType] (e.g. if it returns `List` while `List` is needed). + * + * For example, if [executableId] is [Optional.of] and [neededType] is `Optional`, + * then one element list containing `String` type is returned. + */ +fun resolveParameterTypes( + executableId: ExecutableId, + neededType: Type +): List? { + return try { + val actualType = when (executableId) { + is MethodId -> executableId.method.genericReturnType + is ConstructorId -> executableId.constructor.declaringClass.toTypeParametrizedByTypeVariables() + } + + val neededClass = neededType.typeToken.rawType + val actualClass = actualType.typeToken.rawType + + if (!neededClass.isAssignableFrom(actualClass)) + return null + + @Suppress("UNCHECKED_CAST") + val actualSuperType = actualType.typeToken.getSupertype(neededClass as Class).type + val typeResolver = try { + TypeResolver().where(actualSuperType, neededType) + } catch (e: Exception) { + // TypeResolver.where() throws an exception when unification of actual & needed types fails + // e.g. when unifying Optional and Optional + return null + } + + // in some cases when bounded wildcards are involved TypeResolver.where() doesn't throw even though types are + // incompatible (e.g. when needed type is `List` while actual super type is `List`) + if (!typeResolver.resolveType(actualSuperType).typeToken.isSubtypeOf(neededType)) + return null + + executableId.executable.genericParameterTypes.map { + typeResolver.resolveType(it) + } + } catch (e: Exception) { + if (loggedUnresolvedExecutables.add(executableId)) + logger.error(e) { "Failed to resolve types for $executableId, using unresolved generic type" } + + executableId.executable.genericParameterTypes.toList() + } +} diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/UtFuzzedExecution.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/UtFuzzedExecution.kt new file mode 100644 index 0000000000..806f3126ec --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/UtFuzzedExecution.kt @@ -0,0 +1,101 @@ +package org.utbot.fuzzer + +import org.utbot.framework.plugin.api.* + +/** + * Fuzzed execution. + * + * Contains: + * - execution parameters, including thisInstance; + * - result; + * - static fields changed during execution; + * - coverage information (instructions) if this execution was obtained from the concrete execution. + * - comments, method names and display names created by utbot-summary module. + */ +class UtFuzzedExecution( + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + result: UtExecutionResult, + coverage: Coverage? = null, + summary: List? = null, + testMethodName: String? = null, + displayName: String? = null, + val fuzzingValues: List? = null, + val fuzzedMethodDescription: FuzzedMethodDescription? = null, + override val instrumentation: List = emptyList(), +) : UtExecution(stateBefore, stateAfter, result, coverage, summary, testMethodName, displayName), UtExecutionWithInstrumentation { + /** + * By design the 'before' and 'after' states contain info about the same fields. + * It means that it is not possible for a field to be present at 'before' and to be absent at 'after'. + * The reverse is also impossible. + */ + val staticFields: Set + get() = stateBefore.statics.keys // TODO: should we keep it for the Fuzzed Execution? + + override fun copy( + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + result: UtExecutionResult, + coverage: Coverage?, + summary: List?, + testMethodName: String?, + displayName: String? + ): UtExecution { + return UtFuzzedExecution( + stateBefore, + stateAfter, + result, + coverage, + summary, + testMethodName, + displayName, + fuzzingValues = fuzzingValues, + fuzzedMethodDescription = fuzzedMethodDescription, + instrumentation = instrumentation, + ) + } + + override fun toString(): String = buildString { + append("UtFuzzedExecution(") + appendLine() + + append(":") + appendLine() + append(stateBefore) + appendLine() + + append(":") + appendLine() + append(stateAfter) + appendLine() + + append(":") + appendLine() + append(result) + append(")") + } + + fun copy( + stateBefore: EnvironmentModels = this.stateBefore, + stateAfter: EnvironmentModels = this.stateAfter, + result: UtExecutionResult = this.result, + coverage: Coverage? = this.coverage, + summary: List? = this.summary, + testMethodName: String? = this.testMethodName, + displayName: String? = this.displayName, + fuzzingValues: List? = this.fuzzingValues, + fuzzedMethodDescription: FuzzedMethodDescription? = this.fuzzedMethodDescription, + instrumentation: List = this.instrumentation, + ): UtExecution = UtFuzzedExecution( + stateBefore = stateBefore, + stateAfter = stateAfter, + result = result, + coverage = coverage, + summary = summary, + testMethodName = testMethodName, + displayName = displayName, + fuzzingValues = fuzzingValues, + fuzzedMethodDescription = fuzzedMethodDescription, + instrumentation = instrumentation, + ) +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt similarity index 83% rename from utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt rename to utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt index 0f05bacf8e..bc50efcfa0 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/objects/AssembleModelUtils.kt @@ -3,27 +3,14 @@ package org.utbot.fuzzer.objects 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.FieldId import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtDirectSetFieldModel 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.voidClassId -import org.utbot.fuzzer.FuzzedValue -import org.utbot.fuzzer.ModelProvider -import org.utbot.fuzzer.hex - - -fun ModelProvider.assembleModel(id: Int, constructorId: ConstructorId, params: List): FuzzedValue { - return UtAssembleModel( - id, - constructorId.classId, - "${constructorId.classId.name}${constructorId.parameters}#" + id.hex(), - UtExecutableCallModel(instance = null, constructorId, params.map { it.model }) - ).fuzzed { - summary = "%var% = ${constructorId.classId.simpleName}(${constructorId.parameters.joinToString { it.simpleName }})" - } -} fun ClassId.create( block: AssembleModelDsl.() -> Unit @@ -38,6 +25,7 @@ class AssembleModelDsl internal constructor( val call = KeyWord.Call val constructor = KeyWord.Constructor(classId) val method = KeyWord.Method(classId) + val field = KeyWord.Field(classId) var id: () -> Int? = { null } var name: (Int?) -> String = { "" } @@ -53,10 +41,15 @@ class AssembleModelDsl internal constructor( infix fun KeyWord.Call.instance(executableId: T) = CallDsl(executableId, false) + infix fun KeyWord.Call.instance(field: T) = FieldDsl(field, false) + infix fun KeyWord.Using.static(executableId: T) = UsingDsl(executableId) infix fun KeyWord.Call.static(executableId: T) = CallDsl(executableId, true) + infix fun KeyWord.Call.static(field: T) = FieldDsl(field, true) + + @Suppress("UNUSED_PARAMETER") infix fun KeyWord.Using.empty(ignored: KeyWord.Constructor) { initialization = { UtExecutableCallModel(null, ConstructorId(classId, emptyList()), emptyList()) } } @@ -73,6 +66,10 @@ class AssembleModelDsl internal constructor( modChain += { UtExecutableCallModel(it, executableId, models.toList()) } } + infix fun FieldDsl.with(model: UtModel) { + modChain += { UtDirectSetFieldModel(it, fieldId, model) } + } + internal fun build(): UtAssembleModel { val objectId = id() return UtAssembleModel( @@ -102,8 +99,14 @@ class AssembleModelDsl internal constructor( return MethodId(classId, name, returns, params) } } + class Field(val classId: ClassId) : KeyWord() { + operator fun invoke(name: String): FieldId { + return FieldId(classId, name) + } + } } class UsingDsl(val executableId: ExecutableId) class CallDsl(val executableId: ExecutableId, val isStatic: Boolean) + class FieldDsl(val fieldId: FieldId, val isStatic: Boolean) } \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt new file mode 100644 index 0000000000..0dc668cc8c --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -0,0 +1,251 @@ +package org.utbot.fuzzing + +import mu.KotlinLogging +import org.utbot.framework.fuzzer.IdentityPreservingIdGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.Instruction +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.* +import org.utbot.fuzzing.providers.* +import org.utbot.fuzzing.utils.Trie +import java.lang.reflect.* +import java.util.concurrent.CancellationException +import java.util.concurrent.TimeUnit +import kotlin.random.Random + +private val logger = KotlinLogging.logger {} + +typealias JavaValueProvider = ValueProvider + +class FuzzedDescription( + val description: FuzzedMethodDescription, + val tracer: Trie, + val typeCache: MutableMap, + val random: Random, + val scope: Scope? = null +) : Description( + description.parameters.mapIndexed { index, classId -> + description.fuzzerType(index) ?: FuzzedType(classId) + } +) { + val constants: Sequence + get() = description.concreteValues.asSequence() + + override fun clone(scope: Scope): FuzzedDescription { + return FuzzedDescription(description, tracer, typeCache, random, scope) + } +} + +fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = listOf( + BooleanValueProvider, + IntegerValueProvider, + FloatValueProvider, + StringValueProvider, + NumberValueProvider, + anyObjectValueProvider(idGenerator), + ArrayValueProvider(idGenerator), + EnumValueProvider(idGenerator), + ListSetValueProvider(idGenerator), + MapValueProvider(idGenerator), + IteratorValueProvider(idGenerator), + EmptyCollectionValueProvider(idGenerator), + DateValueProvider(idGenerator), + VoidValueProvider, + NullValueProvider, +) + +suspend fun runJavaFuzzing( + idGenerator: IdentityPreservingIdGenerator, + methodUnderTest: ExecutableId, + constants: Collection, + names: List, + providers: List = defaultValueProviders(idGenerator), + exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List) -> BaseFeedback, FuzzedType, FuzzedValue> +) { + val random = Random(0) + val classUnderTest = methodUnderTest.classId + val name = methodUnderTest.classId.simpleName + "." + methodUnderTest.name + val returnType = methodUnderTest.returnType + val parameters = methodUnderTest.parameters + + // For a concrete fuzzing run we need to track types we create. + // Because of generics can be declared as recursive structures like `>`, + // we should track them by reference and do not call `equals` and `hashCode` recursively. + val typeCache = hashMapOf() + /** + * To fuzz this instance, the class of it is added into head of parameters list. + * Done for compatibility with old fuzzer logic and should be reworked more robust way. + */ + fun createFuzzedMethodDescription(self: ClassId?) = FuzzedMethodDescription( + name, returnType, listOfNotNull(self) + parameters, constants + ).apply { + compilableName = if (!methodUnderTest.isConstructor) methodUnderTest.name else null + className = classUnderTest.simpleName + canonicalName = classUnderTest.canonicalName + isNested = classUnderTest.isNested + isStatic = methodUnderTest.isStatic + packageName = classUnderTest.packageName + parameterNameMap = { index -> + when { + self != null && index == 0 -> "this" + self != null -> names.getOrNull(index - 1) + else -> names.getOrNull(index) + } + } + fuzzerType = { + try { + when { + self != null && it == 0 -> toFuzzerType(methodUnderTest.executable.declaringClass, typeCache) + self != null -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it - 1], typeCache) + else -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it], typeCache) + } + } catch (_: Throwable) { + null + } + } + shouldMock = { false } + } + + val thisInstance = with(methodUnderTest) { + if (!isStatic && !isConstructor) { classUnderTest } else { null } + } + val tracer = Trie(Instruction::id) + val descriptionWithOptionalThisInstance = FuzzedDescription(createFuzzedMethodDescription(thisInstance), tracer, typeCache, random) + val descriptionWithOnlyParameters = FuzzedDescription(createFuzzedMethodDescription(null), tracer, typeCache, random) + val start = System.nanoTime() + try { + logger.info { "Starting fuzzing for method: $methodUnderTest" } + logger.info { "\tuse thisInstance = ${thisInstance != null}" } + logger.info { "\tparameters = $parameters" } + var totalExecutionCalled = 0 + runFuzzing( + provider = ValueProvider.of(providers), + description = descriptionWithOptionalThisInstance, random, + configuration = Configuration() + ) { _, t -> + totalExecutionCalled++ + if (thisInstance == null) { + exec(null, descriptionWithOnlyParameters, t) + } else { + exec(t.first(), descriptionWithOnlyParameters, t.drop(1)) + } + } + val totalFuzzingTime = System.nanoTime() - start + logger.info { "Finishing fuzzing for method: $methodUnderTest in ${TimeUnit.NANOSECONDS.toMillis(totalFuzzingTime)} ms" } + logger.info { "\tTotal execution called: $totalExecutionCalled" } + } catch (ce: CancellationException) { + val totalFuzzingTime = System.nanoTime() - start + logger.info { "Fuzzing is stopped because of timeout. Total execution time: ${TimeUnit.NANOSECONDS.toMillis(totalFuzzingTime)} ms" } + logger.debug(ce) { "\tStacktrace:" } + } catch (t: Throwable) { + logger.info(t) { "Fuzzing is stopped because of an error" } + } +} + +/** + * Traverse though type hierarchy of this fuzzed type. + * Ignores all set [FuzzedType.generics] of source type. + * + * todo Process types like `Fuzzed[Any, generics = T1, T2]` to match those T1 and T2 types with superclass and interfaces + */ +internal fun FuzzedType.traverseHierarchy(typeCache: MutableMap): Sequence = sequence { + val typeQueue = mutableListOf(this@traverseHierarchy) + var index = 0 + while (typeQueue.isNotEmpty()) { + val next = typeQueue.removeFirst() + if (index++ > 0) { + yield(next) + } + val jClass = next.classId.jClass + val superclass = jClass.genericSuperclass + if (superclass != null) { + typeQueue += toFuzzerType(superclass, typeCache) + } + typeQueue += jClass.genericInterfaces.asSequence().map { toFuzzerType(it, typeCache) } + } +} + +/** + * Resolve a fuzzer type that has class info and some generics. + * + * @param type to be resolved + * @param cache is used to store same [FuzzedType] for same java types + */ +fun toFuzzerType(type: Type, cache: MutableMap): FuzzedType { + return toFuzzerType( + type = type, + classId = { t -> toClassId(t, cache) }, + generics = { t -> toGenerics(t) }, + cache = cache, + ) +} + +/** + * Resolve a fuzzer type that has class info and some generics. + * + * Cache is used to stop recursive call in case of some recursive class definition like: + * + * ``` + * public > call(T type) { ... } + * ``` + * + * @param type to be resolved into a fuzzed type. + * @param classId is a function that produces classId by general type. + * @param generics is a function that produced a list of generics for this concrete type. + * @param cache is used to store all generated types. + */ +private fun toFuzzerType( + type: Type, + classId: (type: Type) -> ClassId, + generics: (parent: Type) -> Array, + cache: MutableMap, +): FuzzedType { + val g = mutableListOf() + val t = type.replaceWithUpperBoundUntilNotTypeVariable() + var target = cache[t] + if (target == null) { + target = FuzzedType(classId(t), g) + cache[t] = target + g += generics(t).map { + toFuzzerType(it, classId, generics, cache) + } + } + return target +} + +internal fun Type.replaceWithUpperBoundUntilNotTypeVariable() : Type { + var type: Type = this + while (type is TypeVariable<*>) { + type = type.bounds.firstOrNull() ?: java.lang.Object::class.java + } + return type +} + +private fun toClassId(type: Type, cache: MutableMap): ClassId { + return when (type) { + is WildcardType -> type.upperBounds.firstOrNull()?.let { toClassId(it, cache) } ?: objectClassId + is GenericArrayType -> { + val genericComponentType = type.genericComponentType + val classId = toFuzzerType(genericComponentType, cache).classId + if (genericComponentType !is GenericArrayType) { + ClassId("[L${classId.name};", classId) + } else { + ClassId("[" + classId.name, classId) + } + } + is ParameterizedType -> (type.rawType as Class<*>).id + is Class<*> -> type.id + else -> error("unknown type: $type") + } +} + +private fun toGenerics(t: Type) : Array { + return when (t) { + is WildcardType -> t.upperBounds.firstOrNull()?.let { toGenerics(it) } ?: emptyArray() + is GenericArrayType -> arrayOf(t.genericComponentType) + is ParameterizedType -> t.actualTypeArguments + is Class<*> -> t.typeParameters + else -> emptyArray() + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Arrays.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Arrays.kt new file mode 100644 index 0000000000..b9bbba8018 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Arrays.kt @@ -0,0 +1,55 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.fuzzer.IdGenerator +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.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.* + +class ArrayValueProvider( + val idGenerator: IdGenerator, +) : JavaValueProvider { + + override fun accept(type: FuzzedType) = type.classId.isArray + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence> { + yield( + Seed.Collection( + construct = Routine.Collection { + UtArrayModel( + id = idGenerator.createId(), + classId = type.classId, + length = it, + constModel = type.classId.elementClassId!!.defaultValueModel(), + stores = hashMapOf(), + ).fuzzed { + summary = "%var% = ${type.classId.elementClassId!!.simpleName}[$it]" + } + }, + modify = Routine.ForEach(listOf(resolveArrayElementType(type))) { self, i, values -> + (self.model as UtArrayModel).stores[i] = values.first().model + } + )) + } + + /** + * Resolves array's element type. In case a generic type is used, like `T[]` the type should pass generics. + * + * For example, List[] returns List. + */ + private fun resolveArrayElementType(arrayType: FuzzedType): FuzzedType = when { + !arrayType.classId.isArray -> error("$arrayType is not array") + arrayType.generics.size == 1 -> arrayType.generics.first() + arrayType.generics.isEmpty() -> FuzzedType( + arrayType.classId.elementClassId ?: error("Element classId of $arrayType is not found") + ) + + else -> error("Array has ${arrayType.generics.size} generic type for ($arrayType), that should not happen") + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Collections.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Collections.kt new file mode 100644 index 0000000000..3a00334d3c --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Collections.kt @@ -0,0 +1,284 @@ +package org.utbot.fuzzing.providers + +import com.google.common.reflect.TypeToken +import org.utbot.framework.fuzzer.IdGenerator +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzer.jType +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.hex +import kotlin.reflect.KClass + +class EmptyCollectionValueProvider( + val idGenerator: IdGenerator +) : JavaValueProvider { + private class Info(val classId: ClassId, val methodName: String, val returnType: ClassId = classId) + + private val unmodifiableCollections = listOf( + Info(java.util.List::class.id, "emptyList"), + Info(java.util.Set::class.id, "emptySet"), + Info(java.util.Map::class.id, "emptyMap"), + Info(java.util.Collection::class.id, "emptyList", returnType = java.util.List::class.id), + Info(java.lang.Iterable::class.id, "emptyList", returnType = java.util.List::class.id), + Info(java.util.Iterator::class.id, "emptyIterator"), + ) + + private val emptyCollections = listOf( + Info(java.util.NavigableSet::class.id, "constructor", java.util.TreeSet::class.id), + Info(java.util.SortedSet::class.id, "constructor", java.util.TreeSet::class.id), + Info(java.util.NavigableMap::class.id, "constructor", java.util.TreeMap::class.id), + Info(java.util.SortedMap::class.id, "constructor", java.util.TreeMap::class.id), + ) + + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence { + unmodifiableCollections + .asSequence() + .filter { it.classId == type.classId } + .forEach { info -> + yieldWith(info.classId, MethodId(java.util.Collections::class.id, info.methodName, info.returnType, emptyList())) + } + emptyCollections + .asSequence() + .filter { it.classId == type.classId } + .forEach { info -> + yieldWith(info.classId, ConstructorId(info.returnType, emptyList())) + } + } + + private suspend fun SequenceScope>.yieldWith(classId: ClassId, executableId: ExecutableId) { + yield(Seed.Recursive( + construct = Routine.Create(executableId.parameters.map { FuzzedType(it) }) { value -> + UtAssembleModel( + id = idGenerator.createId(), + classId = classId, + modelName = "", + instantiationCall = UtExecutableCallModel(null, executableId, value.map { it.model }) + ).fuzzed { + summary = "%var% = ${executableId.classId.simpleName}#${executableId.name}" + } + }, + empty = Routine.Empty { + if (executableId.parameters.isEmpty()) { + UtAssembleModel( + id = idGenerator.createId(), + classId = classId, + modelName = "", + instantiationCall = UtExecutableCallModel(null, executableId, emptyList()) + + ).fuzzed{ + summary = "%var% = ${executableId.classId.simpleName}#${executableId.name}" + } + } else { + nullFuzzedValue(classId) + } + }, + )) + } +} + +class MapValueProvider( + idGenerator: IdGenerator +) : CollectionValueProvider(idGenerator, java.util.Map::class.id) { + + override fun resolveType(description: FuzzedDescription, type: FuzzedType) = sequence { + val (keyGeneric, valueGeneric) = resolveGenericsOfSuperClass>(description, type) + when (type.classId) { + java.util.Map::class.id -> { + if (keyGeneric.classId isSubtypeOf Comparable::class) { + yield(FuzzedType(java.util.TreeMap::class.id, listOf(keyGeneric, valueGeneric))) + } + yield(FuzzedType(java.util.HashMap::class.id, listOf(keyGeneric, valueGeneric))) + } + java.util.NavigableMap::class.id, + java.util.SortedMap::class.id -> { + if (keyGeneric.classId isSubtypeOf Comparable::class) { + yield(FuzzedType(java.util.TreeMap::class.id, listOf(keyGeneric, valueGeneric))) + } + } + else -> yieldConcreteClass(FuzzedType(type.classId, listOf(keyGeneric, valueGeneric))) + } + } + + override fun findMethod(resolvedType: FuzzedType, values: List): MethodId { + return MethodId(resolvedType.classId, "put", objectClassId, listOf(objectClassId, objectClassId)) + } +} + +class ListSetValueProvider( + idGenerator: IdGenerator +) : CollectionValueProvider(idGenerator, java.util.Collection::class.id) { + + override fun resolveType(description: FuzzedDescription, type: FuzzedType) = sequence { + val (generic) = resolveGenericsOfSuperClass>(description, type) + when (type.classId) { + java.util.Queue::class.id, + java.util.Deque::class.id-> { + yield(FuzzedType(java.util.ArrayDeque::class.id, listOf(generic))) + } + java.util.List::class.id -> { + yield(FuzzedType(java.util.ArrayList::class.id, listOf(generic))) + yield(FuzzedType(java.util.LinkedList::class.id, listOf(generic))) + } + java.util.Collection::class.id -> { + yield(FuzzedType(java.util.ArrayList::class.id, listOf(generic))) + yield(FuzzedType(java.util.HashSet::class.id, listOf(generic))) + } + java.util.Set::class.id -> { + if (generic.classId isSubtypeOf Comparable::class) { + yield(FuzzedType(java.util.TreeSet::class.id, listOf(generic))) + } + yield(FuzzedType(java.util.HashSet::class.id, listOf(generic))) + } + java.util.NavigableSet::class.id, + java.util.SortedSet::class.id -> { + if (generic.classId isSubtypeOf Comparable::class) { + yield(FuzzedType(java.util.TreeSet::class.id, listOf(generic))) + } + } + else -> yieldConcreteClass(FuzzedType(type.classId, listOf(generic))) + } + } + + override fun findMethod(resolvedType: FuzzedType, values: List): MethodId { + return MethodId(resolvedType.classId, "add", booleanClassId, listOf(objectClassId)) + } +} + +/** + * Accepts only instances of Collection or Map + */ +abstract class CollectionValueProvider( + private val idGenerator: IdGenerator, + vararg acceptableCollectionTypes: ClassId +) : JavaValueProvider { + + private val acceptableCollectionTypes = acceptableCollectionTypes.toList() + + override fun accept(type: FuzzedType): Boolean { + return with (type.classId) { + acceptableCollectionTypes.any { acceptableCollectionType -> + isSubtypeOf(acceptableCollectionType.kClass) + } + } + } + + protected suspend fun SequenceScope.yieldConcreteClass(type: FuzzedType) { + if (with (type.classId) { !isAbstract && isPublic && (!isInner || isStatic) }) { + val emptyConstructor = type.classId.allConstructors.find { it.parameters.isEmpty() } + if (emptyConstructor != null && emptyConstructor.isPublic) { + yield(type) + } + } + } + + /** + * Types should be resolved with type parameters + */ + abstract fun resolveType(description: FuzzedDescription, type: FuzzedType) : Sequence + + abstract fun findMethod(resolvedType: FuzzedType, values: List) : MethodId + + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence> { + resolveType(description, type).forEach { resolvedType -> + val typeParameter = resolvedType.generics + yield(Seed.Collection( + construct = Routine.Collection { + UtAssembleModel( + id = idGenerator.createId(), + classId = resolvedType.classId, + modelName = "", + instantiationCall = UtExecutableCallModel( + null, + ConstructorId(resolvedType.classId, emptyList()), + emptyList() + ), + modificationsChainProvider = { mutableListOf() } + ).fuzzed { + summary = "%var% = collection" + } + }, + modify = Routine.ForEach(typeParameter) { self, _, values -> + val model = self.model as UtAssembleModel + (model.modificationsChain as MutableList) += UtExecutableCallModel( + model, + findMethod(resolvedType, values), + values.map { it.model } + ) + } + )) + } + } + + protected infix fun ClassId.isSubtypeOf(klass: KClass<*>): Boolean { + // commented code above doesn't work this case: SomeList extends LinkedList {} and Collection +// return isSubtypeOf(another) + return klass.java.isAssignableFrom(this.jClass) + } +} + +class IteratorValueProvider(val idGenerator: IdGenerator) : JavaValueProvider { + override fun accept(type: FuzzedType): Boolean { + return type.classId == Iterator::class.id + } + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> { + val (generic) = resolveGenericsOfSuperClass>(description, type) + return sequenceOf(Seed.Recursive( + construct = Routine.Create(listOf(FuzzedType(iterableClassId, listOf(generic)))) { v -> + val id = idGenerator.createId() + val iterable = when (val model = v.first().model) { + is UtAssembleModel -> model + is UtNullModel -> return@Create v.first() + else -> error("Model can be only UtNullModel or UtAssembleModel, but $model is met") + } + UtAssembleModel( + id = id, + classId = type.classId, + modelName = "iterator#${id.hex()}", + instantiationCall = UtExecutableCallModel( + instance = iterable, + executable = MethodId(iterableClassId, "iterator", type.classId, emptyList()), + params = emptyList() + ) + ).fuzzed { + summary = "%var% = ${iterable.classId.simpleName}#iterator()" + } + }, + empty = Routine.Empty { + val id = idGenerator.createId() + UtAssembleModel( + id = id, + classId = type.classId, + modelName = "emptyIterator#${id.hex()}", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = MethodId(java.util.Collections::class.id, "emptyIterator", type.classId, emptyList()), + params = emptyList() + ) + ).fuzzed { + summary = "%var% = empty iterator" + } + } + )) + } +} + +private inline fun resolveGenericsOfSuperClass( + description: FuzzedDescription, + type: FuzzedType, +): List { + val superClass = T::class.java + return try { + check(superClass.isAssignableFrom(type.classId.jClass)) { "$superClass isn't super class of $type" } + @Suppress("UNCHECKED_CAST") + toFuzzerType( + TypeToken.of(type.jType).getSupertype(superClass as Class).type, + description.typeCache + ).generics + } catch (e: Throwable) { + superClass.typeParameters.map { toFuzzerType(it, description.typeCache) } + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Custom.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Custom.kt new file mode 100644 index 0000000000..cd51c0a6f7 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Custom.kt @@ -0,0 +1,18 @@ +package org.utbot.fuzzing.providers + +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Seed + +interface CustomJavaValueProviderHolder { + val javaValueProvider: JavaValueProvider +} + +object DelegatingToCustomJavaValueProvider : JavaValueProvider { + override fun accept(type: FuzzedType): Boolean = type is CustomJavaValueProviderHolder + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> = + (type as CustomJavaValueProviderHolder).javaValueProvider.generate(description, type) +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Enums.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Enums.kt new file mode 100644 index 0000000000..d5c1de0a39 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Enums.kt @@ -0,0 +1,33 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.fuzzer.IdentityPreservingIdGenerator +import org.utbot.framework.plugin.api.UtEnumConstantModel +import org.utbot.framework.plugin.api.util.isEnum +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Seed + +class EnumValueProvider( + val idGenerator: IdentityPreservingIdGenerator, +) : JavaValueProvider { + override fun accept(type: FuzzedType) = type.classId.isEnum + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence> { + val jClass = type.classId.jClass + if (isAccessible(jClass, description.description.packageName)) { + jClass.enumConstants.filterIsInstance>().forEach { enum -> + val id = idGenerator.getOrCreateIdForValue(enum) + yield(Seed.Simple(UtEnumConstantModel(id, type.classId, enum).fuzzed { + summary = "%var% = $enum" + })) + } + } + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/FuzzerProperties.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/FuzzerProperties.kt new file mode 100644 index 0000000000..d5f6da74f8 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/FuzzerProperties.kt @@ -0,0 +1,8 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.fuzzing.ScopeProperty + +val NULLABLE_PROP = ScopeProperty("Whether or not type can be nullable") + +val SPRING_BEAN_PROP = ScopeProperty<(ClassId) -> List>("Maps java class to its beans") \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt new file mode 100644 index 0000000000..7c7fd73773 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt @@ -0,0 +1,385 @@ +package org.utbot.fuzzing.providers + +import mu.KotlinLogging +import org.utbot.framework.UtSettings +import org.utbot.framework.fuzzer.IdGenerator +import org.utbot.framework.fuzzer.IdentityPreservingIdGenerator +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.UtAssembleModel +import org.utbot.framework.plugin.api.UtDirectSetFieldModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.id +import org.utbot.framework.plugin.api.util.classClassId +import org.utbot.framework.plugin.api.util.constructor +import org.utbot.framework.plugin.api.util.dateClassId +import org.utbot.framework.plugin.api.util.executable +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.isAbstract +import org.utbot.framework.plugin.api.util.isCollectionOrMap +import org.utbot.framework.plugin.api.util.isEnum +import org.utbot.framework.plugin.api.util.isPrimitiveWrapper +import org.utbot.framework.plugin.api.util.isRefType +import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.method +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Scope +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.toFuzzerType +import org.utbot.fuzzing.traverseHierarchy +import org.utbot.fuzzing.utils.hex +import org.utbot.modifications.AnalysisMode +import org.utbot.modifications.FieldInvolvementMode +import org.utbot.modifications.UtBotFieldsModificatorsSearcher +import soot.Scene +import soot.SootClass +import java.lang.reflect.Field +import java.lang.reflect.Member +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.lang.reflect.TypeVariable + +private val logger = KotlinLogging.logger {} + +private fun isKnownTypes(type: ClassId): Boolean { + return type == stringClassId + || type == dateClassId + || type == NumberValueProvider.classId + || type.isCollectionOrMap + || type.isPrimitiveWrapper + || type.isEnum +} + +private fun isIgnored(type: ClassId): Boolean { + return isKnownTypes(type) + || type.isAbstract + || (type.isInner && !type.isStatic) +} + +fun anyObjectValueProvider(idGenerator: IdentityPreservingIdGenerator) = + ObjectValueProvider(idGenerator).letIf(UtSettings.fuzzingImplementationOfAbstractClasses) { ovp -> + ovp.withFallback(AbstractsObjectValueProvider(idGenerator)) + } + +/** + * Marker interface that shows that this [JavaValueProvider] can potentially provide values of + * arbitrary types, unlike type-specific value providers that were designed to provide values of + * few specific popular types (e.g. `List`, `String`, etc.). + */ +interface AnyObjectValueProvider + +class ObjectValueProvider( + val idGenerator: IdGenerator, +) : JavaValueProvider, AnyObjectValueProvider { + + override fun accept(type: FuzzedType) = !isIgnored(type.classId) + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + val classId = type.classId + val constructors = classId.allConstructors + .filter { + isAccessible(it.constructor, description.description.packageName) + } + constructors.forEach { constructorId -> + yield(createValue(classId, constructorId, description)) + } + } + + private fun createValue(classId: ClassId, constructorId: ConstructorId, description: FuzzedDescription): Seed.Recursive = + Seed.Recursive( + construct = Routine.Create(constructorId.executable.genericParameterTypes.map { + toFuzzerType(it, description.typeCache) + }) { values -> + val id = idGenerator.createId() + UtAssembleModel( + id = id, + classId = classId, + modelName = "${constructorId.classId.name}${constructorId.parameters}#" + id.hex(), + instantiationCall = UtExecutableCallModel( + null, + constructorId, + values.map { it.model }), + modificationsChainProvider = { mutableListOf() } + ).fuzzed { + summary = "%var% = ${classId.simpleName}(${constructorId.parameters.joinToString { it.simpleName }})" + } + }, + modify = sequence { + findAccessibleModifiableFields(description, classId, description.description.packageName).forEach { fd -> + when { + fd.canBeSetDirectly -> { + yield(Routine.Call(listOf(fd.type)) { self, values -> + val model = self.model as UtAssembleModel + model.modificationsChain as MutableList += UtDirectSetFieldModel( + model, + FieldId(classId, fd.name), + values.first().model + ) + }) + } + + fd.setter != null && fd.getter != null -> { + yield(Routine.Call(listOf(fd.type)) { self, values -> + val model = self.model as UtAssembleModel + model.modificationsChain as MutableList += UtExecutableCallModel( + model, + fd.setter.executableId, + values.map { it.model }) + }) + } + } + } + }, + empty = nullRoutine(classId) + ) +} + +@Suppress("unused") +object NullValueProvider : JavaValueProvider, AnyObjectValueProvider { + + override fun enrich(description: FuzzedDescription, type: FuzzedType, scope: Scope) { + // any value in static function is ok to fuzz + if (description.description.isStatic == true && scope.recursionDepth == 1) { + scope.putProperty(NULLABLE_PROP, true) + } + // any value except this + if (description.description.isStatic == false && scope.parameterIndex > 0 && scope.recursionDepth == 1) { + scope.putProperty(NULLABLE_PROP, true) + } + } + + override fun accept(type: FuzzedType) = type.classId.isRefType + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence> { + if (description.scope?.getProperty(NULLABLE_PROP) == true) { + yield(Seed.Simple(nullFuzzedValue(classClassId))) + } + } +} + +/** + * Unlike [NullValueProvider] can generate `null` values at any depth. + * + * Intended to be used as a last fallback. + */ +object AnyDepthNullValueProvider : JavaValueProvider, AnyObjectValueProvider { + + override fun accept(type: FuzzedType) = type.classId.isRefType + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequenceOf>(Seed.Simple(nullFuzzedValue(classClassId))) +} + +/** + * Finds and create object from implementations of abstract classes or interfaces. + */ +class AbstractsObjectValueProvider( + val idGenerator: IdGenerator, +) : JavaValueProvider, AnyObjectValueProvider { + + override fun accept(type: FuzzedType) = type.classId.isRefType && !isKnownTypes(type.classId) + + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence> { + val t = try { + Scene.v().getRefType(type.classId.name).sootClass + } catch (ignore: Throwable) { + logger.error(ignore) { "Soot may be not initialized" } + return@sequence + } + fun canCreateClass(sc: SootClass): Boolean { + try { + if (!sc.isConcrete) return false + val packageName = sc.packageName + if (packageName != null) { + if (packageName.startsWith("jdk.internal") || + packageName.startsWith("org.utbot") || + packageName.startsWith("sun.")) + return false + } + val isAnonymousClass = sc.name.matches(""".*\$\d+$""".toRegex()) + if (isAnonymousClass) { + return false + } + val jClass = sc.id.jClass + return isAccessible(jClass, description.description.packageName) && + jClass.declaredConstructors.any { isAccessible(it, description.description.packageName) } && + jClass.let { + // This won't work in case of implementations with generics like `Impl implements A`. + // Should be reworked with accurate generic matching between all classes. + toFuzzerType(it, description.typeCache).traverseHierarchy(description.typeCache).contains(type) + } + } catch (ignore: Throwable) { + return false + } + } + + val implementations = when { + t.isInterface -> Scene.v().fastHierarchy.getAllImplementersOfInterface(t).filter(::canCreateClass) + t.isAbstract -> Scene.v().fastHierarchy.getSubclassesOf(t).filter(::canCreateClass) + else -> emptyList() + } + implementations.shuffled(description.random).take(10).forEach { concrete -> + yield(Seed.Recursive( + construct = Routine.Create(listOf(toFuzzerType(concrete.id.jClass, description.typeCache))) { + it.first() + }, + empty = nullRoutine(type.classId) + )) + } + } +} + +internal class PublicSetterGetter( + val setter: Method, + val getter: Method, +) + +internal class FieldDescription( + val name: String, + val type: FuzzedType, + val canBeSetDirectly: Boolean, + val setter: Method?, + val getter: Method? +) + +internal class MethodDescription( + val name: String, + val parameterTypes: List, + val method: Method +) + +internal fun findAccessibleModifiableFields(description: FuzzedDescription?, classId: ClassId, packageName: String?): List { + return generateSequence(classId.jClass) { it.superclass }.flatMap { jClass -> + jClass.declaredFields.map { field -> + val setterAndGetter = jClass.findPublicSetterGetterIfHasPublicGetter(field, packageName) + FieldDescription( + name = field.name, + type = if (description != null) toFuzzerType( + field.genericType, + description.typeCache + ) else FuzzedType(field.type.id), + canBeSetDirectly = isAccessible( + field, + packageName + ) && !Modifier.isFinal(field.modifiers) && !Modifier.isStatic(field.modifiers), + setter = setterAndGetter?.setter, + getter = setterAndGetter?.getter, + ) + } + }.toList() +} + +internal fun findMethodsToModifyWith( + description: FuzzedDescription, + valueClassId: ClassId, + classUnderTest: ClassId, + ): List { + val packageName = description.description.packageName + + val methodUnderTestName = description.description.name.substringAfter(description.description.className + ".") + val modifyingMethods = findModifyingMethodNames(methodUnderTestName, valueClassId, classUnderTest) + return valueClassId.allMethods + .map { it.method } + .mapNotNull { method -> + if (isAccessible(method, packageName)) { + if (method.name !in modifyingMethods) return@mapNotNull null + if (method.genericParameterTypes.any { it is TypeVariable<*> }) return@mapNotNull null + + val parameterTypes = + method + .parameterTypes + .map { toFuzzerType(it, description.typeCache) } + + MethodDescription( + name = method.name, + parameterTypes = parameterTypes, + method = method + ) + } else null + } + .toList() +} + +internal fun Class<*>.findPublicSetterGetterIfHasPublicGetter(field: Field, packageName: String?): PublicSetterGetter? { + @Suppress("DEPRECATION") val postfixName = field.name.capitalize() + val setterName = "set$postfixName" + val getterName = "get$postfixName" + val getter = try { getDeclaredMethod(getterName) } catch (_: NoSuchMethodException) { return null } + return if (isAccessible(getter, packageName) && getter.returnType == field.type) { + declaredMethods.find { + isAccessible(it, packageName) && + it.name == setterName && + it.parameterCount == 1 && + it.parameterTypes[0] == field.type + }?.let { PublicSetterGetter(it, getter) } + } else { + null + } +} + +internal fun isAccessible(member: Member, packageName: String?): Boolean { + var clazz = member.declaringClass + while (clazz != null) { + if (!isAccessible(clazz, packageName)) return false + clazz = clazz.enclosingClass + } + return Modifier.isPublic(member.modifiers) || + (packageName != null && isNotPrivateOrProtected(member.modifiers) && member.declaringClass.`package`?.name == packageName) +} + +internal fun isAccessible(clazz: Class<*>, packageName: String?): Boolean { + return Modifier.isPublic(clazz.modifiers) || + (packageName != null && isNotPrivateOrProtected(clazz.modifiers) && clazz.`package`?.name == packageName) +} + +private fun findModifyingMethodNames( + methodUnderTestName: String, + valueClassId: ClassId, + classUnderTest: ClassId, + ) : Set = + UtBotFieldsModificatorsSearcher(fieldInvolvementMode = FieldInvolvementMode.ReadAndWrite) + .let { searcher -> + searcher.update(setOf(valueClassId, classUnderTest)) + val modificatorsToFields = searcher.getModificatorToFields(analysisMode = AnalysisMode.Methods) + + modificatorsToFields[methodUnderTestName]?.let { fieldsModifiedByMut -> + modificatorsToFields + .mapNotNull { (methodName, fieldSet) -> + val relevantFields = if (UtSettings.tryMutateOtherClassesFieldsWithMethods) { + fieldsModifiedByMut + } else { + fieldsModifiedByMut + .filter { field -> field.declaringClass == classUnderTest } + .toSet() + } + + val methodIsModifying = fieldSet.intersect(relevantFields).isNotEmpty() + && methodName != methodUnderTestName + if (methodIsModifying) methodName else null + } + .toSet() + } + ?: setOf() + } + +private fun isNotPrivateOrProtected(modifiers: Int): Boolean { + val hasAnyAccessModifier = Modifier.isPrivate(modifiers) || Modifier.isProtected(modifiers) + return !hasAnyAccessModifier +} diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Others.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Others.kt new file mode 100644 index 0000000000..c86b3b8064 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Others.kt @@ -0,0 +1,140 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.fuzzer.IdGenerator +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.hex +import java.text.SimpleDateFormat +import java.util.* + +abstract class ClassValueProvider( + val classId: ClassId +) : JavaValueProvider { + override fun accept(type: FuzzedType) = type.classId == classId +} + +object NumberValueProvider : ClassValueProvider(Number::class.id) { + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence> { + listOf( + byteClassId, shortClassId, intClassId, longClassId, floatClassId, doubleClassId + ).forEach { numberPrimitiveType -> + yield(Seed.Recursive( + construct = Routine.Create(listOf(FuzzedType(numberPrimitiveType))) { v -> v.first() }, + empty = Routine.Empty { UtNullModel(type.classId).fuzzed() } + )) + } + } +} + +class DateValueProvider( + private val idGenerator: IdGenerator +) : ClassValueProvider(Date::class.id) { + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence> { + // now date + val nowDateModel = UtAssembleModel( + id = idGenerator.createId(), + classId = type.classId, + modelName = "Date::now", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = type.classId.allConstructors.firstOrNull { it.parameters.isEmpty() } + ?: error("Cannot find default constructor of ${type.classId}"), + params = emptyList()) + ).fuzzed { } + yield(Seed.Simple(nowDateModel)) + + // from string dates + val strings = description.constants + .filter { + it.classId == stringClassId + } + .map { it.value } + .filterIsInstance() + .distinct() + val dateFormats = strings + .filter { it.isDateFormat() } + defaultDateFormat + val formatToDates = dateFormats.associateWith { format -> strings.filter { it.isDate(format) } } + formatToDates.forEach { (format, dates) -> + dates.forEach { date -> + yield(Seed.Simple( + assembleDateFromString(idGenerator.createId(), format, date) + )) + } + } + + // from numbers + type.classId.allConstructors + .filter { + it.parameters.isNotEmpty() && it.parameters.all { p -> p == intClassId || p == longClassId } + } + .forEach { constructor -> + yield(Seed.Recursive( + construct = Routine.Create(constructor.parameters.map { FuzzedType(it) }) { values -> + UtAssembleModel( + id = idGenerator.createId(), + classId = type.classId, + modelName = "Date(${values.map { it.model.classId }})", + instantiationCall = UtExecutableCallModel(null, constructor, values.map { it.model }) + ).fuzzed { } + }, + empty = Routine.Empty { nowDateModel } + )) + } + } + + companion object { + private const val defaultDateFormat = "dd-MM-yyyy HH:mm:ss:ms" + } + + private fun String.isDate(format: String): Boolean { + val formatter = SimpleDateFormat(format).apply { + isLenient = false + } + return runCatching { formatter.parse(trim()) }.isSuccess + } + + private fun String.isDateFormat(): Boolean { + return none { it.isDigit() } && // fixes concrete date values + runCatching { SimpleDateFormat(this) }.isSuccess + } + + private fun assembleDateFromString(id: Int, formatString: String, dateString: String): FuzzedValue { + val simpleDateFormatModel = assembleSimpleDateFormat(idGenerator.createId(), formatString) + val dateFormatParse = simpleDateFormatModel.classId.jClass + .getMethod("parse", String::class.java).executableId + val instantiationCall = UtExecutableCallModel( + simpleDateFormatModel, dateFormatParse, listOf(UtPrimitiveModel(dateString)) + ) + return UtAssembleModel( + id, + dateClassId, + "$dateFormatParse#" + id.hex(), + instantiationCall + ).fuzzed { + summary = "%var% = $dateFormatParse($stringClassId)" + } + } + + private fun assembleSimpleDateFormat(id: Int, formatString: String): UtAssembleModel { + val simpleDateFormatId = SimpleDateFormat::class.java.id + val formatStringConstructor = simpleDateFormatId.allConstructors.first { + it.parameters.singleOrNull() == stringClassId + } + val formatSetLenient = SimpleDateFormat::setLenient.executableId + val formatModel = UtPrimitiveModel(formatString) + + val instantiationCall = UtExecutableCallModel(instance = null, formatStringConstructor, listOf(formatModel)) + return UtAssembleModel( + id, + simpleDateFormatId, + "$simpleDateFormatId[$stringClassId]#" + id.hex(), + instantiationCall + ) { + listOf(UtExecutableCallModel(instance = this, formatSetLenient, listOf(UtPrimitiveModel(false)))) + } + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Primitives.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Primitives.kt new file mode 100644 index 0000000000..4cb1ce1879 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Primitives.kt @@ -0,0 +1,245 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.UtVoidModel +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.FuzzedContext +import org.utbot.fuzzer.FuzzedContext.Comparison.* +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.* +import org.utbot.fuzzing.seeds.* +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException +import kotlin.random.Random + +abstract class PrimitiveValueProvider( + vararg acceptableTypes: ClassId +) : JavaValueProvider { + protected val acceptableTypes = acceptableTypes.toSet() + + final override fun accept(type: FuzzedType) = type.classId in acceptableTypes + + protected suspend fun > SequenceScope>.yieldKnown( + value: T, + toValue: T.() -> Any + ) { + yield(Seed.Known(value) { known -> + UtPrimitiveModel(toValue(known)).fuzzed { + summary = buildString { + append("%var% = ${known.valueToString()}") + if (known.mutatedFrom != null) { + append(" (mutated from ${known.mutatedFrom?.valueToString()})") + } + } + } + }) + } + + private fun > T.valueToString(): String { + when (this) { + is BitVectorValue -> { + for (defaultBound in Signed.values()) { + if (defaultBound.test(this)) { + return defaultBound.name.lowercase() + } + } + return when (size) { + 8 -> toByte().toString() + 16 -> toShort().toString() + 32 -> toInt().toString() + 64 -> toLong().toString() + else -> toString(10) + } + } + is IEEE754Value -> { + for (defaultBound in DefaultFloatBound.values()) { + if (defaultBound.test(this)) { + return defaultBound.name.lowercase().replace("_", " ") + } + } + return when { + is32Float() -> toFloat().toString() + is64Float() -> toDouble().toString() + else -> toString() + } + } + is RegexValue -> { + return "'${value.substringToLength(10, "...")}' from $pattern" + } + is StringValue -> { + return "'${value.substringToLength(10, "...")}'" + } + else -> return toString() + } + } + + private fun String.substringToLength(size: Int, postfix: String): String { + return when { + length <= size -> this + else -> substring(0, size) + postfix + } + } +} + +object BooleanValueProvider : PrimitiveValueProvider(booleanClassId, booleanWrapperClassId) { + override fun generate(description: FuzzedDescription, type: FuzzedType) = sequence { + yieldKnown(Bool.TRUE()) { toBoolean() } + yieldKnown(Bool.FALSE()) { toBoolean() } + } +} + +object IntegerValueProvider : PrimitiveValueProvider( + charClassId, charWrapperClassId, + byteClassId, byteWrapperClassId, + shortClassId, shortWrapperClassId, + intClassId, intWrapperClassId, + longClassId, longWrapperClassId, +) { + + private val ClassId.typeSize: Int + get() = when (this) { + charClassId, charWrapperClassId -> 7 + byteClassId, byteWrapperClassId -> 8 + shortClassId, shortWrapperClassId -> 16 + intClassId, intWrapperClassId -> 32 + longClassId, longWrapperClassId -> 64 + else -> error("unknown type $this") + } + + /** + * Returns null when values cannot be cast. + */ + private fun ClassId.tryCast(value: BitVectorValue): Any? = when (this) { + charClassId, charWrapperClassId -> value.takeIf { typeSize <= charClassId.typeSize }?.toCharacter() + byteClassId, byteWrapperClassId -> value.takeIf { typeSize <= byteClassId.typeSize }?.toByte() + shortClassId, shortWrapperClassId -> value.takeIf { typeSize <= shortClassId.typeSize }?.toShort() + intClassId, intWrapperClassId -> value.takeIf { typeSize <= intClassId.typeSize }?.toInt() + longClassId, longWrapperClassId -> value.takeIf { typeSize <= longClassId.typeSize }?.toLong() + else -> error("unknown type $this") + } + + private fun ClassId.cast(value: BitVectorValue): Any = tryCast(value)!! + + private val randomStubWithNoUsage = Random(0) + private val configurationStubWithNoUsage = Configuration() + + private fun BitVectorValue.change(func: BitVectorValue.() -> Unit): BitVectorValue { + return Mutation> { _, _, _ -> + BitVectorValue(this).apply { func() } + }.mutate(this, randomStubWithNoUsage, configurationStubWithNoUsage) as BitVectorValue + } + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + description.constants.forEach { (t, v, c) -> + if (t in acceptableTypes) { + val value = BitVectorValue.fromValue(v) + val values = listOfNotNull( + value, + when (c) { + EQ, NE, LE, GT -> value.change { inc() } + LT, GE -> value.change { dec() } + else -> null + } + ) + values.forEach { + if (type.classId.tryCast(it) != null) { + yieldKnown(it) { + type.classId.cast(this) + } + } + } + + } + } + Signed.values().forEach { bound -> + val s = type.classId.typeSize + val value = bound(s) + if (type.classId.tryCast(value) != null) { + yieldKnown(value) { + type.classId.cast(this) + } + } + } + } +} + +object FloatValueProvider : PrimitiveValueProvider( + floatClassId, floatWrapperClassId, + doubleClassId, doubleWrapperClassId, +) { + private val ClassId.typeSize: Pair + get() = when (this) { + floatClassId, floatWrapperClassId -> 23 to 8 + doubleClassId, doubleWrapperClassId -> 52 to 11 + else -> error("unknown type $this") + } + + /** + * Returns null when values cannot be cast. + */ + private fun ClassId.cast(value: IEEE754Value): Any = when (this) { + floatClassId, floatWrapperClassId -> value.toFloat() + doubleClassId, doubleWrapperClassId -> value.toDouble() + else -> error("unknown type $this") + } + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + description.constants.forEach { (t, v, _) -> + if (t in acceptableTypes) { + yieldKnown(IEEE754Value.fromValue(v)) { type.classId.cast(this) } + } + } + DefaultFloatBound.values().forEach { bound -> + val (m, e) = type.classId.typeSize + yieldKnown(bound(m ,e)) { + type.classId.cast(this) + } + } + } +} + +object StringValueProvider : PrimitiveValueProvider(stringClassId, java.lang.CharSequence::class.java.id) { + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + val constants = description.constants + .filter { it.classId == stringClassId } + val values = constants + .mapNotNull { it.value as? String } + + sequenceOf("", "abc", "XZ", "#$\\\"'", "\n\t\r", "10", "-3") + values.forEach { yieldKnown(StringValue(it), StringValue::value) } + constants + .filter { it.fuzzedContext.isPatterMatchingContext() } + .map { it.value as String } + .distinct() + .filter(String::isSupportedPattern) + .forEach { + yieldKnown(RegexValue(it, Random(0)), StringValue::value) + } + } + + private fun FuzzedContext.isPatterMatchingContext(): Boolean { + if (this !is FuzzedContext.Call) return false + val stringMethodWithRegexArguments = setOf("matches", "split") + return when { + method.classId == Pattern::class.java.id -> true + method.classId == String::class.java.id && stringMethodWithRegexArguments.contains(method.name) -> true + else -> false + } + } +} + +object VoidValueProvider : PrimitiveValueProvider(voidClassId) { + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> = + sequenceOf(Seed.Simple(UtVoidModel.fuzzed { summary = "%var% = void" })) +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Utils.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Utils.kt new file mode 100644 index 0000000000..3e00a45c15 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Utils.kt @@ -0,0 +1,21 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.util.defaultValueModel +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.Routine + +fun nullRoutine(classId: ClassId): Routine.Empty = + Routine.Empty { nullFuzzedValue(classId) } + +fun nullFuzzedValue(classId: ClassId): FuzzedValue = + UtNullModel(classId).fuzzed { summary = "%var% = null" } + +fun defaultValueRoutine(classId: ClassId): Routine.Empty = + Routine.Empty { defaultFuzzedValue(classId) } + +fun defaultFuzzedValue(classId: ClassId): FuzzedValue = + classId.defaultValueModel().fuzzed { summary = "%var% = $model" } \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/FuzzedTypeWithProperties.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/FuzzedTypeWithProperties.kt new file mode 100644 index 0000000000..83b628d2d2 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/FuzzedTypeWithProperties.kt @@ -0,0 +1,33 @@ +package org.utbot.fuzzing.spring + +import org.utbot.common.DynamicProperties +import org.utbot.common.DynamicProperty +import org.utbot.common.dynamicPropertiesOf +import org.utbot.common.plus +import org.utbot.common.withoutProperty +import org.utbot.fuzzer.FuzzedType + +typealias FuzzedTypeProperties = DynamicProperties +typealias FuzzedTypeProperty = DynamicProperty +typealias FuzzedTypeFlag = FuzzedTypeProperty + +data class FuzzedTypeWithProperties( + val origin: FuzzedType, + val properties: FuzzedTypeProperties +) : FuzzedType(origin.classId, origin.generics) + +val FuzzedType.origin: FuzzedType + get() = + if (this is FuzzedTypeWithProperties) origin + else this + +val FuzzedType.properties: FuzzedTypeProperties + get() = + if (this is FuzzedTypeWithProperties) properties + else dynamicPropertiesOf() + +fun FuzzedType.withoutProperty(property: FuzzedTypeProperty<*>): FuzzedTypeWithProperties = + FuzzedTypeWithProperties(origin, properties.withoutProperty(property)) + +fun FuzzedType.addProperties(properties: FuzzedTypeProperties): FuzzedTypeWithProperties = + FuzzedTypeWithProperties(origin, this.properties + properties) diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/GeneratedField.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/GeneratedField.kt new file mode 100644 index 0000000000..0d2033d983 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/GeneratedField.kt @@ -0,0 +1,80 @@ +package org.utbot.fuzzing.spring + +import mu.KotlinLogging +import org.utbot.common.dynamicPropertiesOf +import org.utbot.common.withValue +import org.utbot.framework.fuzzer.IdGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.DirectFieldAccessId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtDirectGetFieldModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtReferenceModel +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.replaceWithWrapperIfPrimitive +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.providers.defaultFuzzedValue +import org.utbot.fuzzing.providers.defaultValueRoutine +import org.utbot.fuzzing.spring.valid.EntityLifecycleState +import org.utbot.fuzzing.spring.valid.EntityLifecycleStateProperty +import org.utbot.fuzzing.toFuzzerType + +class GeneratedFieldValueProvider( + private val idGenerator: IdGenerator, + private val entityClassId: ClassId, + private val fieldId: FieldId, +) : JavaValueProvider { + companion object { + private val logger = KotlinLogging.logger {} + } + + override fun accept(type: FuzzedType): Boolean = + replaceWithWrapperIfPrimitive(type.classId).jClass + .isAssignableFrom(replaceWithWrapperIfPrimitive(fieldId.type).jClass) + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> = sequenceOf( + Seed.Recursive( + construct = Routine.Create(listOf( + toFuzzerType(entityClassId.jClass, description.typeCache).addProperties( + dynamicPropertiesOf( + EntityLifecycleStateProperty.withValue(EntityLifecycleState.MANAGED) + ) + ) + )) { values -> + val thisInstanceValue = values.single() + val thisInstanceModel = when (val model = thisInstanceValue.model) { + is UtReferenceModel -> model + is UtNullModel -> return@Create defaultFuzzedValue(type.classId) + else -> { + logger.warn { "This instance model can be only UtReferenceModel or UtNullModel, but $model is met" } + return@Create defaultFuzzedValue(type.classId) + } + } + UtAssembleModel( + id = idGenerator.createId(), + classId = type.classId, + modelName = "${thisInstanceModel.modelName}.${fieldId.name}", + instantiationCall = UtDirectGetFieldModel( + instance = thisInstanceModel, + fieldAccess = DirectFieldAccessId( + classId = fieldId.declaringClass, + name = "", + fieldId = fieldId + ) + ) + ).fuzzed { + summary = "${thisInstanceValue.summary}.${fieldId.name}" + } + }, + modify = emptySequence(), + empty = defaultValueRoutine(type.classId) + ) + ) +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/JavaLangObject.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/JavaLangObject.kt new file mode 100644 index 0000000000..000fdf7356 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/JavaLangObject.kt @@ -0,0 +1,35 @@ +package org.utbot.fuzzing.spring + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.toTypeParametrizedByTypeVariables +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.providers.nullRoutine +import org.utbot.fuzzing.toFuzzerType + +class JavaLangObjectValueProvider( + private val classesToTryUsingAsJavaLangObject: List, +) : JavaValueProvider { + override fun accept(type: FuzzedType): Boolean { + return type.classId == objectClassId + } + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> = + classesToTryUsingAsJavaLangObject.map { classToUseAsObject -> + val fuzzedType = toFuzzerType( + type = classToUseAsObject.jClass.toTypeParametrizedByTypeVariables(), + cache = description.typeCache + ) + Seed.Recursive( + construct = Routine.Create(listOf(fuzzedType)) { (value) -> value }, + modify = emptySequence(), + empty = nullRoutine(type.classId) + ) + }.asSequence() +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/SpringBeanValueProvider.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/SpringBeanValueProvider.kt new file mode 100644 index 0000000000..840822c50c --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/SpringBeanValueProvider.kt @@ -0,0 +1,88 @@ +package org.utbot.fuzzing.spring + +import org.utbot.common.dynamicPropertiesOf +import org.utbot.common.withValue +import org.utbot.framework.fuzzer.IdGenerator +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.SpringModelUtils +import org.utbot.framework.plugin.api.util.SpringModelUtils.persistMethodIdOrNull +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.* +import org.utbot.fuzzing.providers.SPRING_BEAN_PROP +import org.utbot.fuzzing.providers.findMethodsToModifyWith +import org.utbot.fuzzing.providers.nullRoutine +import org.utbot.fuzzing.spring.valid.EntityLifecycleState +import org.utbot.fuzzing.spring.valid.EntityLifecycleStateProperty + +class SpringBeanValueProvider( + private val idGenerator: IdGenerator, + private val beanNameProvider: (ClassId) -> List, + private val relevantRepositories: Set +) : JavaValueProvider { + + override fun enrich(description: FuzzedDescription, type: FuzzedType, scope: Scope) { + if (description.description.isStatic == false + && scope.parameterIndex == 0 + && scope.recursionDepth == 1) { + scope.putProperty(SPRING_BEAN_PROP, beanNameProvider) + } + } + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + val beans = description.scope?.getProperty(SPRING_BEAN_PROP) + beans?.invoke(type.classId)?.forEach { beanName -> + yield(createValue(type.classId, beanName, description)) + } + } + + private fun createValue(classId: ClassId, beanName: String, description: FuzzedDescription): Seed.Recursive = + Seed.Recursive( + construct = Routine.Create(types = emptyList()) { + SpringModelUtils.createBeanModel( + beanName = beanName, + id = idGenerator.createId(), + classId = classId, + ).fuzzed { summary = "@Autowired ${classId.simpleName} $beanName" } + }, + modify = sequence { + // TODO mutate model itself (not just repositories) + relevantRepositories.forEach { repositoryId -> + yield(Routine.Call( + listOf(toFuzzerType(repositoryId.entityClassId.jClass, description.typeCache).addProperties( + dynamicPropertiesOf( + EntityLifecycleStateProperty.withValue(EntityLifecycleState.MANAGED) + ) + )) + ) { selfValue, (entityValue) -> + val self = selfValue.model as UtAssembleModel + val modificationChain: MutableList = + self.modificationsChain as MutableList + val entity = entityValue.model + if (entity is UtReferenceModel) { + persistMethodIdOrNull?.let { persistMethodId -> + ((entity as? UtAssembleModel)?.modificationsChain as? MutableList)?.removeAll { + it is UtExecutableCallModel && it.executable == persistMethodId + } + modificationChain.add( + UtExecutableCallModel( + instance = UtSpringEntityManagerModel(), + executable = persistMethodId, + params = listOf(entity) + ) + ) + } + + } + }) + } + }, + empty = nullRoutine(classId) + ) +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/ModifyingWithMethodsProviderWrapper.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/ModifyingWithMethodsProviderWrapper.kt new file mode 100644 index 0000000000..d94b2ea18a --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/ModifyingWithMethodsProviderWrapper.kt @@ -0,0 +1,56 @@ +package org.utbot.fuzzing.spring.decorators + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.providers.findMethodsToModifyWith + +/** + * Value provider that is a buddy for another provider + * that keeps all it's functionality and also allows + * to use methods to mutate field states of an object. + * + * NOTE!!! + * Instances represented by [UtAssembleModel] only can be mutated with methods. + */ +class ModifyingWithMethodsProviderWrapper( + private val classUnderTest: ClassId, + delegate: JavaValueProvider +) : ValueProviderDecorator(delegate) { + + override fun wrap(provider: JavaValueProvider): JavaValueProvider = + ModifyingWithMethodsProviderWrapper(classUnderTest, provider) + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> = + delegate + .generate(description, type) + .map { seed -> + if (seed is Seed.Recursive) { + Seed.Recursive( + construct = seed.construct, + modify = seed.modify + + findMethodsToModifyWith(description, type.classId, classUnderTest) + .asSequence() + .map { md -> + Routine.Call(md.parameterTypes) { self, values -> + val model = self.model as UtAssembleModel + model.modificationsChain as MutableList += + UtExecutableCallModel( + model, + md.method.executableId, + values.map { it.model } + ) + } + }, + empty = seed.empty, + ) + } else seed + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/PropertyPreservingValueProvider.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/PropertyPreservingValueProvider.kt new file mode 100644 index 0000000000..5f52435daa --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/PropertyPreservingValueProvider.kt @@ -0,0 +1,72 @@ +package org.utbot.fuzzing.spring.decorators + +import org.utbot.common.toDynamicProperties +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.spring.FuzzedTypeProperty +import org.utbot.fuzzing.spring.addProperties +import org.utbot.fuzzing.spring.properties + +/** + * @see preserveProperties + */ +interface PreservableFuzzedTypeProperty : FuzzedTypeProperty + +/** + * Returns wrapper of [this] provider that preserves [preservable properties][PreservableFuzzedTypeProperty], + * i.e. all [FuzzedType]s mentioned in any returned [Seed] will have all [preservable properties] + * [PreservableFuzzedTypeProperty] of the type that was used to generate that seed. + * + * That's useful if we want a whole part of the object tree created by fuzzer to possess some property + * (e.g. all entities used to create MANAGED entity should themselves be also MANAGED). + */ +fun JavaValueProvider.preserveProperties() : JavaValueProvider = + PropertyPreservingValueProvider(this) + +class PropertyPreservingValueProvider( + delegate: JavaValueProvider +) : ValueProviderDecorator(delegate) { + override fun wrap(provider: JavaValueProvider): JavaValueProvider = + provider.preserveProperties() + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> { + val delegateSeeds = delegate.generate(description, type) + + val preservedProperties = type.properties.entries + .filter { it.property is PreservableFuzzedTypeProperty } + .toDynamicProperties() + if (preservedProperties.entries.isEmpty()) return delegateSeeds + + fun List.addPreservedProperties() = map { it.addProperties(preservedProperties) } + + return delegateSeeds.map { seed -> + when (seed) { + is Seed.Recursive -> Seed.Recursive( + construct = Routine.Create( + types = seed.construct.types.addPreservedProperties(), + builder = seed.construct.builder + ), + modify = seed.modify.map { modification -> + Routine.Call( + types = modification.types.addPreservedProperties(), + callable = modification.callable + ) + }, + empty = seed.empty + ) + is Seed.Collection -> Seed.Collection( + construct = seed.construct, + modify = Routine.ForEach( + types = seed.modify.types.addPreservedProperties(), + callable = seed.modify.callable + ) + ) + is Seed.Known, is Seed.Simple -> seed + } + } + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/SeedFilteringValueProvider.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/SeedFilteringValueProvider.kt new file mode 100644 index 0000000000..62b9dbf8f0 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/SeedFilteringValueProvider.kt @@ -0,0 +1,19 @@ +package org.utbot.fuzzing.spring.decorators + +import org.utbot.fuzzing.Description +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +fun > ValueProvider.filterSeeds(predicate: (Seed) -> Boolean) = + SeedFilteringValueProvider(delegate = this, predicate) + +class SeedFilteringValueProvider>( + delegate: ValueProvider, + private val predicate: (Seed) -> Boolean +) : ValueProviderDecorator(delegate) { + override fun wrap(provider: ValueProvider): ValueProvider = + provider.filterSeeds(predicate) + + override fun generate(description: D, type: T): Sequence> = + delegate.generate(description, type).filter(predicate) +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/TypeFilteringValueProvider.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/TypeFilteringValueProvider.kt new file mode 100644 index 0000000000..7c46dfa46b --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/TypeFilteringValueProvider.kt @@ -0,0 +1,18 @@ +package org.utbot.fuzzing.spring.decorators + +import org.utbot.fuzzing.Description +import org.utbot.fuzzing.ValueProvider + +fun > ValueProvider.filterTypes(predicate: (T) -> Boolean) = + TypeFilteringValueProvider(delegate = this, predicate) + +class TypeFilteringValueProvider>( + delegate: ValueProvider, + private val predicate: (T) -> Boolean +) : ValueProviderDecorator(delegate) { + override fun wrap(provider: ValueProvider): ValueProvider = + provider.filterTypes(predicate) + + override fun accept(type: T): Boolean = + predicate(type) && super.accept(type) +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/TypeReplacingValueProvider.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/TypeReplacingValueProvider.kt new file mode 100644 index 0000000000..2df9984d00 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/TypeReplacingValueProvider.kt @@ -0,0 +1,28 @@ +package org.utbot.fuzzing.spring.decorators + +import org.utbot.fuzzing.Description +import org.utbot.fuzzing.Scope +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +fun > ValueProvider.replaceTypes(typeReplacer: (D, T) -> T) = + TypeReplacingValueProvider(delegate = this, typeReplacer) + +class TypeReplacingValueProvider>( + delegate: ValueProvider, + private val typeReplacer: (D, T) -> T +) : ValueProviderDecorator(delegate) { + override fun wrap(provider: ValueProvider): ValueProvider = + provider.replaceTypes(typeReplacer) + + override fun enrich(description: D, type: T, scope: Scope) = + super.enrich(description, typeReplacer(description, type), scope) + + override fun accept(type: T): Boolean = true + + override fun generate(description: D, type: T): Sequence> = + if (super.accept(typeReplacer(description, type))) + super.generate(description, typeReplacer(description, type)) + else + emptySequence() +} diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/ValueProviderDecorator.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/ValueProviderDecorator.kt new file mode 100644 index 0000000000..142c6c82d8 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/decorators/ValueProviderDecorator.kt @@ -0,0 +1,24 @@ +package org.utbot.fuzzing.spring.decorators + +import org.utbot.fuzzing.Description +import org.utbot.fuzzing.Scope +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +abstract class ValueProviderDecorator>( + protected val delegate: ValueProvider +) : ValueProvider { + protected abstract fun wrap(provider: ValueProvider): ValueProvider + + override fun enrich(description: D, type: T, scope: Scope) = + delegate.enrich(description, type, scope) + + override fun accept(type: T): Boolean = + delegate.accept(type) + + override fun generate(description: D, type: T): Sequence> = + delegate.generate(description, type) + + override fun map(transform: (ValueProvider) -> ValueProvider): ValueProvider = + transform(wrap(delegate.map(transform))) +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/InjectMocks.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/InjectMocks.kt new file mode 100644 index 0000000000..59c6f6d1b1 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/InjectMocks.kt @@ -0,0 +1,75 @@ +package org.utbot.fuzzing.spring.unit + +import org.utbot.framework.fuzzer.IdGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.util.allDeclaredFieldIds +import org.utbot.framework.plugin.api.util.isFinal +import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.framework.plugin.api.util.jField +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Scope +import org.utbot.fuzzing.ScopeProperty +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.toFuzzerType + +val INJECT_MOCK_FLAG = ScopeProperty( + "INJECT_MOCK_FLAG is present if composite model should be used (i.e. thisInstance is being created)" +) + +/** + * Models created by this class can be used with `@InjectMock` annotation, because + * they are [UtCompositeModel]s similar to the ones created by the symbolic engine. + * + * This class only creates models for thisInstance of type [classUnderTest]. + */ +class InjectMockValueProvider( + private val idGenerator: IdGenerator, + private val classUnderTest: ClassId, + private val isFieldNonNull: (FieldId) -> Boolean +) : JavaValueProvider { + override fun enrich(description: FuzzedDescription, type: FuzzedType, scope: Scope) { + if (description.description.isStatic == false && scope.parameterIndex == 0 && scope.recursionDepth == 1) { + scope.putProperty(INJECT_MOCK_FLAG, Unit) + } + } + + override fun accept(type: FuzzedType): Boolean = type.classId == classUnderTest + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> { + if (description.scope?.getProperty(INJECT_MOCK_FLAG) == null) return emptySequence() + val fields = type.classId.allDeclaredFieldIds.filterNot { it.isStatic && it.isFinal }.toList() + val (nonNullFields, nullableFields) = fields.partition(isFieldNonNull) + return sequenceOf(Seed.Recursive( + construct = Routine.Create( + types = nonNullFields.map { toFuzzerType(it.jField.genericType, description.typeCache) } + ) { values -> + emptyFuzzedValue(type.classId).also { + (it.model as UtCompositeModel).fields.putAll( + nonNullFields.zip(values).associate { (field, value) -> field to value.model } + ) + } + }, + modify = nullableFields.map { field -> + Routine.Call( + types = listOf(toFuzzerType(field.jField.genericType, description.typeCache)) + ) { instance, (value) -> + (instance.model as UtCompositeModel).fields[field] = value.model + } + }.asSequence(), + empty = Routine.Empty { emptyFuzzedValue(type.classId) } + )) + } + + private fun emptyFuzzedValue(classId: ClassId) = UtCompositeModel( + id = idGenerator.createId(), + classId = classId, + isMock = false, + ).fuzzed { summary = "%var% = ${classId.simpleName}()" } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/Mocks.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/Mocks.kt new file mode 100644 index 0000000000..203b24fd52 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/Mocks.kt @@ -0,0 +1,89 @@ +package org.utbot.fuzzing.spring.unit + +import com.google.common.reflect.TypeResolver +import mu.KotlinLogging +import org.utbot.framework.fuzzer.IdGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.method +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Scope +import org.utbot.fuzzing.ScopeProperty +import org.utbot.fuzzing.Seed +import org.utbot.fuzzer.jType +import org.utbot.fuzzer.toTypeParametrizedByTypeVariables +import org.utbot.fuzzer.typeToken +import org.utbot.fuzzing.toFuzzerType + +val methodsToMockProperty = ScopeProperty>( + description = "Method ids that can be mocked by `MockValueProvider`" +) + +// TODO shouldn't be used for primitives and other "easy" to create types +class MockValueProvider(private val idGenerator: IdGenerator) : JavaValueProvider { + companion object { + private val logger = KotlinLogging.logger {} + private val loggedMockedMethods = mutableSetOf() + private val loggedUnresolvedMethods = mutableSetOf() + } + + private val methodsToMock = mutableSetOf() + + override fun enrich(description: FuzzedDescription, type: FuzzedType, scope: Scope) { + val publicMethods = type.classId.jClass.methods.map { it.executableId } + publicMethods.intersect(methodsToMock).takeIf { it.isNotEmpty() }?.let { + scope.putProperty(methodsToMockProperty, it) + } + } + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> = + sequenceOf(Seed.Recursive( + construct = Routine.Create(types = emptyList()) { emptyMockFuzzedValue(type.classId) }, + empty = Routine.Empty { emptyMockFuzzedValue(type.classId) }, + modify = (description.scope?.getProperty(methodsToMockProperty)?.asSequence() ?: emptySequence()).map { methodId -> + val methodDeclaringClass = methodId.classId.jClass + + val returnType = try { + TypeResolver().where( + methodDeclaringClass.toTypeParametrizedByTypeVariables(), + @Suppress("UNCHECKED_CAST") + type.jType.typeToken.getSupertype(methodDeclaringClass as Class).type + ).resolveType(methodId.method.genericReturnType) + } catch (e: Exception) { + if (loggedUnresolvedMethods.add(methodId)) + logger.error(e) { "Failed to resolve return type for $methodId, using unresolved generic type" } + + methodId.method.genericReturnType + } + + // TODO accept `List` instead of singular `resolvedReturnType` + Routine.Call(types = listOf(toFuzzerType(returnType, description.typeCache))) { instance, (value) -> + if (loggedMockedMethods.add(methodId)) + logger.info { "Actually mocked $methodId for the first time" } + + (instance.model as UtCompositeModel).mocks[methodId] = listOf(value.model) + } + } + )) + + private fun emptyMockFuzzedValue(classId: ClassId) = UtCompositeModel( + id = idGenerator.createId(), + classId = classId, + isMock = true, + canHaveRedundantOrMissingMocks = true, + ).fuzzed { summary = "%var% = mock()" } + + fun addMockingCandidates(detectedMockingCandidates: Set) = + detectedMockingCandidates.forEach { methodId -> + if (methodsToMock.add(methodId)) + logger.info { "Detected that $methodId may need mocking" } + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/AbstractPrimitiveValidValueProvider.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/AbstractPrimitiveValidValueProvider.kt new file mode 100644 index 0000000000..7adb6442f5 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/AbstractPrimitiveValidValueProvider.kt @@ -0,0 +1,51 @@ +package org.utbot.fuzzing.spring.valid + +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.id +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.providers.nullFuzzedValue +import org.utbot.fuzzing.spring.FuzzedTypeProperty + +abstract class AbstractPrimitiveValidValueProvider, T, V> : + AbstractValidValueProvider() { + + protected abstract val primitiveClass: Class + + protected abstract fun defaultValidPrimitiveValue(validationData: T): V + protected abstract fun makeValid(originalValue: V, validationData: T): V + protected abstract fun isNullValid(): Boolean + + override fun acceptsType(type: FuzzedType): Boolean = + primitiveClass.id == type.classId + + final override fun makeValid(originalValue: FuzzedValue, validationData: T): FuzzedValue = + when (val model = originalValue.model) { + is UtNullModel -> if (isNullValid()) originalValue else defaultValidValue(validationData) + is UtPrimitiveModel -> { + if (primitiveClass.isInstance(model.value)) + primitiveFuzzedValue(makeValid(primitiveClass.cast(model.value), validationData)) + else + originalValue + } + else -> originalValue + } + + final override fun defaultValidValue(validationData: T): FuzzedValue = + primitiveFuzzedValue(defaultValidPrimitiveValue(validationData)) + + private fun primitiveFuzzedValue(value: V) = value?.let { + UtPrimitiveModel(value).fuzzed { + summary = "%var% = ${ + when (value) { + is String -> "\"$value\"" + is Char -> "\'$value\'" + else -> value.toString() + } + }" + } + } ?: nullFuzzedValue(primitiveClass.id) + +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/AbstractValidValueProvider.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/AbstractValidValueProvider.kt new file mode 100644 index 0000000000..ca17953fa4 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/AbstractValidValueProvider.kt @@ -0,0 +1,34 @@ +package org.utbot.fuzzing.spring.valid + +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.spring.FuzzedTypeProperty +import org.utbot.fuzzing.spring.properties +import org.utbot.fuzzing.spring.withoutProperty + +abstract class AbstractValidValueProvider, T> : JavaValueProvider { + protected abstract val validationDataTypeProperty: TProp + + protected abstract fun acceptsType(type: FuzzedType): Boolean + protected abstract fun makeValid(originalValue: FuzzedValue, validationData: T): FuzzedValue + protected abstract fun defaultValidValue(validationData: T): FuzzedValue + + final override fun accept(type: FuzzedType): Boolean = + validationDataTypeProperty in type.properties && acceptsType(type) + + final override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> { + val validationData = type.properties.getValue(validationDataTypeProperty) + return sequenceOf( + Seed.Recursive( + construct = Routine.Create(listOf(type.withoutProperty(validationDataTypeProperty))) { (origin) -> + makeValid(origin, validationData) + }, + empty = Routine.Empty { defaultValidValue(validationData) } + ) + ) + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/Email.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/Email.kt new file mode 100644 index 0000000000..fec1c1c111 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/Email.kt @@ -0,0 +1,23 @@ +package org.utbot.fuzzing.spring.valid + +import org.utbot.fuzzing.spring.FuzzedTypeFlag + +object EmailTypeFlag : FuzzedTypeFlag + +class EmailValueProvider : AbstractPrimitiveValidValueProvider() { + override val primitiveClass: Class + get() = String::class.java + + override val validationDataTypeProperty get() = EmailTypeFlag + + override fun defaultValidPrimitiveValue(validationData: Unit): String = "johnDoe@sample.com" + + override fun makeValid(originalValue: String, validationData: Unit): String = + when { + originalValue.length < 2 -> "johnDoe@sample.com" + else -> originalValue.take(originalValue.length / 2) + "@" + originalValue.drop(originalValue.length / 2) + } + + // `null` is a valid email according to `jakarta.validation.constraints.Email` javadoc + override fun isNullValid(): Boolean = true +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/NotBlank.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/NotBlank.kt new file mode 100644 index 0000000000..a8b7309db3 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/NotBlank.kt @@ -0,0 +1,19 @@ +package org.utbot.fuzzing.spring.valid + +import org.utbot.fuzzing.spring.FuzzedTypeFlag + +object NotBlankTypeFlag : FuzzedTypeFlag + +class NotBlankStringValueProvider : AbstractPrimitiveValidValueProvider() { + override val primitiveClass: Class + get() = String::class.java + + override val validationDataTypeProperty get() = NotBlankTypeFlag + + override fun defaultValidPrimitiveValue(validationData: Unit): String = "e" + + override fun makeValid(originalValue: String, validationData: Unit): String = + originalValue.ifBlank { "e" } + + override fun isNullValid(): Boolean = false +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/NotEmpty.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/NotEmpty.kt new file mode 100644 index 0000000000..a0532f6fca --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/NotEmpty.kt @@ -0,0 +1,19 @@ +package org.utbot.fuzzing.spring.valid + +import org.utbot.fuzzing.spring.FuzzedTypeFlag + +object NotEmptyTypeFlag : FuzzedTypeFlag + +class NotEmptyStringValueProvider : AbstractPrimitiveValidValueProvider() { + override val primitiveClass: Class + get() = String::class.java + + override val validationDataTypeProperty get() = NotEmptyTypeFlag + + override fun defaultValidPrimitiveValue(validationData: Unit): String = " " + + override fun makeValid(originalValue: String, validationData: Unit): String = + originalValue.ifEmpty { " " } + + override fun isNullValid(): Boolean = false +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/ValidEntity.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/ValidEntity.kt new file mode 100644 index 0000000000..4a3b349639 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/valid/ValidEntity.kt @@ -0,0 +1,164 @@ +package org.utbot.fuzzing.spring.valid + +import org.utbot.common.toDynamicProperties +import org.utbot.common.withValue +import org.utbot.framework.fuzzer.IdGenerator +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.UtAssembleModel +import org.utbot.framework.plugin.api.UtDirectSetFieldModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtSpringEntityManagerModel +import org.utbot.framework.plugin.api.util.SpringModelUtils.entityClassIds +import org.utbot.framework.plugin.api.util.SpringModelUtils.generatedValueClassIds +import org.utbot.framework.plugin.api.util.SpringModelUtils.detachMethodIdOrNull +import org.utbot.framework.plugin.api.util.SpringModelUtils.emailClassIds +import org.utbot.framework.plugin.api.util.SpringModelUtils.persistMethodIdOrNull +import org.utbot.framework.plugin.api.util.SpringModelUtils.idClassIds +import org.utbot.framework.plugin.api.util.SpringModelUtils.notBlankClassIds +import org.utbot.framework.plugin.api.util.SpringModelUtils.notEmptyClassIds +import org.utbot.framework.plugin.api.util.allDeclaredFieldIds +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.jField +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzzed +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.providers.findAccessibleModifiableFields +import org.utbot.fuzzing.providers.nullRoutine +import org.utbot.fuzzing.spring.decorators.PreservableFuzzedTypeProperty +import org.utbot.fuzzing.spring.addProperties +import org.utbot.fuzzing.spring.properties +import org.utbot.fuzzing.utils.hex + +enum class EntityLifecycleState( + val fieldAnnotationsToAvoidInitializing: List = emptyList(), + val entityManagerMethodsGetter: () -> List = { emptyList() }, +) { + NEW_WITHOUT_GENERATED_VALUES(generatedValueClassIds), + NEW_WITHOUT_ID(idClassIds), + NEW, + MANAGED(generatedValueClassIds + idClassIds, { listOfNotNull(persistMethodIdOrNull) }), + DETACHED(generatedValueClassIds + idClassIds, { listOfNotNull(persistMethodIdOrNull, detachMethodIdOrNull) }), +} + +object EntityLifecycleStateProperty : PreservableFuzzedTypeProperty + +class ValidEntityValueProvider( + val idGenerator: IdGenerator, + val onlyAcceptWhenValidIsRequired: Boolean +) : JavaValueProvider { + override fun accept(type: FuzzedType): Boolean { + return (!onlyAcceptWhenValidIsRequired || EntityLifecycleStateProperty in type.properties) && + entityClassIds.any { + @Suppress("UNCHECKED_CAST") + type.classId.jClass.getAnnotation(it.jClass as Class) != null + } + } + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> = + sequence { + val lifecycleStates = type.properties[EntityLifecycleStateProperty]?.let { listOf(it) } ?: + EntityLifecycleState.values().toList() + lifecycleStates.forEach { lifecycleState -> + generateForLifecycleState(description, type.classId, lifecycleState)?.let { yield(it) } + } + } + + private fun generateForLifecycleState( + description: FuzzedDescription, + classId: ClassId, + lifecycleState: EntityLifecycleState + ): Seed.Recursive? { + val noArgConstructorId = try { + classId.jClass.getDeclaredConstructor().executableId + } catch (e: NoSuchMethodException) { + return null + } + return Seed.Recursive( + construct = Routine.Create(types = emptyList()) { _ -> + val id = idGenerator.createId() + UtAssembleModel( + id = id, + classId = classId, + modelName = "${noArgConstructorId.classId.name}${noArgConstructorId.parameters}#" + id.hex(), + instantiationCall = UtExecutableCallModel(null, noArgConstructorId, params = emptyList()), + modificationsChainProvider = { + lifecycleState.entityManagerMethodsGetter().map { methodId -> + UtExecutableCallModel( + instance = UtSpringEntityManagerModel(), + executable = methodId, + params = listOf(this), + ) + } + } + ).fuzzed { + summary = "%var% = ${classId.simpleName}()" + } + }, + modify = sequence { + // TODO maybe all fields + findAccessibleModifiableFields( + description, + classId, + description.description.packageName + ).forEach { fd -> + val field = classId.allDeclaredFieldIds.first { it.name == fd.name }.jField + if (lifecycleState.fieldAnnotationsToAvoidInitializing.any { + @Suppress("UNCHECKED_CAST") + field.getAnnotation(it.jClass as Class) != null + }) return@forEach + + val validationProperties = field.annotatedType.annotations.mapNotNull { annotation -> + when (annotation.annotationClass.id) { + in notEmptyClassIds -> NotEmptyTypeFlag.withValue(Unit) + in notBlankClassIds -> NotBlankTypeFlag.withValue(Unit) + in emailClassIds -> EmailTypeFlag.withValue(Unit) + // TODO support more validators + else -> null + } + }.toDynamicProperties() + + val typeWithProperties = fd.type.addProperties(validationProperties) + + when { + fd.canBeSetDirectly -> { + yield(Routine.Call(listOf(typeWithProperties)) { self, values -> + val model = self.model as UtAssembleModel + (model.modificationsChain as MutableList).add( + index = 0, // prepending extra modifications to keep `persist()` modification last + UtDirectSetFieldModel( + model, + FieldId(classId, fd.name), + values.first().model + ) + ) + }) + } + + fd.setter != null -> { + yield(Routine.Call(listOf(typeWithProperties)) { self, values -> + val model = self.model as UtAssembleModel + (model.modificationsChain as MutableList).add( + index = 0, // prepending extra modifications to keep `persist()` modification last + UtExecutableCallModel( + model, + fd.setter.executableId, + values.map { it.model } + ) + ) + }) + } + } + } + }, + empty = nullRoutine(classId) + ) + } +} diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/AccessibleObjects.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/AccessibleObjects.java new file mode 100644 index 0000000000..5dd7bb78ec --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/AccessibleObjects.java @@ -0,0 +1,33 @@ +package org.utbot.fuzzing.samples; + +@SuppressWarnings("unused") +public class AccessibleObjects { + + public boolean test(Inn.Node n) { + return n.value * n.value == 36; + } + + private static class Inn { + static class Node { + public int value; + + public Node() { + + } + } + } + + public int ordinal(InnEn val) { + switch (val) { + case ONE: + return 0; + case TWO: + return 1; + } + return -1; + } + + private enum InnEn { + ONE, TWO + } +} diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/ConcreateMap.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/ConcreateMap.java new file mode 100644 index 0000000000..36a7891e10 --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/ConcreateMap.java @@ -0,0 +1,6 @@ +package org.utbot.fuzzing.samples; + +import java.util.HashMap; + +public class ConcreateMap extends HashMap { +} diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/ConcreteList.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/ConcreteList.java new file mode 100644 index 0000000000..14d1749cd8 --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/ConcreteList.java @@ -0,0 +1,7 @@ +package org.utbot.fuzzing.samples; + +import java.util.ArrayList; +import java.util.Collection; + +public class ConcreteList extends ArrayList> { +} diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/DeepNested.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/DeepNested.java new file mode 100644 index 0000000000..88c1748719 --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/DeepNested.java @@ -0,0 +1,14 @@ +package org.utbot.fuzzing.samples; + +public class DeepNested { + public class Nested1 { + public class Nested2 { + public int f(int i) { + if (i > 0) { + return 10; + } + return 0; + } + } + } +} diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/FailToGenerateListGeneric.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/FailToGenerateListGeneric.java new file mode 100644 index 0000000000..0e8a3d9aaf --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/FailToGenerateListGeneric.java @@ -0,0 +1,14 @@ +package org.utbot.fuzzing.samples; + +import java.util.List; + +@SuppressWarnings("unused") +public class FailToGenerateListGeneric { + + interface Something {} + + int func(List x) { + return x.size(); + } + +} diff --git a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/FieldSetterClass.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/FieldSetterClass.java similarity index 93% rename from utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/FieldSetterClass.java rename to utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/FieldSetterClass.java index 1e4d344c3f..d8d184c6b3 100644 --- a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/FieldSetterClass.java +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/FieldSetterClass.java @@ -1,4 +1,4 @@ -package org.utbot.framework.plugin.api.samples; +package org.utbot.fuzzing.samples; @SuppressWarnings("All") public class FieldSetterClass { diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/Implementations.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/Implementations.java new file mode 100644 index 0000000000..8ad0b55a08 --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/Implementations.java @@ -0,0 +1,43 @@ +package org.utbot.fuzzing.samples; + +public final class Implementations { + public interface A { + T getValue(); + } + + public static class AString implements A { + + private final String value; + + public AString(String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + } + + public static class AInteger implements A { + + private final Integer value; + + public AInteger(Integer value) { + this.value = value; + } + + @Override + public Integer getValue() { + return value; + } + } + + @SuppressWarnings("unused") + public static int test(A value) { + if (value.getValue() < 0) { + return 0; + } + return value.getValue(); + } +} diff --git a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/InnerClassWithEnums.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/InnerClassWithEnums.java similarity index 82% rename from utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/InnerClassWithEnums.java rename to utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/InnerClassWithEnums.java index 3f3dcd8be0..5e86197403 100644 --- a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/InnerClassWithEnums.java +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/InnerClassWithEnums.java @@ -1,6 +1,4 @@ -package org.utbot.framework.plugin.api.samples; - -import org.jetbrains.annotations.NotNull; +package org.utbot.fuzzing.samples; public class InnerClassWithEnums { private SampleEnum a; diff --git a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/OuterClassWithEnums.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/OuterClassWithEnums.java similarity index 95% rename from utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/OuterClassWithEnums.java rename to utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/OuterClassWithEnums.java index 61354b2c52..4118036977 100644 --- a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/OuterClassWithEnums.java +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/OuterClassWithEnums.java @@ -1,4 +1,4 @@ -package org.utbot.framework.plugin.api.samples; +package org.utbot.fuzzing.samples; public class OuterClassWithEnums { private SampleEnum value; diff --git a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/PackagePrivateFieldAndClass.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/PackagePrivateFieldAndClass.java similarity index 82% rename from utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/PackagePrivateFieldAndClass.java rename to utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/PackagePrivateFieldAndClass.java index f8975924b0..11986d8a16 100644 --- a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/PackagePrivateFieldAndClass.java +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/PackagePrivateFieldAndClass.java @@ -1,4 +1,4 @@ -package org.utbot.framework.plugin.api.samples; +package org.utbot.fuzzing.samples; @SuppressWarnings("All") public class PackagePrivateFieldAndClass { diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/SampleEnum.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/SampleEnum.java new file mode 100644 index 0000000000..401bd83217 --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/SampleEnum.java @@ -0,0 +1,6 @@ +package org.utbot.fuzzing.samples; + +public enum SampleEnum { + LEFT, + RIGHT +} diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringList.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringList.java new file mode 100644 index 0000000000..0cd91ecd37 --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringList.java @@ -0,0 +1,30 @@ +package org.utbot.fuzzing.samples; + +import java.util.*; + +public class StringList extends ArrayList { + public static StringList create() { + return new StringList(); + } + + public static List createAndUpcast() { + return new StringList(); + } + + public static List createListOfLists() { + return new ArrayList<>(); + } + + public static List> createListOfUpcastedLists() { + return new ArrayList<>(); + } + + public static List> createReadOnlyListOfReadOnlyLists() { + return new ArrayList<>(); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static List> createListOfParametrizedLists(Optional elm) { + return Collections.singletonList(Collections.singletonList(elm.orElse(null))); + } +} diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringListHolder.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringListHolder.java new file mode 100644 index 0000000000..88df01f163 --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/StringListHolder.java @@ -0,0 +1,20 @@ +package org.utbot.fuzzing.samples; + +import java.util.List; + +public class StringListHolder { + private List strings; + + + public List getStrings() { + return strings; + } + + + public void setStrings(List strings) { + this.strings = strings; + } + + + public void methodUnderTest() {} +} diff --git a/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/Stubs.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/Stubs.java new file mode 100644 index 0000000000..8423b68cbf --- /dev/null +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/Stubs.java @@ -0,0 +1,35 @@ +package org.utbot.fuzzing.samples; + +import java.util.List; + +@SuppressWarnings("unused") +public class Stubs { + + public static void name(String value) {} + + public static > int resolve(T recursive) { + int result = 0; + for (T t : recursive) { + result++; + } + return result; + } + + public static > int types(T[] t1, T[] t2, T[] t3) { + return t1.length + t2.length + t3.length; + } + + public static int arrayLength(java.util.List[][] test) { + int length = 0; + for (int i = 0; i < test.length; i++) { + for (int j = 0; j < test[i].length; j++) { + length += test[i][j].size(); + } + } + return length; + } + + public static , B extends List, C extends List>> A example(A c1, B c2, C c) { + return c2.iterator().next(); + } +} diff --git a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/WithInnerClass.java b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/WithInnerClass.java similarity index 88% rename from utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/WithInnerClass.java rename to utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/WithInnerClass.java index 49da2e8fda..3b012a73ad 100644 --- a/utbot-fuzzers/src/test/java/org/utbot/framework/plugin/api/samples/WithInnerClass.java +++ b/utbot-java-fuzzing/src/test/java/org/utbot/fuzzing/samples/WithInnerClass.java @@ -1,4 +1,4 @@ -package org.utbot.framework.plugin.api.samples; +package org.utbot.fuzzing.samples; public class WithInnerClass { public class NonStatic { diff --git a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzer/TypeUtilsTest.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzer/TypeUtilsTest.kt new file mode 100644 index 0000000000..d8e1df48b2 --- /dev/null +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzer/TypeUtilsTest.kt @@ -0,0 +1,200 @@ +package org.utbot.fuzzer + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.fuzzing.runBlockingWithContext +import org.utbot.fuzzing.samples.StringList +import java.io.File +import java.lang.reflect.Type +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.Optional + +class TypeUtilsTest { + @Test + fun `resolveParameterTypes works correctly with non-generic method`() = runBlockingWithContext { + val actualTypes = resolveParameterTypes(LocalDateTime::ofInstant.executableId, jTypeOf()) + val expectedTypes = listOf(jTypeOf(), jTypeOf()) + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with non-generic constructor`() = runBlockingWithContext { + val constructor = File::class.java.getConstructor(File::class.java, String::class.java) + val actualTypes = resolveParameterTypes(constructor.executableId, jTypeOf()) + val expectedTypes = listOf(jTypeOf(), jTypeOf()) + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with non-generic method and incompatible return type`() = runBlockingWithContext { + val actualTypes = resolveParameterTypes(LocalDateTime::ofInstant.executableId, jTypeOf()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with non-generic constructor and incompatible return type`() = runBlockingWithContext { + val constructor = File::class.java.getConstructor(File::class.java, String::class.java) + val actualTypes = resolveParameterTypes(constructor.executableId, jTypeOf()) + + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with simple generic method`() = runBlockingWithContext { + val method = Optional::class.java.getMethod("of", Object::class.java) + val actualTypes = resolveParameterTypes(method.executableId, jTypeOf>()) + val expectedTypes = listOf(jTypeOf()) + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with simple generic constructor`() = runBlockingWithContext { + val method = ArrayList::class.java.getConstructor(Collection::class.java) + val actualTypes = resolveParameterTypes(method.executableId, jTypeOf>()) + val expectedTypes = listOf(jTypeOf>()) + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with incompatible type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning subtype with incompatible type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::create.executableId, jTypeOf>()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with incompatible upper bound type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with incompatible lower bounded type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with compatible type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning subtype with compatible type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::create.executableId, jTypeOf>()) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works correctly with method returning type with compatible upper bound type parameter`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes(StringList::createAndUpcast.executableId, jTypeOf>()) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works with deep compatible bounded types`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createListOfLists.executableId, + jTypeOf>>() + ) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works with deep types incompatible at the top layer`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createListOfLists.executableId, + jTypeOf>>() + ) + + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works with deep compatible unbounded types`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createListOfUpcastedLists.executableId, + jTypeOf>>() + ) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works with deep types incompatible at the second layer`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createListOfUpcastedLists.executableId, + jTypeOf>>() + ) + + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes works with methods returning compatible nested bounded types`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createReadOnlyListOfReadOnlyLists.executableId, + jTypeOf>>() + ) + val expectedTypes = emptyList() + + assertEquals(expectedTypes, actualTypes) + } + + @Test + fun `resolveParameterTypes works with methods returning incompatible nested bounded types`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::createReadOnlyListOfReadOnlyLists.executableId, + jTypeOf>>() + ) + assertNull(actualTypes) + } + + @Test + fun `resolveParameterTypes resolves non-toplevel generics`() = + runBlockingWithContext { + val actualTypes = resolveParameterTypes( + StringList::class.java.getMethod("createListOfParametrizedLists", Optional::class.java).executableId, + jTypeOf>>>() + ) + val expectedTypes = listOf(jTypeOf>>()) + + assertEquals(expectedTypes, actualTypes) + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/IdGeneratorTest.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/IdGeneratorTest.kt similarity index 97% rename from utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/IdGeneratorTest.kt rename to utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/IdGeneratorTest.kt index fbad13f551..34e36ac950 100644 --- a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/IdGeneratorTest.kt +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/IdGeneratorTest.kt @@ -1,10 +1,10 @@ -package org.utbot.framework.plugin.api +package org.utbot.fuzzing import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -import org.utbot.fuzzer.ReferencePreservingIntIdGenerator +import org.utbot.framework.fuzzer.ReferencePreservingIntIdGenerator class IdGeneratorTest { private enum class Size { S, M, L, XL } diff --git a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt new file mode 100644 index 0000000000..987b43c323 --- /dev/null +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaFuzzingTest.kt @@ -0,0 +1,254 @@ +package org.utbot.fuzzing + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test +import org.utbot.framework.fuzzer.IdentityPreservingIdGenerator +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.constructor.ValueConstructor +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzing.providers.NullValueProvider +import org.utbot.fuzzing.samples.AccessibleObjects +import org.utbot.fuzzing.samples.DeepNested +import org.utbot.fuzzing.samples.FailToGenerateListGeneric +import org.utbot.fuzzing.samples.StringListHolder +import org.utbot.fuzzing.samples.Stubs +import org.utbot.fuzzing.utils.Trie +import java.util.concurrent.atomic.AtomicInteger +import kotlin.reflect.jvm.javaMethod + +internal object TestIdentityPreservingIdGenerator : IdentityPreservingIdGenerator { + private val cache = mutableMapOf() + private val gen = AtomicInteger() + override fun getOrCreateIdForValue(value: Any): Int = cache.computeIfAbsent(value) { createId() } + override fun createId(): Int = gen.incrementAndGet() +} + +class JavaFuzzingTest { + + @Test + fun `fuzzing doesn't throw an exception when type is unknown`() { + var count = 0 + runBlockingWithContext { + runJavaFuzzing( + TestIdentityPreservingIdGenerator, + methodUnderTest = MethodId( + DeepNested.Nested1.Nested2::class.id, + "f", + intClassId, + listOf(intClassId) + ), + constants = emptyList(), + names = emptyList(), + ) { _, _, _ -> + count += 1 + BaseFeedback(Trie.emptyNode(), Control.STOP) + } + } + assertEquals(0, count) + } + + @Test + fun `string generates same values`() { + fun collect(): List { + return runBlockingWithContext { + val results = mutableListOf() + var count = 0 + val probes = 10000 + runJavaFuzzing( + TestIdentityPreservingIdGenerator, + methodUnderTest = MethodId(Stubs::class.id, "name", voidClassId, listOf(stringClassId)), + constants = listOf( + FuzzedConcreteValue(stringClassId, "Hello"), + FuzzedConcreteValue(stringClassId, "World"), + FuzzedConcreteValue(stringClassId, "!"), + ), + names = emptyList() + ) { _, _, values -> + results += (values.first().model as UtPrimitiveModel).value as String + BaseFeedback(Trie.emptyNode(), if (++count < probes) Control.CONTINUE else Control.STOP) + } + assertEquals(count, results.size) + results + } + } + + val probe1 = collect() + val probe2 = collect() + assertEquals(probe1, probe2) + } + + @Test + fun `fuzzing should not generate values of private classes`() { + var exec = 0 + runBlockingWithContext { + runJavaFuzzing( + TestIdentityPreservingIdGenerator, + methodUnderTest = AccessibleObjects::class.java.declaredMethods.first { it.name == "test" }.executableId, + constants = emptyList(), + names = emptyList(), + ) { _, _, v -> + if (v.first().model !is UtNullModel) { + exec += 1 + } + BaseFeedback(Trie.emptyNode(), Control.STOP) + } + } + assertEquals(0, exec) { "Fuzzer should not create any values of private classes" } + } + + @Test + fun `fuzzing should not generate values of private enums`() { + var exec = 0 + runBlockingWithContext { + runJavaFuzzing( + TestIdentityPreservingIdGenerator, + methodUnderTest = AccessibleObjects::class.java.declaredMethods.first { it.name == "ordinal" }.executableId, + constants = emptyList(), + names = emptyList(), + ) { _, _, v -> + if (v.first().model !is UtNullModel) { + exec += 1 + } + BaseFeedback(Trie.emptyNode(), Control.STOP) + } + } + assertEquals(0, exec) { "Fuzzer should not create any values of private classes" } + } + + @Test + fun `fuzzing generate single test in case of collection with fail-to-generate generic type`() { + val size = 100 + var exec = size + val collections = ArrayList(exec) + runBlockingWithContext { + runJavaFuzzing( + TestIdentityPreservingIdGenerator, + methodUnderTest = FailToGenerateListGeneric::class.java.declaredMethods.first { it.name == "func" }.executableId, + constants = emptyList(), + names = emptyList(), + providers = defaultValueProviders(TestIdentityPreservingIdGenerator).map { it.except { it is NullValueProvider } } + ) { _, _, v -> + collections.add(v.first().model as? UtAssembleModel) + BaseFeedback(Trie.emptyNode(), if (--exec > 0) Control.CONTINUE else Control.STOP) + } + } + assertEquals(0, exec) { "Total fuzzer run number must be 0" } + assertEquals(size, collections.size) { "Total generated values number must be $size" } + assertEquals(size, collections.count { it is UtAssembleModel }) { "Total assemble models size must be $size" } + collections.filterIsInstance().forEach { + assertEquals(0, it.modificationsChain.size) + } + } + + @Test + fun `fuzzer correctly works with settable field that has a parameterized type`() { + val seenStringListHolders = mutableListOf() + var remainingRuns = 100 + runBlockingWithContext { + runJavaFuzzing( + TestIdentityPreservingIdGenerator, + methodUnderTest = StringListHolder::methodUnderTest.javaMethod!!.executableId, + constants = emptyList(), + names = emptyList(), + ) { thisInstance, _, _ -> + thisInstance?.let { + seenStringListHolders.add( + ValueConstructor().construct(listOf(it.model)).single().value as StringListHolder + ) + } + remainingRuns-- + BaseFeedback(Trie.emptyNode(), if (remainingRuns > 0) Control.CONTINUE else Control.STOP) + } + } + val seenStrings = seenStringListHolders.flatMap { it.strings.orEmpty().filterNotNull() } + assertNotEquals(emptyList(), seenStrings) + seenStrings.forEach { assertInstanceOf(String::class.java, it) } + } + + @Test + fun `value providers override every function of fuzzing in simple case`() { + val provided = MarkerValueProvider("p") + `value providers override every function of fuzzing`(provided, provided) + } + + @Test + fun `value providers override every function of fuzzing when merging`() { + val provider1 = MarkerValueProvider("p1") + val provider2 = MarkerValueProvider("p2") + val provided = provider1.with(provider2) + `value providers override every function of fuzzing`(provided, provider1) + `value providers override every function of fuzzing`(provided, provider2) + } + + @Test + fun `value providers override every function of fuzzing when excepting`() { + val provider1 = MarkerValueProvider("p1") + val provider2 = MarkerValueProvider("p2") + val provided = provider1.except(provider2) + `value providers override every function of fuzzing`(provided, provider1) + } + + @Test + fun `value providers override every function of fuzzing when fallback`() { + val provider1 = MarkerValueProvider("p1") + val provider2 = MarkerValueProvider("p2") + val provided = provider1.withFallback(provider2) + `value providers override every function of fuzzing`(provided, provider1) + `value providers override every function of fuzzing`(provided, provider2) + } + + private fun `value providers override every function of fuzzing`(provided: JavaValueProvider, valueProvider: MarkerValueProvider) { + var executions = 0 + runBlockingWithContext { + runJavaFuzzing( + TestIdentityPreservingIdGenerator, + methodUnderTest = FailToGenerateListGeneric::class.java.declaredMethods.first { it.name == "func" }.executableId, + constants = emptyList(), + names = emptyList(), + providers = listOfNotNull(provided) + ) { _, _, _ -> + executions++ + BaseFeedback(Trie.emptyNode(), Control.STOP) + } + } + + assertNotEquals(0, valueProvider.enrich) { "Enrich is never called for ${valueProvider.name}" } + assertNotEquals(0, valueProvider.accept) { "Accept is never called for ${valueProvider.name}" } + assertNotEquals(0, valueProvider.generate) { "Generate is never called for ${valueProvider.name}" } + assertEquals(0, executions) { "Execution must be never called, because of empty seed supply for ${valueProvider.name}" } + } +} + +class MarkerValueProvider>( + val name: String +) : ValueProvider { + var enrich: Int = 0 + var accept: Int = 0 + var generate: Int = 0 + + override fun enrich(description: D, type: T, scope: Scope) { + enrich++ + } + + override fun accept(type: T): Boolean { + accept++ + return true + } + + override fun generate(description: D, type: T): Sequence> { + generate++ + return emptySequence() + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaTypesTest.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaTypesTest.kt new file mode 100644 index 0000000000..5b6a5fbbfa --- /dev/null +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaTypesTest.kt @@ -0,0 +1,173 @@ +package org.utbot.fuzzing + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.iterableClassId +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzing.samples.Implementations +import org.utbot.fuzzing.samples.Stubs +import java.lang.Number +import java.lang.reflect.GenericArrayType +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.util.* +import java.util.List +import kotlin.collections.HashMap +import kotlin.collections.HashSet + +class JavaTypesTest { + + @Test + fun `recursive generic types are recognized correctly`() { + runBlockingWithContext { + val methods = Stubs::class.java.methods + val method = methods.first { it.name == "resolve" && it.returnType == Int::class.javaPrimitiveType } + val typeCache = IdentityHashMap() + val type = toFuzzerType(method.genericParameterTypes.first(), typeCache) + Assertions.assertEquals(1, typeCache.size) + Assertions.assertTrue(typeCache.values.all { type === it }) + Assertions.assertEquals(1, type.generics.size) + Assertions.assertTrue(typeCache.values.all { type.generics[0] === it }) + + try { + // If FuzzerType has implemented `equals` and `hashCode` or is data class, + // that implements those methods implicitly, + // then adding it to hash table throws [StackOverflowError] + val set = HashSet() + set += type + } catch (soe: StackOverflowError) { + Assertions.fail( + "Looks like FuzzerType implements equals and hashCode, " + + "which leads unstable behaviour in recursive generics ", soe + ) + } + } + } + + @Test + fun `can pass types through`() { + runBlockingWithContext { + val cache = HashMap() + val methods = Stubs::class.java.methods + val method = methods.first { it.name == "types" } + val types = method.genericParameterTypes.map { + toFuzzerType(it, cache) + } + Assertions.assertEquals( + 3, + cache.size + ) { "Cache should contain following types: List, Number and T[] for $method" } + Assertions.assertTrue(cache.keys.any { t -> + t is Class<*> && t == Number::class.java + }) + Assertions.assertTrue(cache.keys.any { t -> + t is ParameterizedType + && t.rawType == List::class.java + && t.actualTypeArguments.size == 1 + && t.actualTypeArguments.first() == Number::class.java + }) + Assertions.assertTrue(cache.keys.any { t -> + t is GenericArrayType + && t.typeName == "T[]" + }) + } + } + + @Test + fun `arrays with generics can be resolved`() { + runBlockingWithContext { + val cache = HashMap() + val methods = Stubs::class.java.methods + val method = methods.first { it.name == "arrayLength" } + method.genericParameterTypes.map { + toFuzzerType(it, cache) + } + Assertions.assertEquals( + 4, + cache.size + ) { "Cache should contain following types: List, Number and T[] for $method" } + Assertions.assertTrue(cache.keys.any { t -> + t is Class<*> && t == Number::class.java + }) + Assertions.assertTrue(cache.keys.any { t -> + t is ParameterizedType + && t.rawType == List::class.java + && t.actualTypeArguments.size == 1 + && t.actualTypeArguments.first().typeName == "T" + }) + Assertions.assertTrue(cache.keys.any { t -> + t is GenericArrayType + && t.typeName == "java.util.List[]" + }) + Assertions.assertTrue(cache.keys.any { t -> + t is GenericArrayType + && t.typeName == "java.util.List[][]" + }) + } + } + + @Test + fun `run complex type dependency call`() { + runBlockingWithContext { + val cache = HashMap() + val methods = Stubs::class.java.methods + val method = methods.first { it.name == "example" } + val types = method.genericParameterTypes + Assertions.assertTrue(types.size == 3 && types[0].typeName == "A" && types[1].typeName == "B" && types[2].typeName == "C") { "bad input parameters" } + method.genericParameterTypes.map { + toFuzzerType(it, cache) + } + Assertions.assertEquals(4, cache.size) + val typeIterableB = cache[types[0].replaceWithUpperBoundUntilNotTypeVariable()]!! + val genericOfIterableB = with(typeIterableB) { + Assertions.assertEquals(iterableClassId, classId) + Assertions.assertEquals(1, generics.size) + generics[0] + } + val typeListA = cache[types[1].replaceWithUpperBoundUntilNotTypeVariable()]!! + val genericOfListA = with(typeListA) { + Assertions.assertEquals(List::class.id, classId) + Assertions.assertEquals(1, generics.size) + generics[0] + } + Assertions.assertEquals(1, genericOfIterableB.generics.size) + Assertions.assertEquals(1, genericOfListA.generics.size) + Assertions.assertTrue(genericOfIterableB.generics[0] === typeIterableB) { "Because of recursive types generic of B must depend on B itself" } + Assertions.assertTrue(genericOfListA.generics[0] === typeListA) { "Because of recursive types generic of A must depend on A itself" } + + val typeListC = cache[types[2].replaceWithUpperBoundUntilNotTypeVariable()]!! + val genericOfListC = with(typeListC) { + Assertions.assertEquals(List::class.id, classId) + Assertions.assertEquals(1, generics.size) + generics[0] + } + + Assertions.assertEquals(1, genericOfListC.generics.size) + Assertions.assertEquals(iterableClassId, genericOfListC.generics[0].classId) + Assertions.assertTrue(genericOfListC.generics[0].generics[0] === typeListA) { "Generic of C must lead to type A" } + } + } + + @Test + fun `can correctly gather hierarchy information`() { + runBlockingWithContext { + val cache = HashMap() + val methods = Implementations::class.java.methods + val method = methods.first { it.name == "test" } + val type = method.genericParameterTypes.map { + toFuzzerType(it, cache) + }.first() + + val badType = toFuzzerType(Implementations.AString::class.java, cache) + val badTypeHierarchy = badType.traverseHierarchy(cache).toSet() + Assertions.assertEquals(2, badTypeHierarchy.size) { "There's only one class (Object) and one interface should be found" } + Assertions.assertFalse(badTypeHierarchy.contains(type)) { "Bad type hierarchy should not contain tested type $type" } + + val goodType = toFuzzerType(Implementations.AInteger::class.java, cache) + val goodTypeHierarchy = goodType.traverseHierarchy(cache).toSet() + Assertions.assertEquals(2, goodTypeHierarchy.size) { "There's only one class (Object) and one interface should be found" } + Assertions.assertTrue(goodTypeHierarchy.contains(type)) { "Good type hierarchy should contain tested type $type" } + } + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaValueProviderTest.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaValueProviderTest.kt new file mode 100644 index 0000000000..32fcf7b156 --- /dev/null +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/JavaValueProviderTest.kt @@ -0,0 +1,61 @@ +package org.utbot.fuzzing + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.Instruction +import org.utbot.framework.plugin.api.util.collectionClassId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzing.providers.ListSetValueProvider +import org.utbot.fuzzing.providers.MapValueProvider +import org.utbot.fuzzing.samples.ConcreateMap +import org.utbot.fuzzing.samples.ConcreteList +import org.utbot.fuzzing.utils.Trie +import java.lang.reflect.Type +import kotlin.random.Random + +fun emptyFuzzerDescription(typeCache: MutableMap) = FuzzedDescription( + FuzzedMethodDescription("no name", voidClassId, emptyList()), + Trie(Instruction::id), + typeCache, + Random(42) +) + +class JavaValueProviderTest { + + @Test + fun `collection value provider correctly resolves types for concrete types of map`() { + val typeCache = mutableMapOf() + runBlockingWithContext { + val seed = MapValueProvider(TestIdentityPreservingIdGenerator).generate( + emptyFuzzerDescription(typeCache), + toFuzzerType(ConcreateMap::class.java, typeCache) + ).first() + val collection = seed as Seed.Collection + val types = collection.modify.types + Assertions.assertEquals(2, types.size) + Assertions.assertEquals(types[0].classId, stringClassId) + Assertions.assertEquals(types[1].classId, java.lang.Number::class.java.id) + } + } + + @Test + fun `collection value provider correctly resolves types for concrete types of list`() { + val typeCache = mutableMapOf() + runBlockingWithContext { + val seed = ListSetValueProvider(TestIdentityPreservingIdGenerator).generate( + emptyFuzzerDescription(typeCache), + toFuzzerType(ConcreteList::class.java, typeCache) + ).first() + val collection = seed as Seed.Collection + val types = collection.modify.types + Assertions.assertEquals(1, types.size) + Assertions.assertEquals(types[0].classId, collectionClassId) + Assertions.assertEquals(1, types[0].generics.size) + Assertions.assertEquals(types[0].generics[0].classId, stringClassId) + } + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/Utils.kt b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/Utils.kt new file mode 100644 index 0000000000..d9a7b10c7e --- /dev/null +++ b/utbot-java-fuzzing/src/test/kotlin/org/utbot/fuzzing/Utils.kt @@ -0,0 +1,16 @@ +package org.utbot.fuzzing + +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext + +internal fun V.runBlockingWithContext(block: suspend () -> T) : T { + return withUtContext(UtContext(this!!::class.java.classLoader)) { + runBlocking { + withTimeout(10000) { + block() + } + } + } +} \ No newline at end of file diff --git a/utbot-js/.gitignore b/utbot-js/.gitignore new file mode 100644 index 0000000000..c7708ff1e2 --- /dev/null +++ b/utbot-js/.gitignore @@ -0,0 +1 @@ +samples \ No newline at end of file diff --git a/utbot-js/build.gradle.kts b/utbot-js/build.gradle.kts new file mode 100644 index 0000000000..9fdffc1fb7 --- /dev/null +++ b/utbot-js/build.gradle.kts @@ -0,0 +1,46 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_17 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + api(project(":utbot-framework")) + // https://mvnrepository.com/artifact/com.google.javascript/closure-compiler + implementation("com.google.javascript:closure-compiler:v20221102") + + // https://mvnrepository.com/artifact/org.json/json + implementation(group = "org.json", name = "json", version = "20220320") + + // https://mvnrepository.com/artifact/commons-io/commons-io + implementation(group = "commons-io", name = "commons-io", version = "2.11.0") + + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation("org.functionaljava:functionaljava:5.0") + implementation("org.functionaljava:functionaljava-quickcheck:5.0") + implementation("org.functionaljava:functionaljava-java-core:5.0") + implementation(group = "org.apache.commons", name = "commons-text", version = apacheCommonsTextVersion) +} diff --git a/utbot-js/docs/CLI.md b/utbot-js/docs/CLI.md new file mode 100644 index 0000000000..ee2c5ff0ae --- /dev/null +++ b/utbot-js/docs/CLI.md @@ -0,0 +1,70 @@ +## Build + +.jar file can be built in GitHub Actions with script publish-plugin-and-cli-from-branch. + +## Requirements + +* NodeJs 10.0.0 or higher (available to download https://nodejs.org/en/download/) +* Java 11 or higher (available to download https://www.oracle.com/java/technologies/downloads/) + +## Basic usage + +Generate tests: + + java -jar utbot-cli.jar generate_js --source="dir/file_with_sources.js" --output="dir/generated_tests.js" + +This will generate tests for top-level functions from `file_with_sources.js`. + +Run generated tests: + + java -jar utbot-cli.jar run_js --fileOrDir="generated_tests.js" + +This will run generated tests from file or directory. + +Generate coverage report: + + java -jar utbot-cli.jar coverage_js --source=dir/generated_tests.js + +This will generate coverage report from generated tests and print in `StdOut` + +## `generate_js` options + +- `-s, --source ` + + (required) Source code file for a test generation. +- `-c, --class ` + + If not specified tests for top-level functions are generated, otherwise for the specified class. + +- `-o, --output ` + + File for generated tests. +- `-p, --print-test` + + Specifies whether test should be printed out to `StdOut` (default = false) +- `-t, --timeout ` + + Timeout for Node.js to run scripts in seconds (default = 5) + +## `run_js` options + +- `-f, --fileOrDir` + + (required) File or directory with tests. +- `-o, --output` + + Specifies output of .txt file for test framework result (If empty prints to `StdOut`) + +- `-t, --test-framework [mocha]` + + Test framework of tests to run. + +## `coverage_js` options + +- `-s, --source ` + + (required) File with tests to generate a report. + +- `-o, --output` + + Specifies output .json file for generated tests (If empty prints .json to `StdOut`) diff --git a/utbot-js/samples/arrays.js b/utbot-js/samples/arrays.js new file mode 100644 index 0000000000..28e39a4ca6 --- /dev/null +++ b/utbot-js/samples/arrays.js @@ -0,0 +1,38 @@ +///region Simple array test +function simpleArray(arr) { + if (arr[0] === 5) { + return 5 + } + return 1 +} +simpleArray([0, 2]) +///endregion +///region Array of objects test +class ObjectParameter { + + constructor(a) { + this.first = a + } + + performAction(value) { + return 2 * value + } +} + +function arrayOfObjects(arr) { + if (arr[0].first === 2) { + return 1 + } + return 10 +} + +let arr = [] +arr[0] = new ObjectParameter(10) +arrayOfObjects(arr) +///endregion +///region Function returns array test +function returnsArray(num) { + return [num] +} +returnsArray(5) +///endregion \ No newline at end of file diff --git a/utbot-js/samples/bitOperators.js b/utbot-js/samples/bitOperators.js new file mode 100644 index 0000000000..3f69723358 --- /dev/null +++ b/utbot-js/samples/bitOperators.js @@ -0,0 +1,31 @@ +class BitOperators { + + complement(x) { + return (~x) === 1 + } + + xor(x, y) { + return (x ^ y) === 0 + } + + and(x) { + return (x & (x - 1)) === 0 + } + + Not(a, b) { + let d = a && b + let e = !a || b + return d && e ? 100 : 200 + } + + shl(x) { + return (x << 1) === 2 + } + + shlWithBigLongShift(shift) { + if (shift < 40) { + return 1 + } + return (0x77777777 << shift) === 0x77777770 ? 2 : 3 + } +} diff --git a/utbot-js/samples/commonIfStatement.js b/utbot-js/samples/commonIfStatement.js new file mode 100644 index 0000000000..94b6880a7e --- /dev/null +++ b/utbot-js/samples/commonIfStatement.js @@ -0,0 +1,7 @@ +function foo(a,b) { + if (a > 10) { + return a * b + } else { + return -1 + } +} diff --git a/utbot-js/samples/commonLoops.js b/utbot-js/samples/commonLoops.js new file mode 100644 index 0000000000..9aae30694c --- /dev/null +++ b/utbot-js/samples/commonLoops.js @@ -0,0 +1,27 @@ +class Loops { + + whileLoop(value) { + let i = 0 + let sum = 0 + while (i < value) { + sum += i + i += 1 + } + return sum + } + + loopInsideLoop(x) { + for (let i = x - 5; i < x; i++) { + if (i < 0) { + return 2 + } else { + for (let j = i; j < x + i; j++) { + if (j === 7) { + return 1 + } + } + } + } + return -1 + } +} diff --git a/utbot-js/samples/commonRecursion.js b/utbot-js/samples/commonRecursion.js new file mode 100644 index 0000000000..e2bd694537 --- /dev/null +++ b/utbot-js/samples/commonRecursion.js @@ -0,0 +1,19 @@ +class Recursion { + factorial(n) { + if (n < 0) + return -1 + if (n === 0) + return 1 + return n * this.factorial(n - 1) + } + + fib(n) { + if (n < 0 || n > 25) + return -1 + if (n === 0) + return 0 + if (n === 1) + return 1 + return this.fib(n - 1) + this.fib(n - 2) + } +} diff --git a/utbot-js/samples/commonString.js b/utbot-js/samples/commonString.js new file mode 100644 index 0000000000..24a5cdc2a6 --- /dev/null +++ b/utbot-js/samples/commonString.js @@ -0,0 +1,19 @@ +class StringExamples { + + isNotBlank(cs) { + return cs.length !== 0 + } + + nullableStringBuffer(buffer, i) { + if (i >= 0) { + buffer += "Positive" + } else { + buffer += "Negative" + } + return buffer.toString() + } + + length(cs) { + return cs == null ? 0 : cs.length + } +} diff --git a/utbot-js/samples/functionsThrowExceptionsInRow.js b/utbot-js/samples/functionsThrowExceptionsInRow.js new file mode 100644 index 0000000000..0a90662d13 --- /dev/null +++ b/utbot-js/samples/functionsThrowExceptionsInRow.js @@ -0,0 +1,18 @@ +function customError(a) { + if (a > 5) { + throw Error("MyCustomError") + } else { + return 10 + } +} + +function goodBoy(a) { + switch (a) { + case 5: + return 5 + case 10: + return 10 + default: + return 0 + } +} diff --git a/utbot-js/samples/mapStructure.js b/utbot-js/samples/mapStructure.js new file mode 100644 index 0000000000..2bc07494db --- /dev/null +++ b/utbot-js/samples/mapStructure.js @@ -0,0 +1,21 @@ +// Maps in JavaScript are untyped, so only maps with basic key/value types are feasible to support +///region Simple Map test +function simpleMap(map, compareValue) { + if (map.get("a") === compareValue) { + return 5 + } + return 1 +} + +const map1 = new Map() +map1.set("b", 3.0) +simpleMap(map1, 5) +///endregion +///region Function returns Map test +function returnsMap(name, value) { + let temp = new Map() + return temp.set(name, value) +} + +returnsMap("a", 5) +///endregion diff --git a/utbot-js/samples/scenarioMultyClassNoTopLevel.js b/utbot-js/samples/scenarioMultyClassNoTopLevel.js new file mode 100644 index 0000000000..7f6973db3c --- /dev/null +++ b/utbot-js/samples/scenarioMultyClassNoTopLevel.js @@ -0,0 +1,39 @@ +class Na { + constructor(num) { + this.num = num + } + + double() { + return this.num * 2 + } + + static test(a, b) { + return a + 2 * b + } +} + +class Kek { + foo(a, b) { + return a + b + } + + fString(a, b) { + return a + b + } + + fDel(a, b) { + return a / b + } + + fObj(a, b) { + return a.num + b.num + } + + getDone(a) { + a.done() + } + + done() { + return this.toString() + } +} diff --git a/utbot-js/samples/scenarioObjectParameter.js b/utbot-js/samples/scenarioObjectParameter.js new file mode 100644 index 0000000000..4096277c40 --- /dev/null +++ b/utbot-js/samples/scenarioObjectParameter.js @@ -0,0 +1,16 @@ +class ObjectParameter { + + constructor(a) { + this.first = a + } + + performAction(value) { + return 2 * value + } +} + +function functionToTest(obj, v) { + return obj.performAction(v) +} + +functionToTest(new ObjectParameter(5), 5) diff --git a/utbot-js/samples/scenarioStaticMethod.js b/utbot-js/samples/scenarioStaticMethod.js new file mode 100644 index 0000000000..783937e859 --- /dev/null +++ b/utbot-js/samples/scenarioStaticMethod.js @@ -0,0 +1,13 @@ +class Object { + + constructor(a) { + this.first = a + } + + static functionToTest(value) { + if (value > 1024 && value < 1026) { + return 2 * value + } + return value + } +} diff --git a/utbot-js/samples/scenarioThrowError.js b/utbot-js/samples/scenarioThrowError.js new file mode 100644 index 0000000000..2068695dd1 --- /dev/null +++ b/utbot-js/samples/scenarioThrowError.js @@ -0,0 +1,11 @@ +function functionToTest(a) { + if (a === true) { + throw Error("err") + } else if (a === 1) { + while (true) { + } + } else { + return -1 + } + +} diff --git a/utbot-js/samples/setStructure.js b/utbot-js/samples/setStructure.js new file mode 100644 index 0000000000..547a2e8058 --- /dev/null +++ b/utbot-js/samples/setStructure.js @@ -0,0 +1,22 @@ +// Sets in JavaScript are untyped, so only sets with basic value types are feasible to support +///region Simple Set test +function setTest(set, checkValue) { + if (set.has(checkValue)) { + return set + } + return set +} + +let s = new Set() +s.add(5) +s.add(6) +setTest(s, 4) +///endregion +///region Function returns Set test +function returnsSet(num) { + let temp = new Set() + return temp.add(num) +} + +returnsSet(5) +///endregion \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/api/JsTestGenerator.kt b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt new file mode 100644 index 0000000000..4b7be92daf --- /dev/null +++ b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt @@ -0,0 +1,422 @@ +package api + +import codegen.JsCodeGenerator +import com.google.javascript.rhino.Node +import framework.api.js.JsClassId +import framework.api.js.JsMethodId +import framework.api.js.JsUtFuzzedExecution +import framework.api.js.util.isClass +import framework.api.js.util.isJsArray +import framework.api.js.util.isJsBasic +import framework.api.js.util.jsErrorClassId +import framework.api.js.util.jsUndefinedClassId +import framework.codegen.JsImport +import framework.codegen.ModuleType +import fuzzer.JsFeedback +import fuzzer.JsFuzzingExecutionFeedback +import fuzzer.JsMethodDescription +import fuzzer.JsStatement +import fuzzer.JsTimeoutExecution +import fuzzer.JsValidExecution +import fuzzer.runFuzzing +import java.io.File +import java.util.concurrent.CancellationException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import mu.KotlinLogging +import org.utbot.common.runBlockingWithCancellationPredicate +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.UtExecutionResult +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtExplicitlyThrownException +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtTimeoutException +import org.utbot.fuzzing.Control +import org.utbot.fuzzing.utils.Trie +import parser.JsAstScrapper +import parser.JsFuzzerAstVisitor +import parser.JsParserUtils +import parser.JsParserUtils.getAbstractFunctionName +import parser.JsParserUtils.getAbstractFunctionParams +import parser.JsParserUtils.getClassMethods +import parser.JsParserUtils.getClassName +import parser.JsParserUtils.getParamName +import parser.JsParserUtils.runParser +import parser.JsToplevelFunctionAstVisitor +import providers.exports.IExportsProvider +import service.InstrumentationService +import service.PackageJson +import service.PackageJsonService +import service.ServiceContext +import service.TernService +import service.coverage.CoverageMode +import service.coverage.CoverageServiceProvider +import settings.JsDynamicSettings +import settings.JsExportsSettings.endComment +import settings.JsExportsSettings.startComment +import settings.JsTestGenerationSettings.fileUnderTestAliases +import settings.JsTestGenerationSettings.fuzzingThreshold +import settings.JsTestGenerationSettings.fuzzingTimeout +import utils.PathResolver +import utils.constructClass +import utils.data.ResultData +import utils.toJsAny + +private val logger = KotlinLogging.logger {} + +class JsTestGenerator( + private val fileText: String, + private var sourceFilePath: String, + private var projectPath: String = sourceFilePath.substringBeforeLast("/"), + private val selectedMethods: List? = null, + private var parentClassName: String? = null, + private var outputFilePath: String?, + private val exportsManager: ((String?, String) -> String) -> Unit, + private val settings: JsDynamicSettings, + private val isCancelled: () -> Boolean = { false } +) { + + private val exports = mutableSetOf() + + private lateinit var parsedFile: Node + + private lateinit var astScrapper: JsAstScrapper + + private val utbotDir = "utbotJs" + + init { + fixPathDelims() + } + + private fun fixPathDelims() { + projectPath = projectPath.replace("\\", "/") + outputFilePath = outputFilePath?.replace("\\", "/") + sourceFilePath = sourceFilePath.replace("\\", "/") + } + + /** + * Returns String representation of generated tests. + */ + fun run(): String { + parsedFile = runParser(fileText) + astScrapper = JsAstScrapper(parsedFile, sourceFilePath) + val context = ServiceContext( + utbotDir = utbotDir, + projectPath = projectPath, + filePathToInference = astScrapper.filesToInfer, + parsedFile = parsedFile, + settings = settings, + ) + context.packageJson = PackageJsonService( + sourceFilePath, + File(projectPath), + ).findClosestConfig() + val paramNames = mutableMapOf>() + val testSets = mutableListOf() + val classNode = + JsParserUtils.searchForClassDecl( + className = parentClassName, + parsedFile = parsedFile, + strict = selectedMethods?.isNotEmpty() ?: false + ) + parentClassName = classNode?.getClassName() + val classId = makeJsClassId(classNode, TernService(context)) + val methods = makeMethodsToTest() + if (methods.isEmpty()) throw IllegalArgumentException("No methods to test were found!") + methods.forEach { funcNode -> + makeTestsForMethod(classId, funcNode, classNode, context, testSets, paramNames) + } + val importPrefix = makeImportPrefix() + val moduleType = ModuleType.fromPackageJson(context.packageJson) + val imports = listOf( + JsImport( + "*", + fileUnderTestAliases, + "./$importPrefix/${sourceFilePath.substringAfterLast("/")}", + moduleType + ), + JsImport( + "*", + "assert", + "assert", + moduleType + ) + ) + val codeGen = JsCodeGenerator( + classUnderTest = classId, + paramNames = paramNames, + imports = imports + ) + return codeGen.generateAsStringWithTestReport(testSets).generatedCode + } + + private fun makeTestsForMethod( + classId: JsClassId, + funcNode: Node, + classNode: Node?, + context: ServiceContext, + testSets: MutableList, + paramNames: MutableMap> + ) { + val execId = classId.allMethods.find { + it.name == funcNode.getAbstractFunctionName() + } ?: throw IllegalStateException() + manageExports(classNode, funcNode, execId, context.packageJson) + val executionResults = mutableListOf() + try { + runBlockingWithCancellationPredicate(isCancelled) { + runFuzzingFlow(funcNode, execId, context).collect { + executionResults += it + } + } + } catch (e: CancellationException) { + logger.info { "Fuzzing was stopped due to test generation cancellation" } + } + if (executionResults.isEmpty()) { + if (isCancelled()) return + throw UnsupportedOperationException("No test cases were generated for ${funcNode.getAbstractFunctionName()}") + } + logger.info { "${executionResults.size} test cases were suggested after fuzzing" } + val testsForGenerator = mutableListOf() + val errorsForGenerator = mutableMapOf() + executionResults.forEach { value -> + when (value) { + is JsTimeoutExecution -> errorsForGenerator[value.utTimeout.exception.message!!] = 1 + is JsValidExecution -> testsForGenerator.add(value.utFuzzedExecution) + } + } + + val testSet = CgMethodTestSet( + executableId = execId, + errors = errorsForGenerator, + executions = testsForGenerator, + ) + testSets += testSet + paramNames[execId] = funcNode.getAbstractFunctionParams().map { it.getParamName() } + } + + private fun makeImportPrefix(): String { + return outputFilePath?.let { + PathResolver.getRelativePath( + File(it).parent, + File(sourceFilePath).parent, + ) + } ?: "" + } + + private fun getUtModelResult( + execId: JsMethodId, + resultData: ResultData, + fuzzedValues: List + ): UtExecutionResult { + if (resultData.isError && resultData.rawString == "Timeout") return UtTimeoutException( + TimeoutException("Timeout in generating test for ${ + execId.parameters + .zip(fuzzedValues) + .joinToString( + prefix = "${execId.name}(", + separator = ", ", + postfix = ")" + ) { (_, value) -> value.toString() } + }") + ) + val (returnValue, valueClassId) = resultData.toJsAny( + if (execId.returnType.isJsBasic) JsClassId(resultData.type) else execId.returnType + ) + val result = JsUtModelConstructor().construct(returnValue, valueClassId) + val utExecResult = when (result.classId) { + jsErrorClassId -> UtExplicitlyThrownException(Throwable(returnValue.toString()), false) + else -> UtExecutionSuccess(result) + } + return utExecResult + } + + private fun runFuzzingFlow( + funcNode: Node, + execId: JsMethodId, + context: ServiceContext, + ): Flow = flow { + val fuzzerVisitor = JsFuzzerAstVisitor() + fuzzerVisitor.accept(funcNode) + val jsDescription = JsMethodDescription( + name = funcNode.getAbstractFunctionName(), + parameters = execId.parameters, + classId = execId.classId, + concreteValues = fuzzerVisitor.fuzzedConcreteValues, + tracer = Trie(JsStatement::number) + ) + val collectedValues = mutableListOf>() + // .location field gets us "jsFile:A:B", then we get A and B as ints + val funcLocation = funcNode.firstChild!!.location.substringAfter("jsFile:") + .split(":").map { it.toInt() } + logger.info { "Function under test location according to parser is [${funcLocation[0]}, ${funcLocation[1]}]" } + val instrService = InstrumentationService(context, funcLocation[0] to funcLocation[1]) + instrService.instrument() + val coverageProvider = CoverageServiceProvider( + context, + instrService, + context.settings.coverageMode, + jsDescription + ) + val allStmts = instrService.allStatements + logger.info { "Statements to cover: (${allStmts.joinToString { toString() }})" } + val currentlyCoveredStmts = mutableSetOf() + val startTime = System.currentTimeMillis() + runFuzzing(jsDescription) { description, values -> + if (isCancelled() || System.currentTimeMillis() - startTime > fuzzingTimeout) + return@runFuzzing JsFeedback(Control.STOP) + collectedValues += values + if (collectedValues.size >= if (context.settings.coverageMode == CoverageMode.FAST) fuzzingThreshold else 1) { + try { + val (coveredStmts, executionResults) = coverageProvider.get( + collectedValues, + execId + ) + coveredStmts.zip(executionResults).forEach { (covData, resultData) -> + val params = collectedValues[resultData.index] + val result = + getUtModelResult( + execId = execId, + resultData = resultData, + jsDescription.thisInstance?.let { params.drop(1) } ?: params + ) + if (result is UtTimeoutException) { + emit(JsTimeoutExecution(result)) + return@runFuzzing JsFeedback(Control.PASS) + } else if (!currentlyCoveredStmts.containsAll(covData.additionalCoverage)) { + val (thisObject, modelList) = if (!funcNode.parent!!.isClassMembers) { + null to params + } else params[0] to params.drop(1) + val initEnv = + EnvironmentModels(thisObject, modelList, mapOf(), execId) + emit( + JsValidExecution( + JsUtFuzzedExecution( + stateBefore = initEnv, + stateAfter = initEnv, + result = result, + ) + ) + ) + currentlyCoveredStmts += covData.additionalCoverage + val trieNode = description.tracer.add(covData.additionalCoverage.map { JsStatement(it) }) + return@runFuzzing JsFeedback(control = Control.CONTINUE, result = trieNode) + } + if (currentlyCoveredStmts.containsAll(allStmts)) return@runFuzzing JsFeedback(Control.STOP) + } + } catch (e: TimeoutException) { + emit( + JsTimeoutExecution( + UtTimeoutException( + TimeoutException("Timeout on unknown test case. Consider using \"Basic\" coverage mode") + ) + ) + ) + return@runFuzzing JsFeedback(Control.STOP) + } finally { + collectedValues.clear() + } + } + return@runFuzzing JsFeedback(Control.PASS) + } + instrService.removeTempFiles() + } + + private fun manageExports( + classNode: Node?, + funcNode: Node, + execId: JsMethodId, + packageJson: PackageJson + ) { + val obligatoryExport = (classNode?.getClassName() ?: funcNode.getAbstractFunctionName()).toString() + val collectedExports = collectExports(execId) + val exportsProvider = IExportsProvider.providerByPackageJson(packageJson) + exports += (collectedExports + obligatoryExport) + exportsManager { existingSection, currentFileText -> + val existingExportsSet = existingSection?.let { section -> + val trimmedSection = section.substringAfter(exportsProvider.exportsPrefix) + .substringBeforeLast(exportsProvider.exportsPostfix) + val exportRegex = exportsProvider.exportsRegex + val existingExports = trimmedSection.split(exportsProvider.exportsDelimiter) + .filter { it.contains(exportRegex) && it.isNotBlank() } + existingExports.map { rawLine -> + exportRegex.find(rawLine)?.groups?.get(1)?.value ?: throw IllegalStateException() + }.toSet() + } ?: emptySet() + val resultSet = existingExportsSet + exports.toSet() + val resSection = resultSet.joinToString( + separator = exportsProvider.exportsDelimiter, + prefix = startComment + exportsProvider.exportsPrefix, + postfix = exportsProvider.exportsPostfix + endComment, + ) { + exportsProvider.getExportsFrame(it) + } + existingSection?.let { currentFileText.replace(startComment + existingSection + endComment, resSection) } ?: resSection + } + } + + private fun makeMethodsToTest(): List { + return selectedMethods?.map { + getFunctionNode( + focusedMethodName = it, + parentClassName = parentClassName, + ) + } ?: getMethodsToTest() + } + + private fun makeJsClassId( + classNode: Node?, + ternService: TernService + ): JsClassId { + return classNode?.let { + JsClassId(parentClassName!!).constructClass(ternService, classNode) + } ?: jsUndefinedClassId.constructClass( + ternService = ternService, + functions = extractToplevelFunctions() + ) + } + + private fun extractToplevelFunctions(): List { + val visitor = JsToplevelFunctionAstVisitor() + visitor.accept(parsedFile) + return visitor.extractedMethods + } + + private fun collectExports(methodId: JsMethodId): List { + return (listOf(methodId.returnType) + methodId.parameters).flatMap { it.collectExportsRecursively() } + } + + private fun JsClassId.collectExportsRecursively(): List { + return when { + this.isClass -> listOf(this.name) + (this.constructor?.parameters ?: emptyList()) + .flatMap { it.collectExportsRecursively() } + + this.isJsArray -> (this.elementClassId as? JsClassId)?.collectExportsRecursively() ?: emptyList() + else -> emptyList() + } + } + + private fun getFunctionNode(focusedMethodName: String, parentClassName: String?): Node { + return parentClassName?.let { astScrapper.findMethod(parentClassName, focusedMethodName, parsedFile) } + ?: astScrapper.findFunction(focusedMethodName, parsedFile) + ?: throw IllegalStateException( + "Couldn't locate function \"$focusedMethodName\" with class ${parentClassName ?: ""}" + ) + } + + private fun getMethodsToTest() = + parentClassName?.let { + getClassMethods(it) + } ?: extractToplevelFunctions().ifEmpty { + getClassMethods("") + } + + private fun getClassMethods(className: String): List { + val classNode = astScrapper.findClass(className, parsedFile) + return classNode?.getClassMethods() ?: throw IllegalStateException("Can't extract methods of class $className") + } +} diff --git a/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt new file mode 100644 index 0000000000..09c5b74af3 --- /dev/null +++ b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt @@ -0,0 +1,141 @@ +package api + +import framework.api.js.JsClassId +import framework.api.js.JsEmptyClassId +import framework.api.js.JsMethodId +import framework.api.js.JsNullModel +import framework.api.js.JsPrimitiveModel +import framework.api.js.JsUndefinedModel +import framework.api.js.util.defaultJsValueModel +import framework.api.js.util.isJsArray +import framework.api.js.util.isJsMap +import framework.api.js.util.isJsSet +import framework.api.js.util.jsErrorClassId +import framework.api.js.util.jsUndefinedClassId +import fuzzer.JsIdProvider +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +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.UtModel +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructorInterface + +class JsUtModelConstructor : UtModelConstructorInterface { + + // TODO SEVERE: Requires substantial expansion to other types + @Suppress("NAME_SHADOWING") + override fun construct(value: Any?, classId: ClassId): UtModel { + val classId = classId as JsClassId + if (classId == jsErrorClassId) return UtModel(jsErrorClassId) + return when (value) { + null -> JsNullModel(classId) + is Byte, + is Short, + is Char, + is Int, + is Long, + is Float, + is Double, + is String, + is Boolean -> JsPrimitiveModel(value) + is List<*> -> { + constructStructure(classId, value) + } + is Map<*, *> -> { + constructObject(classId, value) + } + + else -> JsUndefinedModel(classId) + } + } + + private fun constructStructure(classId: JsClassId, values: List): UtModel { + return when { + classId.isJsSet -> { + UtAssembleModel( + id = JsIdProvider.createId(), + classId = classId, + modelName = "", + instantiationCall = UtExecutableCallModel( + null, + ConstructorId(classId, emptyList()), + emptyList() + ), + modificationsChainProvider = { mutableListOf() } + ).apply { + this.modificationsChain as MutableList += values.map { value -> + UtExecutableCallModel( + this, + JsMethodId( + classId = classId, + name = "add", + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = listOf(jsUndefinedClassId) + ), + listOf(construct(value, jsUndefinedClassId)) + ) + } + } + } + classId.isJsArray -> { + UtArrayModel( + id = JsIdProvider.createId(), + classId = classId, + stores = buildMap { + putAll(values.indices.zip(values.map { + construct(it, jsUndefinedClassId) + })) + } as MutableMap, + length = values.size, + constModel = jsUndefinedClassId.defaultJsValueModel() + ) + } + classId.isJsMap -> { + UtAssembleModel( + id = JsIdProvider.createId(), + classId = classId, + modelName = "", + instantiationCall = UtExecutableCallModel( + null, + ConstructorId(classId, emptyList()), + emptyList() + ), + modificationsChainProvider = { mutableListOf() } + ).apply { + this.modificationsChain as MutableList += values.map { value -> + UtExecutableCallModel( + this, + JsMethodId( + classId = classId, + name = "set", + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = listOf(jsUndefinedClassId, jsUndefinedClassId) + ), + (value as Pair).toList().map { construct(it, jsUndefinedClassId) } + ) + } + } + } + else -> throw UnsupportedOperationException( + "Can't make UtModel from JavaScript structure with ${classId.name} type" + ) + } + } + + @Suppress("UNCHECKED_CAST") + private fun constructObject(classId: JsClassId, value: Any?): UtModel { + val constructor = classId.allConstructors.first() + val values = (value as Map).values.map { + construct(it, JsEmptyClassId()) + } + val id = JsIdProvider.createId() + val instantiationCall = UtExecutableCallModel(null, constructor, values) + return UtAssembleModel( + id = id, + classId = constructor.classId, + modelName = "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), + instantiationCall = instantiationCall, + ) + } +} diff --git a/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt new file mode 100644 index 0000000000..f20a5ba837 --- /dev/null +++ b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt @@ -0,0 +1,79 @@ +package codegen + +import framework.api.js.JsClassId +import framework.codegen.JsCgLanguageAssistant +import framework.codegen.JsImport +import framework.codegen.Mocha +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.SimpleTestClassModel +import org.utbot.framework.codegen.generator.CodeGeneratorResult +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.codegen.tree.CgSimpleTestClassConstructor +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MockFramework + +class JsCodeGenerator( + private val classUnderTest: JsClassId, + paramNames: MutableMap> = mutableMapOf(), + testFramework: TestFramework = Mocha, + runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, + hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), + enableTestsTimeout: Boolean = true, + testClassPackageName: String = classUnderTest.packageName, + imports: List, +) { + private var context: CgContext = CgContext( + classUnderTest = classUnderTest, + projectType = ProjectType.JavaScript, + paramNames = paramNames, + testFramework = testFramework, + mockFramework = MockFramework.MOCKITO, + codegenLanguage = CodegenLanguage.defaultItem, + cgLanguageAssistant = JsCgLanguageAssistant, + parametrizedTestSource = ParametrizedTestSource.defaultItem, + staticsMocking = StaticsMocking.defaultItem, + forceStaticMocking = ForceStaticMocking.defaultItem, + generateWarningsForStaticMocking = true, + runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, + hangingTestsTimeout = hangingTestsTimeout, + enableTestsTimeout = enableTestsTimeout, + testClassPackageName = testClassPackageName, + collectedImports = imports.toMutableSet() + ) + + fun generateAsStringWithTestReport( + cgTestSets: List, + testClassCustomName: String? = null, + ): CodeGeneratorResult = withCustomContext(testClassCustomName) { + val testClassModel = SimpleTestClassModel(classUnderTest, cgTestSets) + val astConstructor = CgSimpleTestClassConstructor(context) + val testClassFile = astConstructor.construct(testClassModel) + CodeGeneratorResult(renderClassFile(testClassFile), astConstructor.testsGenerationReport) + } + + private fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { + val prevContext = context + return try { + context = prevContext.customCopy(shouldOptimizeImports = true, testClassCustomName = testClassCustomName) + block() + } finally { + context = prevContext + } + } + + private fun renderClassFile(file: CgClassFile): String { + val renderer = CgAbstractRenderer.makeRenderer(context) + file.accept(renderer) + return renderer.toString() + } +} diff --git a/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt b/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt new file mode 100644 index 0000000000..c918a98a96 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt @@ -0,0 +1,121 @@ +package framework.api.js + +import framework.api.js.util.toJsClassId +import java.lang.reflect.Modifier +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.primitiveModelValueToClassId + +open class JsClassId( + private val jsName: String, + private val methods: Sequence = emptySequence(), + val constructor: JsConstructorId? = null, + private val classPackagePath: String = "", + private val classFilePath: String = "", + elementClassId: JsClassId? = null +) : ClassId(jsName, elementClassId) { + override val simpleName: String + get() = jsName + + override val simpleNameWithEnclosingClasses: String + get() = jsName + + override val allMethods: Sequence + get() = methods + + override val allConstructors: Sequence + get() = if (constructor == null) emptySequence() else sequenceOf(constructor) + + override val packageName: String + get() = classPackagePath + + override val canonicalName: String + get() = jsName + + override val isAnonymous: Boolean + get() = false + + override val isInDefaultPackage: Boolean + get() = false + + override val isInner: Boolean + get() = false + + override val isLocal: Boolean + get() = false + + override val isNested: Boolean + get() = false + + override val isNullable: Boolean + get() = false + + override val isSynthetic: Boolean + get() = false + + override val outerClass: Class<*>? + get() = null + + val filePath: String + get() = classFilePath + +} + +class JsEmptyClassId : JsClassId("empty") +class JsMethodId( + override var classId: JsClassId, + override val name: String, + private val returnTypeNotLazy: JsClassId, + private val parametersNotLazy: List, + private val staticModifier: Boolean = false, + private val lazyReturnType: Lazy? = null, + private val lazyParameters: Lazy>? = null +) : MethodId(classId, name, returnTypeNotLazy, parametersNotLazy) { + + override val parameters: List + get() = lazyParameters?.value ?: parametersNotLazy + + override val returnType: JsClassId + get() = lazyReturnType?.value ?: returnTypeNotLazy + + override val modifiers: Int + get() = if (staticModifier) Modifier.STATIC else 0 + +} + +class JsConstructorId( + override var classId: JsClassId, + override val parameters: List, +) : ConstructorId(classId, parameters) { + override val modifiers: Int + get() = 0 +} + +class JsMultipleClassId(jsJoinedName: String) : JsClassId(jsJoinedName) + +open class JsUtModel( + override val classId: JsClassId +) : UtModel(classId) + +class JsNullModel( + override val classId: JsClassId +) : JsUtModel(classId) { + override fun toString() = "null" +} + +class JsUndefinedModel( + classId: JsClassId +) : JsUtModel(classId) { + override fun toString() = "undefined" +} + +data class JsPrimitiveModel( + val value: Any, +) : JsUtModel(jsPrimitiveModelValueToClassId(value)) { + override fun toString() = value.toString() +} + +private fun jsPrimitiveModelValueToClassId(value: Any) = + primitiveModelValueToClassId(value).toJsClassId() diff --git a/utbot-js/src/main/kotlin/framework/api/js/JsUtFuzzedExecution.kt b/utbot-js/src/main/kotlin/framework/api/js/JsUtFuzzedExecution.kt new file mode 100644 index 0000000000..4b6168b579 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/api/js/JsUtFuzzedExecution.kt @@ -0,0 +1,25 @@ +package framework.api.js + +import org.utbot.framework.plugin.api.* + +class JsUtFuzzedExecution( + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + result: UtExecutionResult +) : UtExecution(stateBefore, stateAfter, result, null, null, null, null) { + override fun copy( + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + result: UtExecutionResult, + coverage: Coverage?, + summary: List?, + testMethodName: String?, + displayName: String? + ): UtExecution { + return JsUtFuzzedExecution( + stateBefore = stateBefore, + stateAfter = stateAfter, + result = result + ) + } +} diff --git a/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt b/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt new file mode 100644 index 0000000000..f82ce4ac0a --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt @@ -0,0 +1,68 @@ +package framework.api.js.util + +import framework.api.js.JsClassId +import framework.api.js.JsMultipleClassId +import org.utbot.framework.plugin.api.ClassId +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.doubleClassId +import org.utbot.framework.plugin.api.util.floatClassId + +val jsUndefinedClassId = JsClassId("undefined") +val jsNumberClassId = JsClassId("number") +val jsBooleanClassId = JsClassId("bool") +val jsDoubleClassId = JsClassId("double") +val jsStringClassId = JsClassId("string") +val jsErrorClassId = JsClassId("error") + +val jsBasic = setOf( + jsBooleanClassId, + jsDoubleClassId, + jsUndefinedClassId, + jsStringClassId, + jsNumberClassId +) + +fun ClassId.toJsClassId() = + when { + this == booleanClassId -> jsBooleanClassId + this == doubleClassId -> jsDoubleClassId + this == floatClassId -> jsDoubleClassId + this.name.lowercase().contains("string") -> jsStringClassId + else -> jsUndefinedClassId + } + +fun JsClassId.defaultJsValueModel(): UtModel = when (this) { + jsNumberClassId -> UtPrimitiveModel(0.0) + jsDoubleClassId -> UtPrimitiveModel(Double.POSITIVE_INFINITY) + jsBooleanClassId -> UtPrimitiveModel(false) + jsStringClassId -> UtPrimitiveModel("default") + jsUndefinedClassId -> UtPrimitiveModel(0.0) + else -> UtNullModel(this) +} + +val JsClassId.isJsBasic: Boolean + get() = this in jsBasic || this.isJsStdStructure + +val JsClassId.isExportable: Boolean + get() = !(this.isJsBasic || this == jsErrorClassId || this is JsMultipleClassId) + +val JsClassId.isClass: Boolean + get() = !(this.isJsBasic || this == jsErrorClassId || this is JsMultipleClassId) + +val JsClassId.isUndefined: Boolean + get() = this == jsUndefinedClassId + +val JsClassId.isJsArray: Boolean + get() = this.name == "Array" + +val JsClassId.isJsMap: Boolean + get() = this.name == "Map" + +val JsClassId.isJsSet: Boolean + get() = this.name == "Set" + +val JsClassId.isJsStdStructure: Boolean + get() = this.isJsArray || this.isJsSet || this.isJsMap diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsCgLanguageAssistant.kt b/utbot-js/src/main/kotlin/framework/codegen/JsCgLanguageAssistant.kt new file mode 100644 index 0000000000..3c07664b18 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/JsCgLanguageAssistant.kt @@ -0,0 +1,47 @@ +package framework.codegen + +import framework.codegen.model.constructor.tree.JsCgCallableAccessManager +import framework.codegen.model.constructor.tree.JsCgMethodConstructor +import framework.codegen.model.constructor.tree.JsCgStatementConstructor +import framework.codegen.model.constructor.tree.JsCgVariableConstructor +import framework.codegen.model.constructor.visitor.CgJsRenderer +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.TestClassContext +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.codegen.renderer.CgPrinter +import org.utbot.framework.codegen.renderer.CgRendererContext +import org.utbot.framework.codegen.services.language.AbstractCgLanguageAssistant +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.utils.ClassNameUtils.generateTestClassName + +object JsCgLanguageAssistant : AbstractCgLanguageAssistant() { + + override val outerMostTestClassContent: TestClassContext = TestClassContext() + + override val extension: String + get() = ".js" + + override val languageKeywords: Set = setOf( + "abstract", "arguments", "await", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", + "debugger", "default", "delete", "do", "double", "else", "enum", "eval", "export", "extends", "false", "final", + "finally", "float", "for", "function", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", + "let", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", + "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typeof", "var", "void", + "volatile", "while", "with", "yield" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + return generateTestClassName(testClassCustomName, testClassPackageName, classUnderTest) + } + + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = CgJsRenderer(context, printer) + override fun getCallableAccessManagerBy(context: CgContext) = JsCgCallableAccessManager(context) + override fun getMethodConstructorBy(context: CgContext) = JsCgMethodConstructor(context) + override fun getStatementConstructorBy(context: CgContext) = JsCgStatementConstructor(context) + override fun getVariableConstructorBy(context: CgContext) = JsCgVariableConstructor(context) + override fun getLanguageTestFrameworkManager() = JsTestFrameworkManager() +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt new file mode 100644 index 0000000000..8e8c32227e --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt @@ -0,0 +1,86 @@ +package framework.codegen + +import framework.api.js.JsClassId +import framework.api.js.util.jsErrorClassId +import framework.api.js.util.jsUndefinedClassId +import org.utbot.framework.codegen.domain.Import +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.ClassId +import service.PackageJson + + +object Mocha : TestFramework(id = "Mocha", displayName = "Mocha") { + override val mainPackage = "" + override val assertionsClass = jsUndefinedClassId + override val arraysAssertionsClass = jsUndefinedClassId + override val kotlinFailureAssertionsClass: ClassId = jsUndefinedClassId + + override val beforeMethodId: ClassId = jsUndefinedClassId + override val afterMethodId: ClassId = jsUndefinedClassId + + override val nestedClassesShouldBeStatic: Boolean + get() = false + override val argListClassId: ClassId + get() = jsUndefinedClassId + + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List { + throw UnsupportedOperationException() + } + + override val testAnnotationId = BuiltinClassId( + name = "Mocha", + canonicalName = "Mocha", + simpleName = "Test" + ) + + override val parameterizedTestAnnotationId = jsUndefinedClassId + override val methodSourceAnnotationId = jsUndefinedClassId +} + +internal val jsAssertEquals by lazy { + BuiltinMethodId( + JsClassId("assert.deepEqual"), "assert.deepEqual", jsUndefinedClassId, listOf( + jsUndefinedClassId, jsUndefinedClassId + ) + ) +} + +internal val jsAssertThrows by lazy { + BuiltinMethodId( + JsClassId("assert.throws"), "assert.throws", jsErrorClassId, listOf( + jsUndefinedClassId, jsUndefinedClassId, jsUndefinedClassId + ) + ) +} + +enum class ModuleType { + MODULE, + COMMONJS; + + companion object { + fun fromPackageJson(packageJson: PackageJson): ModuleType { + return when (packageJson.isModule) { + true -> MODULE + else -> COMMONJS + } + } + } +} + +data class JsImport( + val name: String, + val aliases: String, + val path: String, + val type: ModuleType +): Import(2) { + + override val qualifiedName: String = "$name as $aliases from $path" +} diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsTestFrameworkManager.kt b/utbot-js/src/main/kotlin/framework/codegen/JsTestFrameworkManager.kt new file mode 100644 index 0000000000..525a267bd6 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/JsTestFrameworkManager.kt @@ -0,0 +1,17 @@ +package framework.codegen + +import framework.codegen.model.constructor.tree.MochaManager +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.services.language.LanguageTestFrameworkManager + +class JsTestFrameworkManager: LanguageTestFrameworkManager() { + + override fun managerByFramework(context: CgContext) = when (context.testFramework) { + is Mocha -> MochaManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework = Mocha + + override val testFrameworks = listOf(Mocha) +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt new file mode 100644 index 0000000000..4c44a4c433 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt @@ -0,0 +1,60 @@ +package framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgStaticFieldAccess +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.services.access.CgIncompleteMethodCall +import org.utbot.framework.codegen.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MethodId + +class JsCgCallableAccessManager(context: CgContext) : CgCallableAccessManager, + CgContextOwner by context { + + override operator fun CgExpression?.get(methodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(methodId, this) + + override operator fun ClassId.get(staticMethodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(staticMethodId, null) + + override fun CgExpression.get(fieldId: FieldId): CgExpression { + TODO("Not yet implemented") + } + + override fun ClassId.get(fieldId: FieldId): CgStaticFieldAccess { + TODO("Not yet implemented") + } + + override operator fun ConstructorId.invoke(vararg args: Any?): CgExecutableCall { + val resolvedArgs = args.resolve() + val constructorCall = CgConstructorCall(this, resolvedArgs) + newConstructorCall(this) + return constructorCall + } + + override fun CgIncompleteMethodCall.invoke(vararg args: Any?): CgMethodCall { + val resolvedArgs = args.resolve() + val methodCall = CgMethodCall(caller, method, resolvedArgs) + newMethodCall(method) + return methodCall + } + + private fun newConstructorCall(constructorId: ConstructorId) { + importedClasses += constructorId.classId + } + + private fun newMethodCall(methodId: MethodId) { + if (methodId.classId.name == "undefined") { + importedStaticMethods += methodId + return + } + importedClasses += methodId.classId + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt new file mode 100644 index 0000000000..38102712da --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt @@ -0,0 +1,116 @@ +package framework.codegen.model.constructor.tree + +import framework.api.js.JsClassId +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTestMethodType +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.tree.CgMethodConstructor +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.onFailure +import org.utbot.framework.plugin.api.onSuccess +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.framework.util.isUnit + +class JsCgMethodConstructor(ctx: CgContext) : CgMethodConstructor(ctx) { + + override fun assertEquality(expected: CgValue, actual: CgVariable) { + testFrameworkManager.assertEquals(expected, actual) + } + + override fun createTestMethod(testSet: CgMethodTestSet, execution: UtExecution): CgTestMethod = + withTestMethodScope(execution) { + val testMethodName = nameGenerator.testMethodNameFor(testSet.executableUnderTest, execution.testMethodName) + execution.displayName = execution.displayName?.let { "${testSet.executableUnderTest.name}: $it" } + testMethod(testMethodName, execution.displayName) { + val statics = currentExecution!!.stateBefore.statics + rememberInitialStaticFields(statics) + val mainBody = { + substituteStaticFields(statics) + // build this instance + thisInstance = execution.stateBefore.thisInstance?.let { + variableConstructor.getOrCreateVariable(it) + } + // build arguments + for ((index, param) in execution.stateBefore.parameters.withIndex()) { + val name = paramNames[testSet.executableUnderTest]?.get(index) + methodArguments += variableConstructor.getOrCreateVariable(param, name) + } + recordActualResult() + generateResultAssertions() + generateFieldStateAssertions() + } + + if (statics.isNotEmpty()) { + +tryBlock { + mainBody() + }.finally { + recoverStaticFields() + } + } else { + mainBody() + } + } + } + + override fun generateResultAssertions() { + emptyLineIfNeeded() + val currentExecution = currentExecution!! + val method = currentExecutableUnderTest as MethodId + // build assertions + currentExecution.result + .onSuccess { result -> + methodType = CgTestMethodType.SUCCESSFUL + if (result.isUnit() || method.returnType == voidClassId) { + +thisInstance[method](*methodArguments.toTypedArray()) + } else { + resultModel = result + assertEquality(result, actual) + } + } + .onFailure { exception -> + processExecutionFailure(currentExecution, exception) + } + } + + private fun processExecutionFailure(execution: UtExecution, exception: Throwable) { + val methodInvocationBlock = { + with(currentExecutableUnderTest) { + when (this) { + is MethodId -> thisInstance[this](*methodArguments.toTypedArray()).intercepted() + is ConstructorId -> this(*methodArguments.toTypedArray()).intercepted() + else -> throw IllegalStateException() + } + } + } + + if (shouldTestPassWithException(execution, exception)) { + testFrameworkManager.expectException(JsClassId(exception.message!!)) { + methodInvocationBlock() + } + methodType = CgTestMethodType.SUCCESSFUL + + return + } + + if (shouldTestPassWithTimeoutException(execution, exception)) { + writeWarningAboutTimeoutExceeding() + testFrameworkManager.expectTimeout(hangingTestsTimeout.timeoutMs) { + methodInvocationBlock() + } + methodType = CgTestMethodType.TIMEOUT + + return + } + + methodType = CgTestMethodType.FAILING + writeWarningAboutFailureTest(exception) + + methodInvocationBlock() + } +} diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt new file mode 100644 index 0000000000..daa267d0ed --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt @@ -0,0 +1,286 @@ +package framework.codegen.model.constructor.tree + +import fj.data.Either +import framework.codegen.model.constructor.util.plus +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.AnnotationTarget +import org.utbot.framework.codegen.domain.models.CgAnnotation +import org.utbot.framework.codegen.domain.models.CgAnonymousFunction +import org.utbot.framework.codegen.domain.models.CgComment +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgEmptyLine +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgIfStatement +import org.utbot.framework.codegen.domain.models.CgInnerBlock +import org.utbot.framework.codegen.domain.models.CgIsInstance +import org.utbot.framework.codegen.domain.models.CgLogicalAnd +import org.utbot.framework.codegen.domain.models.CgLogicalOr +import org.utbot.framework.codegen.domain.models.CgMultilineComment +import org.utbot.framework.codegen.domain.models.CgMultipleArgsAnnotation +import org.utbot.framework.codegen.domain.models.CgNamedAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgReturnStatement +import org.utbot.framework.codegen.domain.models.CgSingleArgAnnotation +import org.utbot.framework.codegen.domain.models.CgSingleLineComment +import org.utbot.framework.codegen.domain.models.CgThrowStatement +import org.utbot.framework.codegen.domain.models.CgTryCatch +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.tree.* +import org.utbot.framework.codegen.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtModel + +class JsCgStatementConstructor(context: CgContext) : + CgStatementConstructor, + CgContextOwner by context, + CgCallableAccessManager by CgComponents.getCallableAccessManagerBy(context) { + + private val nameGenerator = CgComponents.getNameGeneratorBy(context) + + override fun newVar( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutable: Boolean, + init: () -> CgExpression + ): CgVariable { + val declarationOrVar: Either = + createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType, + model, + baseName, + isMock, + isMutable, + init + ) + + return declarationOrVar.either( + { declaration -> + currentBlock += declaration + + declaration.variable + }, + { variable -> variable } + ) + } + + override fun createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutableVar: Boolean, + init: () -> CgExpression + ): Either { + + val baseExpr = init() + + val name = nameGenerator.variableName(baseType, baseName, isMock) + + // TODO SEVERE: here was import section for CgClassId. Implement it +// importIfNeeded(baseType) +// if ((baseType as JsClassId).name != "undefined") { +// importedClasses += baseType +// } + + val declaration = buildDeclaration { + variableType = baseType + variableName = name + initializer = baseExpr + isMutable = isMutableVar + } + + rememberVariableForModel(declaration.variable, model) + + return Either.left(declaration) + } + + override fun CgExpression.`=`(value: Any?) { + currentBlock += buildAssignment { + lValue = this@`=` + rValue = value.resolve() + } + } + + override fun CgExpression.and(other: CgExpression): CgLogicalAnd = + CgLogicalAnd(this, other) + + + override fun CgExpression.or(other: CgExpression): CgLogicalOr = + CgLogicalOr(this, other) + + override fun ifStatement( + condition: CgExpression, + trueBranch: () -> Unit, + falseBranch: (() -> Unit)? + ): CgIfStatement { + val trueBranchBlock = block(trueBranch) + val falseBranchBlock = falseBranch?.let { block(it) } + return CgIfStatement(condition, trueBranchBlock, falseBranchBlock).also { + currentBlock += it + } + } + + override fun forLoop(init: CgForLoopBuilder.() -> Unit) { + currentBlock += buildForLoop(init) + } + + override fun whileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun doWhileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildDoWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun forEachLoop(init: CgForEachLoopBuilder.() -> Unit) { + throw UnsupportedOperationException("JavaScript does not have forEach loops") + } + + override fun getClassOf(classId: ClassId): CgExpression { + TODO("Not yet implemented") + } + + override fun createFieldVariable(fieldId: FieldId): CgVariable { + TODO("Not yet implemented") + } + + override fun createExecutableVariable(executableId: ExecutableId, arguments: List): CgVariable { + TODO("Not yet implemented") + } + + override fun tryBlock(init: () -> Unit): CgTryCatch = tryBlock(init, null) + + override fun tryBlock(init: () -> Unit, resources: List?): CgTryCatch = + buildTryCatch { + statements = block(init) + this.resources = resources + } + + override fun CgTryCatch.catch(exception: ClassId, init: (CgVariable) -> Unit): CgTryCatch { + val newHandler = buildExceptionHandler { + val e = declareVariable(exception, nameGenerator.variableName(exception.simpleName.decapitalize())) + this.exception = e + this.statements = block { init(e) } + } + return this.copy(handlers = handlers + newHandler) + } + + override fun CgTryCatch.finally(init: () -> Unit): CgTryCatch { + val finallyBlock = block(init) + return this.copy(finally = finallyBlock) + } + + override fun CgExpression.isInstance(value: CgExpression): CgIsInstance { + TODO("Not yet implemented") + } + + // TODO MINOR: check whether js has inner blocks + override fun innerBlock(init: () -> Unit): CgInnerBlock = + CgInnerBlock(block(init)).also { + currentBlock += it + } + + override fun comment(text: String): CgComment = + CgSingleLineComment(text).also { + currentBlock += it + } + + override fun comment(): CgComment = + CgSingleLineComment("").also { + currentBlock += it + } + + override fun multilineComment(lines: List): CgComment = + CgMultilineComment(lines).also { + currentBlock += it + } + + override fun lambda(type: ClassId, vararg parameters: CgVariable, body: () -> Unit): CgAnonymousFunction { + return withNameScope { + for (parameter in parameters) { + declareParameter(parameter.type, parameter.name) + } + val paramDeclarations = parameters.map { CgParameterDeclaration(it) } + CgAnonymousFunction(type, paramDeclarations, block(body)) + } + } + + override fun addAnnotation(classId: ClassId, argument: Any?, target: AnnotationTarget): CgAnnotation { + val annotation = CgSingleArgAnnotation(classId, argument.resolve(), target) + addAnnotation(annotation) + return annotation + } + + override fun addAnnotation( + classId: ClassId, + namedArguments: List, + target: AnnotationTarget, + ): CgAnnotation { + val annotation = CgMultipleArgsAnnotation(classId, namedArguments.toMutableList(), target) + addAnnotation(annotation) + return annotation + } + + override fun addAnnotation( + classId: ClassId, + target: AnnotationTarget, + buildArguments: MutableList>.() -> Unit, + ): CgAnnotation { + val arguments = mutableListOf>() + .apply(buildArguments) + .map { (name, value) -> CgNamedAnnotationArgument(name, value) } + val annotation = CgMultipleArgsAnnotation(classId, arguments.toMutableList(), target) + addAnnotation(annotation) + return annotation + } + + private fun addAnnotation(annotation: CgAnnotation) { + when (annotation.target) { + AnnotationTarget.Method -> collectedMethodAnnotations.add(annotation) + AnnotationTarget.Class, + AnnotationTarget.Field -> error("Annotation ${annotation.target} is not supported in JavaScript") + } + + importIfNeeded(annotation.classId) + } + + override fun returnStatement(expression: () -> CgExpression) { + currentBlock += CgReturnStatement(expression()) + } + + override fun throwStatement(exception: () -> CgExpression): CgThrowStatement = + CgThrowStatement(exception()).also { currentBlock += it } + + override fun emptyLine() { + currentBlock += CgEmptyLine + } + + override fun emptyLineIfNeeded() { + val lastStatement = currentBlock.lastOrNull() ?: return + if (lastStatement is CgEmptyLine) return + emptyLine() + } + + override fun declareVariable(type: ClassId, name: String): CgVariable = + CgVariable(name, type).also { + rememberVariableForModel(it) + } + + // TODO SEVERE: think about these 2 functions + override fun guardExpression(baseType: ClassId, expression: CgExpression): ExpressionWithType = + ExpressionWithType(baseType, expression) + + override fun wrapTypeIfRequired(baseType: ClassId): ClassId = baseType +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt new file mode 100644 index 0000000000..ef6f845f32 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt @@ -0,0 +1,181 @@ +package framework.codegen.model.constructor.tree + +import framework.api.js.JsClassId +import framework.api.js.JsNullModel +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.isExportable +import framework.api.js.util.jsBooleanClassId +import framework.api.js.util.jsDoubleClassId +import framework.api.js.util.jsNumberClassId +import framework.api.js.util.jsStringClassId +import framework.api.js.util.jsUndefinedClassId +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgAllocateArray +import org.utbot.framework.codegen.domain.models.CgArrayInitializer +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.tree.CgComponents +import org.utbot.framework.codegen.tree.CgVariableConstructor +import org.utbot.framework.codegen.util.at +import org.utbot.framework.codegen.util.inc +import org.utbot.framework.codegen.util.lessThan +import org.utbot.framework.codegen.util.nullLiteral +import org.utbot.framework.codegen.util.resolve +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.UtModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.util.defaultValueModel +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set + +class JsCgVariableConstructor(ctx: CgContext) : CgVariableConstructor(ctx) { + + private val nameGenerator = CgComponents.getNameGeneratorBy(context) + + override fun getOrCreateVariable(model: UtModel, name: String?): CgValue = + valueByUtModelWrapper.getOrPut(model.wrap()) { + when (model) { + is UtAssembleModel -> { + // TODO SEVERE: May lead to unexpected behavior in case of changes to the original method + super.getOrCreateVariable(model, name) + } + + is JsPrimitiveModel -> CgLiteral(model.classId, model.value) + is UtArrayModel -> { + val baseName = name ?: nameGenerator.nameFrom(model.classId) + constructArray(model, baseName) + } + + else -> nullLiteral() + } + } + + private val MAX_ARRAY_INITIALIZER_SIZE = 10 + + private operator fun UtArrayModel.get(index: Int): UtModel = stores[index] ?: constModel + + private val defaultByPrimitiveType: Map = mapOf( + jsBooleanClassId to false, + jsStringClassId to "default", + jsUndefinedClassId to 0.0, + jsNumberClassId to 0.0, + jsDoubleClassId to Double.POSITIVE_INFINITY + ) + + private infix fun UtModel.isNotJsDefaultValueOf(type: JsClassId): Boolean = !(this isJsDefaultValueOf type) + + private infix fun UtModel.isJsDefaultValueOf(type: JsClassId): Boolean = when (this) { + is JsNullModel -> type.isExportable + is JsPrimitiveModel -> value == defaultByPrimitiveType[type] + else -> false + } + + private fun CgVariable.setArrayElement(index: Any, value: CgValue) { + val i = index.resolve() + this.at(i) `=` value + } + + private fun basicForLoop(until: Any, body: (i: CgExpression) -> Unit) { + basicForLoop(start = 0, until, body) + } + + private fun basicForLoop(start: Any, until: Any, body: (i: CgExpression) -> Unit) { + forLoop { + val (i, init) = loopInitialization(jsNumberClassId, "i", start.resolve()) + initialization = init + condition = i lessThan until.resolve() + update = i.inc() + statements = block { body(i) } + } + } + + private fun loopInitialization( + variableType: ClassId, + baseVariableName: String, + initializer: Any? + ): Pair { + val declaration = CgDeclaration(variableType, baseVariableName.toVarName(), initializer.resolve()) + val variable = declaration.variable + rememberVariableForModel(variable) + return variable to declaration + } + + private fun constructArray(arrayModel: UtArrayModel, baseName: String?): CgVariable { + val elementType = (arrayModel.classId.elementClassId ?: jsUndefinedClassId) as JsClassId + val elementModels = (0 until arrayModel.length).map { + arrayModel.stores.getOrDefault(it, arrayModel.constModel) + } + + val allPrimitives = elementModels.all { it is JsPrimitiveModel } + val allNulls = elementModels.all { it is JsNullModel } + // we can use array initializer if all elements are primitives or all of them are null, + // and the size of an array is not greater than the fixed maximum size + val canInitWithValues = (allPrimitives || allNulls) && elementModels.size <= MAX_ARRAY_INITIALIZER_SIZE + + val initializer = if (canInitWithValues) { + val elements = elementModels.map { model -> + when (model) { + is JsPrimitiveModel -> model.value.resolve() + is UtNullModel -> null.resolve() + else -> error("Non primitive or null model $model is unexpected in array initializer") + } + } + CgArrayInitializer(arrayModel.classId, elementType, elements) + } else { + CgAllocateArray(arrayModel.classId, elementType, arrayModel.length) + } + + val array = newVar(arrayModel.classId, baseName) { initializer } + valueByUtModelWrapper[arrayModel.wrap()] = 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 = arrayModel[0] + if (elementModel isNotJsDefaultValueOf elementType) { + array.setArrayElement(0, getOrCreateVariable(elementModel)) + } + } else { + val indexedValuesFromStores = + if (arrayModel.stores.size == arrayModel.length) { + // do not use constModel because stores fully cover array + arrayModel.stores.entries.filter { (_, element) -> element isNotJsDefaultValueOf elementType } + } else { + // fill array if constModel is not default type value + if (arrayModel.constModel isNotJsDefaultValueOf elementType) { + val defaultVariable = getOrCreateVariable(arrayModel.constModel, "defaultValue") + basicForLoop(arrayModel.length) { i -> + array.setArrayElement(i, defaultVariable) + } + } + + // choose all not default values + val defaultValue = if (arrayModel.constModel isJsDefaultValueOf elementType) { + arrayModel.constModel + } else { + elementType.defaultValueModel() + } + arrayModel.stores.entries.filter { (_, element) -> element != defaultValue } + } + + // set all values from stores manually + indexedValuesFromStores + .sortedBy { it.key } + .forEach { (index, element) -> array.setArrayElement(index, getOrCreateVariable(element)) } + } + + return array + } + + private fun String.toVarName(): String = nameGenerator.variableName(this) +} diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt new file mode 100644 index 0000000000..13466f194e --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt @@ -0,0 +1,62 @@ +package framework.codegen.model.constructor.tree + +import framework.codegen.Mocha +import framework.codegen.jsAssertEquals +import framework.codegen.jsAssertThrows +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.TestClassContext +import org.utbot.framework.codegen.domain.models.CgAnnotation +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.services.framework.TestFrameworkManager +import org.utbot.framework.plugin.api.ClassId + +class MochaManager(context: CgContext) : TestFrameworkManager(context) { + override val isExpectedExceptionExecutionBreaking: Boolean = true + + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Mocha) { "According to settings, Mocha.js was expected, but got: $testFramework" } + val lambda = statementConstructor.lambda(exception) { block() } + +assertions[jsAssertThrows](lambda, "Error", exception.name) + } + + override fun addDataProviderAnnotations(dataProviderMethodName: String) { + error("Parametrized tests are not supported for JavaScript") + } + + override fun createArgList(length: Int): CgVariable { + error("Parametrized tests are not supported for JavaScript") + } + + override fun addParameterizedTestAnnotations(dataProviderMethodName: String?) { + error("Parametrized tests are not supported for JavaScript") + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + error("Parametrized tests are not supported for JavaScript") + } + + override fun addTestDescription(description: String) { + TODO("Not yet implemented") + } + + override val dataProviderMethodsHolder: TestClassContext + get() = error("Parametrized tests are not supported for JavaScript") + + override fun addAnnotationForNestedClasses() { + error("Nested classes annotation does not exist in Mocha") + } + + override fun assertEquals(expected: CgValue, actual: CgValue) { + +assertions[jsAssertEquals](expected, actual) + } + + override fun assertSame(expected: CgValue, actual: CgValue) { + error("assertSame does not exist in Mocha") + } + + override fun disableTestMethod(reason: String) { + + } + +} diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt new file mode 100644 index 0000000000..4b33d306c2 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -0,0 +1,16 @@ +package framework.codegen.model.constructor.util + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentSet + +internal operator fun PersistentList.plus(element: T): PersistentList = + this.add(element) + +internal operator fun PersistentList.plus(other: PersistentList): PersistentList = + this.addAll(other) + +internal operator fun PersistentSet.plus(element: T): PersistentSet = + this.add(element) + +internal operator fun PersistentSet.plus(other: PersistentSet): PersistentSet = + this.addAll(other) diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt new file mode 100644 index 0000000000..4ddcd6de10 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt @@ -0,0 +1,428 @@ +package framework.codegen.model.constructor.visitor + +import framework.api.js.JsClassId +import framework.api.js.util.isExportable +import framework.codegen.JsImport +import framework.codegen.ModuleType +import org.apache.commons.text.StringEscapeUtils +import org.utbot.framework.codegen.domain.RegularImport +import org.utbot.framework.codegen.domain.StaticImport +import org.utbot.framework.codegen.domain.models.CgAllocateArray +import org.utbot.framework.codegen.domain.models.CgAllocateInitializedArray +import org.utbot.framework.codegen.domain.models.CgAnonymousFunction +import org.utbot.framework.codegen.domain.models.CgArrayAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgArrayElementAccess +import org.utbot.framework.codegen.domain.models.CgArrayInitializer +import org.utbot.framework.codegen.domain.models.CgClass +import org.utbot.framework.codegen.domain.models.CgClassBody +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgEqualTo +import org.utbot.framework.codegen.domain.models.CgErrorTestMethod +import org.utbot.framework.codegen.domain.models.CgErrorWrapper +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgFieldAccess +import org.utbot.framework.codegen.domain.models.CgForLoop +import org.utbot.framework.codegen.domain.models.CgFormattedString +import org.utbot.framework.codegen.domain.models.CgFrameworkUtilMethod +import org.utbot.framework.codegen.domain.models.CgGetJavaClass +import org.utbot.framework.codegen.domain.models.CgGetKotlinClass +import org.utbot.framework.codegen.domain.models.CgGetLength +import org.utbot.framework.codegen.domain.models.CgInnerBlock +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgMultipleArgsAnnotation +import org.utbot.framework.codegen.domain.models.CgNamedAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgNotNullAssertion +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.domain.models.CgSpread +import org.utbot.framework.codegen.domain.models.CgStaticsRegion +import org.utbot.framework.codegen.domain.models.CgSwitchCase +import org.utbot.framework.codegen.domain.models.CgSwitchCaseLabel +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTypeCast +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.codegen.renderer.CgPrinter +import org.utbot.framework.codegen.renderer.CgPrinterImpl +import org.utbot.framework.codegen.renderer.CgRendererContext +import org.utbot.framework.codegen.tree.VisibilityModifier +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.TypeParameters +import org.utbot.framework.plugin.api.util.isStatic +import settings.JsTestGenerationSettings.fileUnderTestAliases + +internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgPrinterImpl()) : + CgAbstractRenderer(context, printer) { + + override val statementEnding: String = "" + + override val logicalAnd: String + get() = "&&" + + override val logicalOr: String + get() = "||" + + override val langPackage: String = "js" + + override val ClassId.methodsAreAccessibleAsTopLevel: Boolean + get() = false + + override fun visit(element: CgErrorWrapper) { + element.expression.accept(this) + print("alert(\"${element.message}\")") + } + + override fun visit(element: CgInnerBlock) { + println("{") + withIndent { + for (statement in element.statements) { + statement.accept(this) + } + } + println("}") + } + + override fun visit(element: CgParameterDeclaration) { + if (element.isVararg) { + print("...") + } + print(element.name.escapeNamePossibleKeyword()) + } + + override fun visit(element: CgLiteral) { + val value = with(element.value) { + when (this) { + is Double -> toStringConstant() + is String -> "\"" + escapeCharacters() + "\"" + else -> "$this" + } + } + print(value) + } + + private fun Double.toStringConstant() = when { + isNaN() -> "Number.NaN" + this == Double.POSITIVE_INFINITY -> "Number.POSITIVE_INFINITY" + this == Double.NEGATIVE_INFINITY -> "Number.NEGATIVE_INFINITY" + else -> "$this" + } + + override fun renderRegularImport(regularImport: RegularImport) { + println("const ${regularImport.packageName} = require(\"${regularImport.className}\")") + } + + override fun visit(element: CgStaticsRegion) { + if (element.content.isEmpty()) return + + print(regionStart) + element.header?.let { print(" $it") } + println() + + withIndent { + for (item in element.content) { + println() + item.accept(this) + } + } + + println(regionEnd) + } + + + override fun visit(element: CgClass) { + element.body.accept(this) + } + + override fun visit(element: CgFieldAccess) { + element.caller.accept(this) + renderAccess(element.caller) + print(element.fieldId.name) + } + + override fun visit(element: CgArrayElementAccess) { + element.array.accept(this) + print("[") + element.index.accept(this) + print("]") + } + + override fun visit(element: CgArrayAnnotationArgument) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgAnonymousFunction) { + print("function (") + element.parameters.renderSeparated(true) + println(") {") + // cannot use visit(element.body) here because { was already printed + withIndent { + for (statement in element.body) { + statement.accept(this) + } + } + print("}") + } + + override fun visit(element: CgEqualTo) { + element.left.accept(this) + print(" == ") + element.right.accept(this) + } + + // TODO SEVERE + override fun visit(element: CgTypeCast) { + element.expression.accept(this) +// throw Exception("TypeCast not yet implemented") + } + + override fun visit(element: CgSpread) { + print("...") + element.array.accept(this) + } + + override fun visit(element: CgNotNullAssertion) { + throw UnsupportedOperationException("JavaScript does not support not null assertions") + } + + override fun visit(element: CgAllocateArray) { + print("new Array(${element.size})") + } + + override fun visit(element: CgAllocateInitializedArray) { + print("[") + element.initializer.accept(this) + print("]") + } + + // TODO SEVERE: I am unsure about this + override fun visit(element: CgArrayInitializer) { + val elementType = element.elementType + val elementsInLine = arrayElementsInLine(elementType) + print("[") + element.values.renderElements(elementsInLine) + print("]") + } + + override fun visit(element: CgClassFile) { + element.imports.filterIsInstance().forEach { + renderImport(it) + } + println() + element.declaredClass.accept(this) + } + + override fun visit(element: CgSwitchCaseLabel) { + if (element.label != null) { + print("case ") + element.label!!.accept(this) + } else { + print("default") + } + println(": ") + visit(element.statements, printNextLine = true) + } + + @Suppress("DuplicatedCode") + override fun visit(element: CgSwitchCase) { + print("switch (") + element.value.accept(this) + println(") {") + withIndent { + for (caseLabel in element.labels) { + caseLabel.accept(this) + } + element.defaultLabel?.accept(this) + } + println("}") + } + + override fun visit(element: CgGetLength) { + element.variable.accept(this) + print(".size") + } + + override fun visit(element: CgGetJavaClass) { + throw UnsupportedOperationException("No Java classes in JavaScript") + } + + override fun visit(element: CgGetKotlinClass) { + throw UnsupportedOperationException("No Kotlin classes in JavaScript") + } + + override fun visit(element: CgConstructorCall) { + val importPrefix = "$fileUnderTestAliases.".takeIf { + (element.executableId.classId as JsClassId).isExportable + } ?: "" + print("new $importPrefix${element.executableId.classId.name}") + print("(") + element.arguments.renderSeparated() + print(")") + } + + private fun renderImport(import: JsImport) = with(import) { + when (type) { + ModuleType.COMMONJS -> println("const $aliases = require(\"$path\")") + ModuleType.MODULE -> println("import $name as $aliases from \"$path\"") + } + } + + override fun renderStaticImport(staticImport: StaticImport) { + throw Exception("Not implemented yet") + } + + override fun renderMethodSignature(element: CgTestMethod) { + println("it(\"${element.name}\", function ()") + } + + override fun renderMethodSignature(element: CgErrorTestMethod) { + println("it(\"${element.name}\", function ()") + + } + + override fun visit(element: CgMethod) { + super.visit(element) + if (element is CgTestMethod || element is CgErrorTestMethod) { + println(")") + } + } + + override fun visit(element: CgErrorTestMethod) { + renderMethodSignature(element) + visit(element as CgMethod) + } + + override fun visit(element: CgClassBody) { + // render regions for test methods + for ((i, region) in (element.methodRegions + element.nestedClassRegions).withIndex()) { + if (i != 0) println() + + region.accept(this) + } + + if (element.staticDeclarationRegions.isEmpty()) { + return + } + } + + override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { + throw UnsupportedOperationException() + } + + override fun renderMethodSignature(element: CgFrameworkUtilMethod) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgNamedAnnotationArgument) { + + } + + override fun visit(element: CgMultipleArgsAnnotation) { + + } + + override fun visit(element: CgMethodCall) { + val caller = element.caller + if (caller != null) { + caller.accept(this) + renderAccess(caller) + } else { + val method = element.executableId + if (method is BuiltinMethodId) { + + } else if (method.isStatic) { + val line = if (method.classId.toString() == "undefined") "" else "${method.classId}." + print("$fileUnderTestAliases.$line") + } else { + print("$fileUnderTestAliases.") + } + } + print(element.executableId.name.escapeNamePossibleKeyword()) + renderTypeParameters(element.typeParameters) + if (element.type.name == "error") { + print("(") + element.arguments[0].accept(this@CgJsRenderer) + print(", ") + print("Error, ") + element.arguments[2].accept(this@CgJsRenderer) + print(")") + } else { + renderExecutableCallArguments(element) + } + } + + override fun visit(element: CgFormattedString) { + throw NotImplementedError("String interpolation is not supported in JavaScript renderer") + } + + //TODO MINOR: check + override fun renderForLoopVarControl(element: CgForLoop) { + print("for (") + with(element.initialization) { + print("let ") + visit(variable) + print(" = ") + initializer?.accept(this@CgJsRenderer) + print("; ") + visit(element.condition) + print("; ") + print(element.update) + } + } + + override fun renderDeclarationLeftPart(element: CgDeclaration) { + if (element.isMutable) print("var ") else print("let ") + visit(element.variable) + } + + override fun toStringConstantImpl(byte: Byte) = "$byte" + + override fun toStringConstantImpl(short: Short) = "$short" + + override fun toStringConstantImpl(int: Int) = "$int" + + override fun toStringConstantImpl(long: Long) = "$long" + + override fun toStringConstantImpl(float: Float) = "$float" + + override fun renderAccess(caller: CgExpression) { + print(".") + } + + override fun renderTypeParameters(typeParameters: TypeParameters) { + //TODO MINOR: check + } + + override fun renderExecutableCallArguments(executableCall: CgExecutableCall) { + print("(") + executableCall.arguments.renderSeparated() + print(")") + } + + //TODO SEVERE: check + override fun renderExceptionCatchVariable(exception: CgVariable) { + print("${exception.name.escapeNamePossibleKeyword()}: ${exception.type}") + } + + override fun escapeNamePossibleKeywordImpl(s: String): String = s + + override fun renderVisibility(modifier: VisibilityModifier) { + TODO("Not yet implemented") + } + + override fun renderClassModality(aClass: CgClass) { + TODO("Not yet implemented") + } + + //TODO MINOR: check + override fun String.escapeCharacters(): String = + StringEscapeUtils.escapeJava(this) + .replace("$", "\\$") + .replace("\\f", "\\u000C") + .replace("\\xxx", "\\\u0058\u0058\u0058") +} diff --git a/utbot-js/src/main/kotlin/fuzzer/JsFuzzerApi.kt b/utbot-js/src/main/kotlin/fuzzer/JsFuzzerApi.kt new file mode 100644 index 0000000000..feb68bbc8c --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/JsFuzzerApi.kt @@ -0,0 +1,82 @@ +package fuzzer + +import framework.api.js.JsClassId +import framework.api.js.JsUtFuzzedExecution +import framework.api.js.util.isClass +import java.util.concurrent.atomic.AtomicInteger +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtTimeoutException +import org.utbot.fuzzing.Control +import org.utbot.fuzzing.Description +import org.utbot.fuzzing.Feedback +import org.utbot.fuzzing.utils.Trie + +sealed interface JsFuzzingExecutionFeedback +class JsValidExecution(val utFuzzedExecution: JsUtFuzzedExecution) : JsFuzzingExecutionFeedback + +class JsTimeoutExecution(val utTimeout: UtTimeoutException) : JsFuzzingExecutionFeedback + +class JsMethodDescription( + val name: String, + parameters: List, + val concreteValues: Collection, + val thisInstance: JsClassId? = null, + val tracer: Trie +) : Description(parameters) { + + constructor( + name: String, + parameters: List, + classId: JsClassId, + concreteValues: Collection, + tracer: Trie + ) : this( + name, + if (classId.isClass) listOf(classId) + parameters else parameters, + concreteValues, + classId.takeIf { it.isClass }, + tracer + ) +} + +data class JsFeedback( + override val control: Control = Control.CONTINUE, + val result: Trie.Node = Trie.emptyNode() +) : Feedback + +data class JsStatement( + val number: Int +) + +data class JsFuzzedConcreteValue( + val classId: ClassId, + val value: Any, + val fuzzedContext: JsFuzzedContext = JsFuzzedContext.Unknown, +) + +enum class JsFuzzedContext { + EQ, + NE, + GT, + GE, + LT, + LE, + Unknown; + + fun reverse(): JsFuzzedContext = when (this) { + EQ -> NE + NE -> EQ + GT -> LE + LT -> GE + LE -> GT + GE -> LT + Unknown -> Unknown + } +} + +object JsIdProvider { + private var id = AtomicInteger(0) + + fun createId() = id.incrementAndGet() +} diff --git a/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt b/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt new file mode 100644 index 0000000000..c9953083d8 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt @@ -0,0 +1,48 @@ +package fuzzer + +import framework.api.js.JsClassId +import fuzzer.providers.ArrayValueProvider +import fuzzer.providers.BoolValueProvider +import fuzzer.providers.MapValueProvider +import fuzzer.providers.NumberValueProvider +import fuzzer.providers.ObjectValueProvider +import fuzzer.providers.SetValueProvider +import fuzzer.providers.StringValueProvider +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.Fuzzing +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.fuzz + +fun defaultValueProviders() = listOf( + BoolValueProvider, + NumberValueProvider, + StringValueProvider, + MapValueProvider, + SetValueProvider, + ObjectValueProvider(), + ArrayValueProvider() +) + +class JsFuzzing( + val exec: suspend (JsMethodDescription, List) -> JsFeedback +) : Fuzzing { + + override fun generate(description: JsMethodDescription, type: JsClassId): Sequence> { + return defaultValueProviders().asSequence().flatMap { provider -> + if (provider.accept(type)) { + provider.generate(description, type) + } else { + emptySequence() + } + } + } + + override suspend fun handle(description: JsMethodDescription, values: List): JsFeedback { + return exec(description, values) + } +} + +suspend fun runFuzzing( + description: JsMethodDescription, + exec: suspend (JsMethodDescription, List) -> JsFeedback +) = JsFuzzing(exec).fuzz(description) diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/ArrayValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/ArrayValueProvider.kt new file mode 100644 index 0000000000..ab966c29d2 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/ArrayValueProvider.kt @@ -0,0 +1,38 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.util.defaultJsValueModel +import framework.api.js.util.isJsArray +import fuzzer.JsIdProvider +import fuzzer.JsMethodDescription +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +class ArrayValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean = type.isJsArray + + override fun generate( + description: JsMethodDescription, + type: JsClassId + ) = sequence> { + yield( + Seed.Collection( + construct = Routine.Collection { + UtArrayModel( + id = JsIdProvider.createId(), + classId = type, + length = it, + constModel = (type.elementClassId!! as JsClassId).defaultJsValueModel(), + stores = hashMapOf(), + ) + }, + modify = Routine.ForEach(listOf(type.elementClassId!! as JsClassId)) { self, i, values -> + (self as UtArrayModel).stores[i] = values.first() + } + )) + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/BoolValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/BoolValueProvider.kt new file mode 100644 index 0000000000..87031b6559 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/BoolValueProvider.kt @@ -0,0 +1,27 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.isJsBasic +import fuzzer.JsMethodDescription +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.seeds.Bool + +object BoolValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean { + return type.isJsBasic + } + + override fun generate(description: JsMethodDescription, type: JsClassId): Sequence> = + sequence { + yield(Seed.Known(Bool.TRUE()) { + JsPrimitiveModel(true) + }) + yield(Seed.Known(Bool.FALSE()) { + JsPrimitiveModel(false) + }) + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/MapValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/MapValueProvider.kt new file mode 100644 index 0000000000..79122fa6b5 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/MapValueProvider.kt @@ -0,0 +1,58 @@ +package fuzzer.providers + + +import framework.api.js.JsClassId +import framework.api.js.JsMethodId +import framework.api.js.util.isJsMap +import framework.api.js.util.jsUndefinedClassId +import fuzzer.JsIdProvider +import fuzzer.JsMethodDescription +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.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +object MapValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean = type.isJsMap + + override fun generate( + description: JsMethodDescription, + type: JsClassId + ) = sequence> { + yield( + Seed.Collection( + construct = Routine.Collection { + UtAssembleModel( + id = JsIdProvider.createId(), + classId = type, + modelName = "", + instantiationCall = UtExecutableCallModel( + null, + ConstructorId(type, emptyList()), + emptyList() + ), + modificationsChainProvider = { mutableListOf() } + ) + }, + modify = Routine.ForEach(listOf(jsUndefinedClassId, jsUndefinedClassId)) { self, _, values -> + val model = self as UtAssembleModel + model.modificationsChain as MutableList += + UtExecutableCallModel( + model, + JsMethodId( + classId = type, + name = "set", + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = listOf(jsUndefinedClassId, jsUndefinedClassId) + ), + values + ) + } + ) + ) + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/NumberValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/NumberValueProvider.kt new file mode 100644 index 0000000000..b82f06bdfd --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/NumberValueProvider.kt @@ -0,0 +1,46 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.isJsBasic +import fuzzer.JsFuzzedContext.EQ +import fuzzer.JsFuzzedContext.GE +import fuzzer.JsFuzzedContext.GT +import fuzzer.JsFuzzedContext.LE +import fuzzer.JsFuzzedContext.LT +import fuzzer.JsMethodDescription +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.seeds.DefaultFloatBound +import org.utbot.fuzzing.seeds.IEEE754Value + +object NumberValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean { + return type.isJsBasic + } + + override fun generate(description: JsMethodDescription, type: JsClassId): Sequence> = + sequence { + description.concreteValues.forEach { (_, v, c) -> + if (v is Double) { + val balance = when (c) { + EQ, LE, GT -> 1 + LT, GE -> -1 + else -> 0 + } + + yield(Seed.Known(IEEE754Value.fromValue(v)) { known -> + JsPrimitiveModel(known.toDouble() + balance) + }) + } + } + DefaultFloatBound.values().forEach { bound -> + // All numbers in JavaScript are like Double in Java/Kotlin + yield(Seed.Known(bound(52, 11)) { known -> + JsPrimitiveModel(known.toDouble()) + }) + } + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/ObjectValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/ObjectValueProvider.kt new file mode 100644 index 0000000000..7370654808 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/ObjectValueProvider.kt @@ -0,0 +1,58 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.JsConstructorId +import framework.api.js.util.isClass +import fuzzer.JsIdProvider +import fuzzer.JsMethodDescription +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.UtNullModel + + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.utils.hex + +class ObjectValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean { + return type.isClass + } + + override fun generate( + description: JsMethodDescription, + type: JsClassId + ) = sequence { + val constructor = type.constructor ?: JsConstructorId(type, emptyList()) + yield(createValue(type, constructor)) + } + + private fun createValue( + classId: JsClassId, + constructorId: JsConstructorId + ): Seed.Recursive { + return Seed.Recursive( + construct = Routine.Create(constructorId.parameters) { values -> + val id = JsIdProvider.createId() + UtAssembleModel( + id = id, + classId = classId, + modelName = "${constructorId.classId.name}${constructorId.parameters}#" + id.hex(), + instantiationCall = UtExecutableCallModel( + null, + constructorId, + values + ), + modificationsChainProvider = { mutableListOf() } + ) + }, + modify = emptySequence(), + empty = Routine.Empty { + UtNullModel(classId) + } + ) + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/SetValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/SetValueProvider.kt new file mode 100644 index 0000000000..6991518c9f --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/SetValueProvider.kt @@ -0,0 +1,57 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.JsMethodId +import framework.api.js.util.isJsSet +import framework.api.js.util.jsUndefinedClassId +import fuzzer.JsIdProvider +import fuzzer.JsMethodDescription +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.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +object SetValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean = type.isJsSet + + override fun generate( + description: JsMethodDescription, + type: JsClassId + ) = sequence> { + yield( + Seed.Collection( + construct = Routine.Collection { + UtAssembleModel( + id = JsIdProvider.createId(), + classId = type, + modelName = "", + instantiationCall = UtExecutableCallModel( + null, + ConstructorId(type, emptyList()), + emptyList() + ), + modificationsChainProvider = { mutableListOf() } + ) + }, + modify = Routine.ForEach(listOf(jsUndefinedClassId)) { self, _, values -> + val model = self as UtAssembleModel + model.modificationsChain as MutableList += + UtExecutableCallModel( + model, + JsMethodId( + classId = type, + name = "add", + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = listOf(jsUndefinedClassId) + ), + values + ) + } + ) + ) + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/StringValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/StringValueProvider.kt new file mode 100644 index 0000000000..a885c9b92c --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/StringValueProvider.kt @@ -0,0 +1,34 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.isJsBasic +import framework.api.js.util.jsStringClassId +import fuzzer.JsMethodDescription +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.seeds.StringValue + +object StringValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean { + return type.isJsBasic + } + + override fun generate( + description: JsMethodDescription, + type: JsClassId + ): Sequence> = sequence { + val constants = description.concreteValues.asSequence() + .filter { it.classId == jsStringClassId } + val values = constants + .mapNotNull { it.value as? String } + + sequenceOf("", "abc", "\n\t\n") + values.forEach { value -> + yield(Seed.Known(StringValue(value)) { known -> + JsPrimitiveModel(known.value) + }) + } + } +} diff --git a/utbot-js/src/main/kotlin/parser/IAstVisitor.kt b/utbot-js/src/main/kotlin/parser/IAstVisitor.kt new file mode 100644 index 0000000000..1d8bcf64a2 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/IAstVisitor.kt @@ -0,0 +1,8 @@ +package parser + +import com.google.javascript.rhino.Node + +interface IAstVisitor { + + fun accept(rootNode: Node) +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt b/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt new file mode 100644 index 0000000000..a22b232cf2 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt @@ -0,0 +1,149 @@ +package parser + +import com.google.javascript.jscomp.Compiler +import com.google.javascript.jscomp.NodeUtil +import com.google.javascript.jscomp.SourceFile +import com.google.javascript.rhino.Node +import java.io.File +import java.nio.file.Paths +import mu.KotlinLogging +import parser.JsParserUtils.getAbstractFunctionName +import parser.JsParserUtils.getClassMethods +import parser.JsParserUtils.getImportSpecAliases +import parser.JsParserUtils.getImportSpecName +import parser.JsParserUtils.getModuleImportSpecsAsList +import parser.JsParserUtils.getModuleImportText +import parser.JsParserUtils.getRequireImportText +import parser.JsParserUtils.isRequireImport +import kotlin.io.path.pathString + +private val logger = KotlinLogging.logger {} + +class JsAstScrapper( + private val parsedFile: Node, + private val basePath: String, +) { + + // Used not to parse the same file multiple times. + private val _parsedFilesCache = mutableMapOf() + private val _filesToInfer: MutableList = mutableListOf(basePath) + val filesToInfer: List + get() = _filesToInfer.toList() + private val _importsMap = mutableMapOf() + val importsMap: Map + get() = _importsMap.toMap() + + init { + _importsMap.apply { + val visitor = Visitor() + visitor.accept(parsedFile) + val res = visitor.importNodes.fold(emptyMap()) { acc, node -> + val currAcc = acc.toList().toTypedArray() + val more = node.importedNodes().toList().toTypedArray() + mapOf(*currAcc, *more) + } + this.putAll(res) + this.toMap() + } + } + + fun findFunction(key: String, file: Node): Node? { + if (_importsMap[key]?.isFunction == true) return _importsMap[key] + val functionVisitor = JsFunctionAstVisitor(key, null) + functionVisitor.accept(file) + return try { + functionVisitor.targetFunctionNode + } catch (e: Exception) { null } + } + + fun findClass(key: String, file: Node): Node? { + if (_importsMap[key]?.isClass == true) return _importsMap[key] + val classVisitor = JsClassAstVisitor(key) + classVisitor.accept(file) + return try { + classVisitor.targetClassNode + } catch (e: Exception) { null } + } + + fun findMethod(classKey: String, methodKey: String, file: Node): Node? { + val classNode = findClass(classKey, file) + return classNode?.getClassMethods()?.find { it.getAbstractFunctionName() == methodKey } + } + + private fun File.parseIfNecessary(): Node = + _parsedFilesCache.getOrPut(this.path) { + _filesToInfer += this.path.replace("\\", "/") + Compiler().parse(SourceFile.fromCode(this.path, readText())) + } + + private fun Node.importedNodes(): Map { + return when { + this.isRequireImport() -> mapOf( + this.parent!!.string to (makePathFromImport(this.getRequireImportText())?.let { + File(it).parseIfNecessary().findEntityInFile(null) + // Workaround for std imports. + } ?: this.firstChild!!.next!!) + ) + this.isImport -> this.processModuleImport() + else -> emptyMap() + } + } + + private fun Node.processModuleImport(): Map { + try { + val pathToFile = makePathFromImport(this.getModuleImportText()) ?: return emptyMap() + val pFile = File(pathToFile).parseIfNecessary() + return when { + NodeUtil.findPreorder(this, { it.isImportSpecs }, { true }) != null -> { + this.getModuleImportSpecsAsList().associate { spec -> + val realName = spec.getImportSpecName() + val aliases = spec.getImportSpecAliases() + aliases to pFile.findEntityInFile(realName) + } + } + NodeUtil.findPreorder(this, { it.isImportStar }, { true }) != null -> { + val aliases = this.getImportSpecAliases() + mapOf(aliases to pFile) + } + // For example: import foo from "bar" + else -> { + val realName = this.getImportSpecName() + mapOf(realName to pFile.findEntityInFile(realName)) + } + } + } catch (e: Exception) { + logger.error { e.toString() } + return emptyMap() + } + } + + private fun makePathFromImport(importText: String): String? { + val relPath = importText + if (importText.endsWith(".js")) "" else ".js" + // If import text doesn't contain "/", then it is NodeJS stdlib import. + if (!relPath.contains("/")) return null + return Paths.get(File(basePath).parent).resolve(Paths.get(relPath)).pathString + } + + private fun Node.findEntityInFile(key: String?): Node { + return key?.let { k -> + findClass(k, this) + ?: findFunction(k, this) + ?: throw ClassNotFoundException("Could not locate entity $k in ${this.sourceFileName}") + } ?: this + } + + private class Visitor: IAstVisitor { + + private val _importNodes = mutableListOf() + + val importNodes: List + get() = _importNodes.toList() + + // TODO: commented for release since features are incomplete + override fun accept(rootNode: Node) { +// NodeUtil.visitPreOrder(rootNode) { node -> +// if (node.isImport || node.isRequireImport()) _importNodes += node +// } + } + } +} diff --git a/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt new file mode 100644 index 0000000000..925d9a22f1 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt @@ -0,0 +1,26 @@ +package parser + +import com.google.javascript.jscomp.NodeUtil +import com.google.javascript.rhino.Node +import parser.JsParserUtils.getClassName + +class JsClassAstVisitor( + private val target: String? +): IAstVisitor { + + lateinit var targetClassNode: Node + lateinit var atLeastSomeClassNode: Node + var classNodesCount = 0 + + override fun accept(rootNode: Node) = + NodeUtil.visitPreOrder(rootNode) { + if (it.isClass) { + classNodesCount++ + atLeastSomeClassNode = it + if (it.getClassName() == target) { + targetClassNode = it + return@visitPreOrder + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt new file mode 100644 index 0000000000..0ded81e3ff --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt @@ -0,0 +1,40 @@ +package parser + +import com.google.javascript.jscomp.NodeUtil +import com.google.javascript.rhino.Node +import parser.JsParserUtils.getAbstractFunctionName +import parser.JsParserUtils.getClassName + +class JsFunctionAstVisitor( + private val target: String, + private val className: String? +): IAstVisitor { + + lateinit var targetFunctionNode: Node + + override fun accept(rootNode: Node) { + NodeUtil.visitPreOrder(rootNode) { node -> + when { + node.isMemberFunctionDef -> { + val name = node.getAbstractFunctionName() + if ( + name == target && + (node.parent?.parent?.getClassName() + ?: throw IllegalStateException("Method AST node has no parent class node")) == className + ) { + targetFunctionNode = node + return@visitPreOrder + } + } + + node.isFunction -> { + val name = node.getAbstractFunctionName() + if (name == target && className == null && node.parent?.isMemberFunctionDef != true) { + targetFunctionNode = node + return@visitPreOrder + } + } + } + } + } +} diff --git a/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt new file mode 100644 index 0000000000..463edcfb5b --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt @@ -0,0 +1,68 @@ +package parser + + +import com.google.javascript.jscomp.NodeUtil +import com.google.javascript.rhino.Node +import framework.api.js.util.jsBooleanClassId +import framework.api.js.util.jsDoubleClassId +import framework.api.js.util.jsStringClassId +import fuzzer.JsFuzzedConcreteValue +import fuzzer.JsFuzzedContext +import parser.JsParserUtils.getAnyValue +import parser.JsParserUtils.getBinaryExprLeftOperand +import parser.JsParserUtils.getBinaryExprRightOperand +import parser.JsParserUtils.toFuzzedContextComparisonOrNull + +class JsFuzzerAstVisitor : IAstVisitor { + + private var lastFuzzedOpGlobal: JsFuzzedContext = JsFuzzedContext.Unknown + val fuzzedConcreteValues = mutableSetOf() + + override fun accept(rootNode: Node) { + NodeUtil.visitPreOrder(rootNode) { node -> + val currentFuzzedOp = node.toFuzzedContextComparisonOrNull() + when { + node.isCase -> validateNode(node.firstChild?.getAnyValue(), JsFuzzedContext.NE) + node.isCall -> { + validateNode(node.getAnyValue(), JsFuzzedContext.NE) + } + + currentFuzzedOp != null -> { + lastFuzzedOpGlobal = currentFuzzedOp + validateNode(node.getBinaryExprLeftOperand().getAnyValue(), lastFuzzedOpGlobal) + lastFuzzedOpGlobal = lastFuzzedOpGlobal.reverse() + validateNode(node.getBinaryExprRightOperand().getAnyValue(), lastFuzzedOpGlobal) + } + } + } + + } + + private fun validateNode(value: Any?, fuzzedOp: JsFuzzedContext) { + when (value) { + is String -> { + fuzzedConcreteValues.add( + JsFuzzedConcreteValue( + jsStringClassId, + value.toString(), + fuzzedOp + ) + ) + } + + is Boolean -> { + fuzzedConcreteValues.add( + JsFuzzedConcreteValue( + jsBooleanClassId, + value, + fuzzedOp + ) + ) + } + + is Double -> { + fuzzedConcreteValues.add(JsFuzzedConcreteValue(jsDoubleClassId, value, fuzzedOp)) + } + } + } +} diff --git a/utbot-js/src/main/kotlin/parser/JsParserUtils.kt b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt new file mode 100644 index 0000000000..e1d1df2385 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt @@ -0,0 +1,209 @@ +package parser + +import com.google.javascript.jscomp.Compiler +import com.google.javascript.jscomp.NodeUtil +import com.google.javascript.jscomp.SourceFile +import com.google.javascript.rhino.Node +import fuzzer.JsFuzzedContext +import parser.JsParserUtils.getMethodName + +// TODO: make methods more safe by checking the Node method is called on. +// Used for .children() calls. +@Suppress("DEPRECATION") +object JsParserUtils { + + fun runParser(fileText: String): Node = + Compiler().parse(SourceFile.fromCode("jsFile", fileText)) + + // TODO SEVERE: function only works in the same file scope. Add search in exports. + fun searchForClassDecl(className: String?, parsedFile: Node, strict: Boolean = false): Node? { + val visitor = JsClassAstVisitor(className) + visitor.accept(parsedFile) + return try { + visitor.targetClassNode + } catch (e: Exception) { + if (!strict && visitor.classNodesCount == 1) { + visitor.atLeastSomeClassNode + } else null + } + } + + /** + * Called upon node with Class token. + */ + fun Node.getClassName(): String = + this.firstChild?.string ?: throw IllegalStateException("Class AST node has no children") + + /** + * Called upon node with Method token. + */ + private fun Node.getMethodName(): String = this.string + + /** + * Called upon node with Function token. + */ + private fun Node.getFunctionName(): String = + this.firstChild?.string ?: throw IllegalStateException("Function AST node has no children") + + /** + * Called upon node with Parameter token. + */ + fun Node.getParamName(): String = this.string + + /** + * Convenience method. Used as a wrapper for [getFunctionName] and [getMethodName] + * functions when the type of function is unknown. + */ + fun Node.getAbstractFunctionName(): String = when { + this.isMemberFunctionDef -> this.getMethodName() + this.isFunction -> this.getFunctionName() + else -> throw IllegalStateException() + } + + /** + * Called upon node with any kind of literal value token. + */ + fun Node.getAnyValue(): Any? = when { + this.isNumber -> this.double + this.isString -> this.string + this.isTrue -> true + this.isFalse -> false + this.isCall -> { + if (this.firstChild?.isGetProp == true) { + this.firstChild?.next?.getAnyValue() + } else null + } + + else -> null + } + + // For some reason Closure Compiler doesn't contain a built-in method + // to check for some tokens. + /** + * Called upon node with any kind of binary comparison token. + */ + fun Node.toFuzzedContextComparisonOrNull(): JsFuzzedContext? = when { + this.isEQ -> JsFuzzedContext.EQ + this.isNE -> JsFuzzedContext.NE + this.token.name == "LT" -> JsFuzzedContext.LT + this.token.name == "GT" -> JsFuzzedContext.GT + this.token.name == "LE" -> JsFuzzedContext.LE + this.token.name == "GE" -> JsFuzzedContext.GE + this.token.name == "SHEQ" -> JsFuzzedContext.EQ + else -> null + } + + /** + * Called upon node with any kind of binary comparison token. + */ + fun Node.getBinaryExprLeftOperand(): Node = this.getChildAtIndex(0) + + /** + * Called upon node with any kind of binary comparison token. + */ + fun Node.getBinaryExprRightOperand(): Node = this.getChildAtIndex(1) + + /** + * Called upon node with Function token. + */ + private fun Node.getFunctionParams(): List = this.getChildAtIndex(1).children().map { it } + + /** + * Called upon node with Method token. + */ + private fun Node.getMethodParams(): List = + this.firstChild?.getFunctionParams() ?: throw IllegalStateException("Method AST node has no children") + + /** + * Convenience method. Used as a wrapper for [getFunctionParams] and [getMethodParams] + * function when the type of function is unknown. + */ + fun Node.getAbstractFunctionParams(): List = when { + this.isMemberFunctionDef -> getMethodParams() + this.isFunction -> getFunctionParams() + else -> throw IllegalStateException() + } + + /** + * Called upon node with Class token. + */ + fun Node.getClassMethods(): List { + val classMembers = this.children().find { it.isClassMembers } + ?: throw IllegalStateException("Can't extract class members of class ${this.getClassName()}") + return classMembers.children().filter { it.isMemberFunctionDef } + + } + + /** + * Called upon node with Class token. + * + * Returns null if class has no constructor. + */ + fun Node.getConstructor(): Node? { + val classMembers = this.children().find { it.isClassMembers } + ?: throw IllegalStateException("Can't extract methods of class ${this.getClassName()}") + return classMembers.children().find { + it.isMemberFunctionDef && it.getMethodName() == "constructor" + }?.firstChild + } + + /** + * Called upon node with Method token. + */ + fun Node.isStatic(): Boolean = this.isStaticMember + + /** + * Checks if node is "require" JavaScript import. + */ + fun Node.isRequireImport(): Boolean = try { + this.isCall && this.firstChild?.string == "require" + } catch (e: ClassCastException) { + false + } + + /** + * Called upon "require" JavaScript import. + * + * Returns path to imported file as [String]. + */ + fun Node.getRequireImportText(): String = this.firstChild!!.next!!.string + + /** + * Called upon "import" JavaScript import. + * + * Returns path to imported file as [String]. + */ + fun Node.getModuleImportText(): String = this.firstChild!!.next!!.next!!.string + + /** + * Called upon "import" JavaScript import. + * + * Returns imported objects as [List]. + */ + fun Node.getModuleImportSpecsAsList(): List { + val importSpecsNode = NodeUtil.findPreorder(this, { it.isImportSpecs }, { true }) + ?: throw UnsupportedOperationException("Module import doesn't contain \"import_specs\" token as an AST child") + var currNode: Node? = importSpecsNode.firstChild!! + val importSpecsList = mutableListOf() + do { + importSpecsList += currNode!! + currNode = currNode?.next + } while (currNode?.isImportSpec == true) + return importSpecsList + } + + /** + * Called upon IMPORT_SPEC Node. + * + * Returns name of imported object as [String]. + */ + fun Node.getImportSpecName(): String = this.firstChild!!.string + + /** + * Called upon IMPORT_SPEC Node. + * + * Returns import alias as [String]. + */ + fun Node.getImportSpecAliases(): String = this.firstChild!!.next!!.string + +} diff --git a/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt new file mode 100644 index 0000000000..92e32d3b0e --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt @@ -0,0 +1,18 @@ +package parser + +import com.google.javascript.jscomp.NodeUtil +import com.google.javascript.rhino.Node + +class JsToplevelFunctionAstVisitor : IAstVisitor { + + val extractedMethods = mutableListOf() + + + override fun accept(rootNode: Node) { + NodeUtil.visitPreOrder(rootNode) { node -> + when { + node.isFunction && !(node.parent?.isMemberFunctionDef ?: true) -> extractedMethods += node + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/exports/IExportsProvider.kt b/utbot-js/src/main/kotlin/providers/exports/IExportsProvider.kt new file mode 100644 index 0000000000..895b0a8cea --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/exports/IExportsProvider.kt @@ -0,0 +1,25 @@ +package providers.exports + +import service.PackageJson + +interface IExportsProvider { + + val exportsRegex: Regex + + val exportsDelimiter: String + + fun getExportsFrame(exportString: String): String + + val exportsPrefix: String + + val exportsPostfix: String + + fun instrumentationFunExport(funName: String): String + + companion object { + fun providerByPackageJson(packageJson: PackageJson): IExportsProvider = when (packageJson.isModule) { + true -> ModuleExportsProvider() + else -> RequireExportsProvider() + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/exports/ModuleExportsProvider.kt b/utbot-js/src/main/kotlin/providers/exports/ModuleExportsProvider.kt new file mode 100644 index 0000000000..77fae0fc1e --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/exports/ModuleExportsProvider.kt @@ -0,0 +1,16 @@ +package providers.exports + +class ModuleExportsProvider : IExportsProvider { + + override val exportsDelimiter: String = "," + + override val exportsPostfix: String = "}\n" + + override val exportsPrefix: String = "\nexport {" + + override val exportsRegex: Regex = Regex("(.*)") + + override fun getExportsFrame(exportString: String): String = exportString + + override fun instrumentationFunExport(funName: String): String = "\nexport {$funName}" +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/exports/RequireExportsProvider.kt b/utbot-js/src/main/kotlin/providers/exports/RequireExportsProvider.kt new file mode 100644 index 0000000000..644744bd66 --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/exports/RequireExportsProvider.kt @@ -0,0 +1,16 @@ +package providers.exports + +class RequireExportsProvider : IExportsProvider { + + override val exportsDelimiter: String = "\n" + + override val exportsPostfix: String = "\n" + + override val exportsPrefix: String = "\n" + + override val exportsRegex: Regex = Regex("exports[.](.*) =") + + override fun getExportsFrame(exportString: String): String = "exports.$exportString = $exportString" + + override fun instrumentationFunExport(funName: String): String = "\nexports.$funName = $funName" +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/imports/IImportsProvider.kt b/utbot-js/src/main/kotlin/providers/imports/IImportsProvider.kt new file mode 100644 index 0000000000..c2b821a904 --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/imports/IImportsProvider.kt @@ -0,0 +1,18 @@ +package providers.imports + +import service.PackageJson +import service.ServiceContext + +interface IImportsProvider { + + val ternScriptImports: String + + val tempFileImports: String + + companion object { + fun providerByPackageJson(packageJson: PackageJson, context: ServiceContext): IImportsProvider = when (packageJson.isModule) { + true -> ModuleImportsProvider(context) + else -> RequireImportsProvider(context) + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt b/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt new file mode 100644 index 0000000000..7d1aa55c01 --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt @@ -0,0 +1,22 @@ +package providers.imports + +import service.ContextOwner +import service.ServiceContext +import settings.JsTestGenerationSettings.fileUnderTestAliases + +class ModuleImportsProvider(context: ServiceContext) : IImportsProvider, ContextOwner by context { + + override val ternScriptImports: String = buildString { + appendLine("import * as tern from \"tern/lib/tern.js\"") + appendLine("import * as condense from \"tern/lib/condense.js\"") + appendLine("import * as util from \"tern/test/util.js\"") + appendLine("import * as fs from \"fs\"") + appendLine("import * as path from \"path\"") + } + + override val tempFileImports: String = buildString { + val importFileUnderTest = "./instr/${filePathToInference.first().substringAfterLast("/")}" + appendLine("import * as $fileUnderTestAliases from \"$importFileUnderTest\"") + appendLine("import * as fs from \"fs\"") + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/imports/RequireImportsProvider.kt b/utbot-js/src/main/kotlin/providers/imports/RequireImportsProvider.kt new file mode 100644 index 0000000000..d653b2b047 --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/imports/RequireImportsProvider.kt @@ -0,0 +1,23 @@ +package providers.imports + +import service.ContextOwner +import service.ServiceContext +import settings.JsTestGenerationSettings.fileUnderTestAliases + +class RequireImportsProvider(context: ServiceContext) : IImportsProvider, ContextOwner by context { + + override val ternScriptImports: String = buildString { + appendLine("const tern = require(\"tern/lib/tern\")") + appendLine("const condense = require(\"tern/lib/condense.js\")") + appendLine("const util = require(\"tern/test/util.js\")") + appendLine("const fs = require(\"fs\")") + appendLine("const path = require(\"path\")") + } + + override val tempFileImports: String = buildString { + val importFileUnderTest = "instr/${filePathToInference.first().substringAfterLast("/")}" + appendLine("const $fileUnderTestAliases = require(\"./$importFileUnderTest\")") + appendLine("const fs = require(\"fs\")\n") + } + +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/InstrumentationService.kt b/utbot-js/src/main/kotlin/service/InstrumentationService.kt new file mode 100644 index 0000000000..71031359dd --- /dev/null +++ b/utbot-js/src/main/kotlin/service/InstrumentationService.kt @@ -0,0 +1,194 @@ +package service + +import com.google.javascript.jscomp.CodePrinter +import com.google.javascript.jscomp.NodeUtil +import com.google.javascript.rhino.Node +import java.io.File +import java.nio.file.Paths +import org.apache.commons.io.FileUtils +import parser.JsFunctionAstVisitor +import parser.JsParserUtils.getAnyValue +import parser.JsParserUtils.getModuleImportText +import parser.JsParserUtils.getRequireImportText +import parser.JsParserUtils.isRequireImport +import parser.JsParserUtils.runParser +import providers.exports.IExportsProvider +import utils.JsCmdExec +import utils.PathResolver.getRelativePath +import kotlin.io.path.pathString +import kotlin.math.roundToInt + +class InstrumentationService(context: ServiceContext, private val funcDeclOffset: Pair) : + ContextOwner by context { + + private val destinationFolderPath = "${projectPath}/${utbotDir}/instr" + private val instrumentedFilePath = "$destinationFolderPath/${filePathToInference.first().substringAfterLast("/")}" + private lateinit var parsedInstrFile: Node + lateinit var covFunName: String + + val allStatements: Set + get() = getStatementMapKeys() + + private data class Location( + val start: Pair, + val end: Pair + ) + + + /* + Extension functions below are used to parse instrumented file's block that looks like this: + + fnMap: { + "0": { + name: "testFunction", + decl: {start: {line: 4, column: 9}, end: {line: 4, column: 12}}, + loc: {start: {line: 4, column: 20}, end: {line: 9, column: 1}}, + line: 4 + } + } + + Node.getObjectFirstKey() returns "0" Node if called on "fnMap" Node. + Node.getObjectField("loc") returns loc Node if called on "0" Node. + Node.getObjectValue() returns 4 if called on any "line" Node. + Node.getObjectLocation("decl") returns Location class ((4, 9), (4, 12)) if called on "0" Node. + */ + private fun Node.getObjectFirstKey(): Node = this.firstFirstChild + ?: throw IllegalStateException("Node doesn't have child of child") + + private fun Node.getObjectField(fieldName: String): Node? { + var fieldNode: Node? = this.getObjectFirstKey() + do { + if (fieldNode?.string == fieldName) return fieldNode + fieldNode = fieldNode?.next + } while (fieldNode != null) + return null + } + + private fun Node.getObjectValue(): Any = this.firstChild?.getAnyValue() + ?: throw IllegalStateException("Can't get Node's simple value") + + private fun Node.getObjectLocation(locKey: String?): Location { + val generalField = locKey?.let { this.getObjectField(it) } ?: this + val startField = generalField.getObjectFirstKey() + val endField = startField.next!! + return Location( + startField.getLineValue() to startField.getColumnValue(), + endField.getLineValue() to endField.getColumnValue() + ) + } + + private fun Node.getLineValue(): Int = this.getObjectFirstKey().getObjectValue() + .toString() + .toFloat() + .roundToInt() + + private fun Node.getColumnValue(): Int = this.getObjectFirstKey().next!! + .getObjectValue() + .toString() + .toFloat() + .roundToInt() + + private fun Node.findAndIterateOver(key: String, func: (Node?) -> Unit) { + NodeUtil.visitPreOrder(this) { node -> + if (node.isStringKey && node.string == key) { + var currKey: Node? = node.firstChild!!.firstChild!! + do { + func(currKey) + currKey = currKey?.next + } while (currKey != null) + return@visitPreOrder + } + } + } + + private fun getStatementMapKeys() = buildSet { + val funcVisitor = JsFunctionAstVisitor(covFunName, null) + funcVisitor.accept(parsedInstrFile) + val funcNode = funcVisitor.targetFunctionNode + val funcLocation = getFuncLocation(funcNode) + funcNode.findAndIterateOver("statementMap") { currKey -> + operator fun Pair.compareTo(other: Pair): Int = + when { + this.first < other.first || (this.first == other.first && this.second <= other.second) -> -1 + this.first > other.first || (this.first == other.first && this.second >= other.second) -> 1 + else -> 0 + } + + val stmtLocation = currKey!!.getObjectLocation(null) + if (funcLocation.start < stmtLocation.start && funcLocation.end > stmtLocation.end) + add(currKey.string.toInt()) + } + } + + private fun getFuncLocation(covFuncNode: Node): Location { + var result = Location(0 to 0, Int.MAX_VALUE to Int.MAX_VALUE) + covFuncNode.findAndIterateOver("fnMap") { currKey -> + val declLocation = currKey!!.getObjectLocation("decl") + if (funcDeclOffset == declLocation.start) { + result = currKey.getObjectLocation("loc") + return@findAndIterateOver + } + } + return result + } + + fun instrument() { + val fileName = filePathToInference.first().substringAfterLast("/") + + JsCmdExec.runCommand( + cmd = arrayOf(settings.pathToNYC, "instrument", fileName, destinationFolderPath), + dir = filePathToInference.first().substringBeforeLast("/"), + shouldWait = true, + timeout = settings.timeout, + ) + val instrumentedFileText = File(instrumentedFilePath).readText() + parsedInstrFile = runParser(instrumentedFileText) + val covFunRegex = Regex("function (cov_.*)\\(\\).*") + val funName = covFunRegex.find(instrumentedFileText.takeWhile { it != '{' })?.groups?.get(1)?.value + ?: throw IllegalStateException("") + val fixedFileText = fixImportsInInstrumentedFile() + + IExportsProvider.providerByPackageJson(packageJson).instrumentationFunExport(funName) + File(instrumentedFilePath).writeTextAndUpdate(fixedFileText) + + covFunName = funName + } + + private fun File.writeTextAndUpdate(newText: String) { + this.writeText(newText) + parsedInstrFile = runParser(File(instrumentedFilePath).readText()) + } + + private fun fixImportsInInstrumentedFile(): String { + // nyc poorly handles imports paths in file to instrument. Manual fix required. + NodeUtil.visitPreOrder(parsedInstrFile) { node -> + when { + node.isRequireImport() -> { + val currString = node.getRequireImportText() + val relPath = Paths.get( + getRelativePath( + "${projectPath}/${utbotDir}/instr", + File(filePathToInference.first()).parent + ) + ).resolve(currString).pathString.replace("\\", "/") + node.firstChild!!.next!!.string = relPath + } + node.isImport -> { + val currString = node.getModuleImportText() + val relPath = Paths.get( + getRelativePath( + "${projectPath}/${utbotDir}/instr", + File(filePathToInference.first()).parent + ) + ).resolve(currString).pathString.replace("\\", "/") + node.firstChild!!.next!!.next!!.string = relPath + } + } + } + return CodePrinter.Builder(parsedInstrFile).build() + } + + fun removeTempFiles() { + FileUtils.deleteDirectory(File("$projectPath/$utbotDir/instr")) + } + +} diff --git a/utbot-js/src/main/kotlin/service/PackageJsonService.kt b/utbot-js/src/main/kotlin/service/PackageJsonService.kt new file mode 100644 index 0000000000..586a0dafe9 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/PackageJsonService.kt @@ -0,0 +1,42 @@ +package service + +import java.io.File +import java.io.FilenameFilter +import org.json.JSONObject + +data class PackageJson( + val isModule: Boolean, + val deps: Set +) { + companion object { + val defaultConfig = PackageJson(false, emptySet()) + } +} + +class PackageJsonService( + private val filePathToInference: String, + private val projectDir: File +) { + + fun findClosestConfig(): PackageJson { + var currDir = File(filePathToInference) + do { + currDir = currDir.parentFile + val matchingFiles: Array = currDir.listFiles( + FilenameFilter { _, name -> + return@FilenameFilter name == "package.json" + } + ) ?: throw IllegalStateException("Error occurred while scanning file system") + if (matchingFiles.isNotEmpty()) return parseConfig(matchingFiles.first()) + } while (currDir != projectDir) + return PackageJson.defaultConfig + } + + private fun parseConfig(configFile: File): PackageJson { + val configAsJson = JSONObject(configFile.readText()) + return PackageJson( + isModule = configAsJson.optString("type") == "module", + deps = configAsJson.optJSONObject("dependencies")?.keySet() ?: emptySet() + ) + } +} diff --git a/utbot-js/src/main/kotlin/service/ServiceContext.kt b/utbot-js/src/main/kotlin/service/ServiceContext.kt new file mode 100644 index 0000000000..d30ccf1fea --- /dev/null +++ b/utbot-js/src/main/kotlin/service/ServiceContext.kt @@ -0,0 +1,22 @@ +package service + +import com.google.javascript.rhino.Node +import settings.JsDynamicSettings + +class ServiceContext( + override val utbotDir: String, + override val projectPath: String, + override val filePathToInference: List, + override val parsedFile: Node, + override val settings: JsDynamicSettings, + override var packageJson: PackageJson = PackageJson.defaultConfig +) : ContextOwner + +interface ContextOwner { + val utbotDir: String + val projectPath: String + val filePathToInference: List + val parsedFile: Node + val settings: JsDynamicSettings + var packageJson: PackageJson +} diff --git a/utbot-js/src/main/kotlin/service/TernService.kt b/utbot-js/src/main/kotlin/service/TernService.kt new file mode 100644 index 0000000000..fd215752a4 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/TernService.kt @@ -0,0 +1,190 @@ +package service + +import com.google.javascript.rhino.Node +import framework.api.js.JsClassId +import framework.api.js.JsMultipleClassId +import framework.api.js.util.jsUndefinedClassId +import java.io.File +import org.json.JSONException +import org.json.JSONObject +import parser.JsParserUtils +import parser.JsParserUtils.getAbstractFunctionName +import parser.JsParserUtils.getAbstractFunctionParams +import parser.JsParserUtils.getClassName +import parser.JsParserUtils.getConstructor +import providers.imports.IImportsProvider +import utils.JsCmdExec +import utils.constructClass +import utils.data.MethodTypes + +/** + * Installs and sets up scripts for running Tern.js type guesser. + */ +class TernService(context: ServiceContext) : ContextOwner by context { + + private val importProvider = IImportsProvider.providerByPackageJson(packageJson, context) + + private fun ternScriptCode() = """ +${generateImportsSection()} + +var condenseDir = ""; + +function runTest(options) { + + var server = new tern.Server({ + projectDir: util.resolve(condenseDir), + defs: [util.ecmascript], + plugins: options.plugins, + getFile: function(name) { + return fs.readFileSync(path.resolve(condenseDir, name), "utf8"); + } + }); + options.load.forEach(function(file) { + server.addFile(file) + }); + server.flush(function() { + var origins = options.include || options.load; + var condensed = condense.condense(origins, null, {sortOutput: true}); + var out = JSON.stringify(condensed, null, 2); + console.log(out) + }); +} + +function test(options) { + options = {load: options}; + runTest(options); +} + +test(["${filePathToInference.joinToString(separator = "\", \"")}"]) + """ + + init { + with(context) { + setupTernEnv("$projectPath/$utbotDir") + runTypeInferencer() + } + } + + private lateinit var json: JSONObject + + private fun generateImportsSection(): String = importProvider.ternScriptImports + + private fun setupTernEnv(path: String) { + File(path).mkdirs() + val ternScriptFile = File("$path/ternScript.js") + ternScriptFile.writeText(ternScriptCode()) + } + + private fun runTypeInferencer() { + val (inputText, _) = JsCmdExec.runCommand( + dir = "$projectPath/$utbotDir/", + shouldWait = true, + timeout = 20, + cmd = arrayOf("\"${settings.pathToNode}\"", "\"${projectPath}/$utbotDir/ternScript.js\""), + ) + json = try { + JSONObject(inputText.replaceAfterLast("}", "")) + } catch (_: Throwable) { + JSONObject() + } + } + + fun processConstructor(classNode: Node): List { + return try { + val classJson = json.getJSONObject(classNode.getClassName()) + val constructorFunc = classJson.getString("!type") + .filterNot { setOf(' ', '+').contains(it) } + extractParameters(constructorFunc) + } catch (e: JSONException) { + classNode.getConstructor()?.getAbstractFunctionParams()?.map { jsUndefinedClassId } ?: emptyList() + } + } + + private fun extractParameters(line: String): List { + val parametersRegex = Regex("fn[(](.+)[)]") + return parametersRegex.find(line)?.groups?.get(1)?.let { matchResult -> + val value = matchResult.value + val paramGroupList = Regex("(\\w+:\\[\\w+(,\\w+)*]|\\w+:\\w+)|\\w+:\\?").findAll(value).toList() + paramGroupList.map { paramGroup -> + val paramReg = Regex("\\w*:(.*)") + try { + val param = paramGroup.groups[0]!!.value + makeClassId( + paramReg.find(param)?.groups?.get(1)?.value + ?: throw IllegalStateException() + ) + } catch (t: Throwable) { + jsUndefinedClassId + } + } + } ?: emptyList() + } + + private fun extractReturnType(line: String): JsClassId { + val returnTypeRegex = Regex("->(.*)") + return returnTypeRegex.find(line)?.groups?.get(1)?.let { matchResult -> + val value = matchResult.value + try { + makeClassId(value) + } catch (t: Throwable) { + jsUndefinedClassId + } + } ?: jsUndefinedClassId + } + + fun processMethod(className: String?, funcNode: Node, isToplevel: Boolean = false): MethodTypes { + // Js doesn't support nested classes, so if the function is not top-level, then we can check for only one parent class. + try { + var scope = className?.let { + if (!isToplevel) json.getJSONObject(it) else json + } ?: json + try { + scope.getJSONObject(funcNode.getAbstractFunctionName()) + } catch (e: JSONException) { + scope = scope.getJSONObject("prototype") + } + val methodJson = scope.getJSONObject(funcNode.getAbstractFunctionName()) + val typesString = methodJson.getString("!type") + .filterNot { setOf(' ', '+').contains(it) } + val parametersList = lazy { extractParameters(typesString) } + val returnType = lazy { extractReturnType(typesString) } + + return MethodTypes(parametersList, returnType) + } catch (e: Exception) { + return MethodTypes( + lazy { funcNode.getAbstractFunctionParams().map { jsUndefinedClassId } }, + lazy { jsUndefinedClassId } + ) + } + } + + private fun makeClassId(name: String): JsClassId { + val classId = when { + name == "?" || name.toIntOrNull() != null || name.contains('!') -> jsUndefinedClassId + Regex("\\[(.*)]").matches(name) -> { + val arrType = Regex("\\[(.*)]").find(name)?.groups?.get(1)?.value?.substringBefore(",") + ?: throw IllegalStateException() + JsClassId( + jsName = "Array", + elementClassId = makeClassId(arrType) + ) + } + + name.contains('|') -> JsMultipleClassId(name) + else -> JsClassId(name) + } + + return try { + val classNode = JsParserUtils.searchForClassDecl( + className = name, + parsedFile = parsedFile, + strict = true, + ) + classNode?.let { + JsClassId(name).constructClass(this, it) + } ?: classId + } catch (e: Exception) { + classId + } + } +} diff --git a/utbot-js/src/main/kotlin/service/coverage/BasicCoverageService.kt b/utbot-js/src/main/kotlin/service/coverage/BasicCoverageService.kt new file mode 100644 index 0000000000..9b774d8154 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/coverage/BasicCoverageService.kt @@ -0,0 +1,50 @@ +package service.coverage + +import java.io.File +import mu.KotlinLogging +import org.json.JSONObject +import org.utbot.framework.plugin.api.TimeoutException +import service.ServiceContext +import settings.JsTestGenerationSettings.tempFileName +import utils.JsCmdExec +import utils.data.ResultData + +private val logger = KotlinLogging.logger {} + +class BasicCoverageService( + context: ServiceContext, + baseCoverage: Map, + private val scriptTexts: List, +) : CoverageService(context, baseCoverage, scriptTexts) { + + override fun generateCoverageReport() { + scriptTexts.indices.forEach { index -> + try { + val (_, errorText) = JsCmdExec.runCommand( + cmd = arrayOf("\"${settings.pathToNode}\"", "\"$utbotDirPath/$tempFileName$index.js\""), + dir = projectPath, + shouldWait = true, + timeout = settings.timeout, + ) + val resFile = File("$utbotDirPath/$tempFileName$index.json") + val rawResult = resFile.readText() + resFile.delete() + val json = JSONObject(rawResult) + coverageList.add(index to json.getJSONObject("s")) + val resultData = ResultData(json) + _resultList.add(resultData) + if (errorText.isNotEmpty()) { + logger.error { errorText } + } + } catch (e: TimeoutException) { + val resultData = ResultData( + rawString = "Timeout", + index = index, + isError = true, + ) + coverageList.add(index to JSONObject()) + _resultList.add(resultData) + } + } + } +} diff --git a/utbot-js/src/main/kotlin/service/coverage/CoverageMode.kt b/utbot-js/src/main/kotlin/service/coverage/CoverageMode.kt new file mode 100644 index 0000000000..5fee242ac0 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/coverage/CoverageMode.kt @@ -0,0 +1,6 @@ +package service.coverage + +enum class CoverageMode { + FAST, + BASIC +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/coverage/CoverageService.kt b/utbot-js/src/main/kotlin/service/coverage/CoverageService.kt new file mode 100644 index 0000000000..b51106e8cf --- /dev/null +++ b/utbot-js/src/main/kotlin/service/coverage/CoverageService.kt @@ -0,0 +1,110 @@ +package service.coverage + +import java.io.File +import org.json.JSONException +import org.json.JSONObject +import service.ContextOwner +import service.ServiceContext +import settings.JsTestGenerationSettings +import utils.JsCmdExec +import utils.data.CoverageData +import utils.data.ResultData + +abstract class CoverageService( + context: ServiceContext, + private val baseCoverage: Map, + private val scriptTexts: List, +) : ContextOwner by context { + + private val _utbotDirPath = lazy { "${projectPath}/${utbotDir}" } + protected val utbotDirPath: String + get() = _utbotDirPath.value + protected val coverageList = mutableListOf>() + protected val _resultList = mutableListOf() + val resultList: List + get() = _resultList.toList() + + companion object { + + private fun createTempScript(path: String, scriptText: String) { + val file = File(path) + file.writeText(scriptText) + file.createNewFile() + } + + fun getBaseCoverage(context: ServiceContext, baseCoverageScriptText: String): Map { + with(context) { + val utbotDirPath = "${projectPath}/${utbotDir}" + createTempScript( + path = "$utbotDirPath/${JsTestGenerationSettings.tempFileName}Base.js", + scriptText = baseCoverageScriptText + ) + JsCmdExec.runCommand( + cmd = arrayOf( + "\"${settings.pathToNode}\"", + "\"$utbotDirPath/${JsTestGenerationSettings.tempFileName}Base.js\"" + ), + dir = projectPath, + shouldWait = true, + timeout = settings.timeout, + ) + return JSONObject(File("$utbotDirPath/${JsTestGenerationSettings.tempFileName}Base.json").readText()) + .getJSONObject("s").let { obj -> + obj.keySet().associate { key -> + key.toInt() to obj.getInt(key) + } + } + } + } + } + + init { + generateTempFiles() + } + + private fun generateTempFiles() { + scriptTexts.forEachIndexed { index, scriptText -> + val tempScriptPath = "$utbotDirPath/${JsTestGenerationSettings.tempFileName}$index.js" + createTempScript( + path = tempScriptPath, + scriptText = scriptText + ) + } + } + + fun getCoveredLines(): List { + try { + // TODO: sort by coverage size desc + return coverageList + .map { (_, obj) -> + val map = obj.keySet().associate { key -> + val intKey = key.toInt() + intKey to (obj.getInt(key) - baseCoverage.getOrDefault(intKey, 0)) + } + CoverageData(map.mapNotNull { entry -> + entry.key.takeIf { entry.value > 0 } + }.toSet()) + } + } catch (e: JSONException) { + throw Exception("Could not get coverage of test cases!") + } finally { + removeTempFiles() + } + } + + abstract fun generateCoverageReport() + + private fun createTempScript(path: String, scriptText: String) { + val file = File(path) + file.writeText(scriptText) + file.createNewFile() + } + + private fun removeTempFiles() { + File("$utbotDirPath/${JsTestGenerationSettings.tempFileName}Base.js").delete() + File("$utbotDirPath/${JsTestGenerationSettings.tempFileName}Base.json").delete() + for (index in scriptTexts.indices) { + File("$utbotDirPath/${JsTestGenerationSettings.tempFileName}$index.js").delete() + } + } +} diff --git a/utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt b/utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt new file mode 100644 index 0000000000..e88b283fcb --- /dev/null +++ b/utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt @@ -0,0 +1,303 @@ +package service.coverage + +import framework.api.js.JsClassId +import framework.api.js.JsMethodId +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.isExportable +import framework.api.js.util.isUndefined +import fuzzer.JsMethodDescription +import java.util.regex.Pattern +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.UtModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.util.isStatic +import providers.imports.IImportsProvider +import service.ContextOwner +import service.InstrumentationService +import service.ServiceContext +import settings.JsTestGenerationSettings +import settings.JsTestGenerationSettings.tempFileName +import utils.data.CoverageData +import utils.data.ResultData + +class CoverageServiceProvider( + private val context: ServiceContext, + private val instrumentationService: InstrumentationService, + private val mode: CoverageMode, + private val description: JsMethodDescription +) : ContextOwner by context { + + private val imports = IImportsProvider.providerByPackageJson(packageJson, context).tempFileImports + + private val filePredicate = """ +function check_value(value, json) { + if (value === Infinity) { + json.is_inf = true + json.spec_sign = 1 + } + if (value === -Infinity) { + json.is_inf = true + json.spec_sign = -1 + } + if (Number.isNaN(value)) { + json.is_nan = true + } +} + +function getType(value) { + if (value instanceof Set) { + return "Set" + } else if (value instanceof Map) { + return "Map" + } else if (value instanceof Array) { + return "Array" + } + return typeof value +} + +function getRes(value, type) { + if (type === "Set" || type === "Array") { + return Array.from(value, (value) => { + let json = {} + json.type = getType(value) + check_value(value, json) + json.result = getRes(value, json.type) + json.index = 0 + return json + }) + } else if (type === "Map") { + return Array.from(value, ([name, value]) => { + let json = {} + json.type = getType(value) + check_value(value, json) + json.result = getRes(value, json.type) + json.index = 0 + return {name, json} + }) + } + return value +} + """ + + private val baseCoverage: Map + + init { + val temp = makeScriptForBaseCoverage( + instrumentationService.covFunName, + "${projectPath}/${utbotDir}/${tempFileName}Base.json" + ) + baseCoverage = CoverageService.getBaseCoverage( + context, + temp + ) + } + + fun get( + fuzzedValues: List>, + execId: JsMethodId, + ): Pair, List> { + return when (mode) { + CoverageMode.FAST -> runFastCoverageAnalysis( + fuzzedValues, + execId + ) + + CoverageMode.BASIC -> runBasicCoverageAnalysis( + fuzzedValues, + execId + ) + } + } + + private fun runBasicCoverageAnalysis( + fuzzedValues: List>, + execId: JsMethodId, + ): Pair, List> { + val covFunName = instrumentationService.covFunName + val tempScriptTexts = fuzzedValues.indices.map { + imports + "$filePredicate\n\n" + makeStringForRunJs( + fuzzedValue = fuzzedValues[it], + method = execId, + containingClass = if (!execId.classId.isUndefined) execId.classId.name else null, + covFunName = covFunName, + index = it, + resFilePath = "${projectPath}/${utbotDir}/$tempFileName", + ) + } + val coverageService = BasicCoverageService( + context = context, + baseCoverage = baseCoverage, + scriptTexts = tempScriptTexts, + ) + coverageService.generateCoverageReport() + return coverageService.getCoveredLines() to coverageService.resultList + } + + private fun runFastCoverageAnalysis( + fuzzedValues: List>, + execId: JsMethodId, + ): Pair, List> { + val covFunName = instrumentationService.covFunName + val tempScriptTexts = imports + "$filePredicate\n\n" + fuzzedValues.indices.joinToString("\n\n") { + makeStringForRunJs( + fuzzedValue = fuzzedValues[it], + method = execId, + containingClass = if (!execId.classId.isUndefined) execId.classId.name else null, + covFunName = covFunName, + index = it, + resFilePath = "${projectPath}/${utbotDir}/$tempFileName", + ) + } + val coverageService = FastCoverageService( + context = context, + baseCoverage = baseCoverage, + scriptTexts = listOf(tempScriptTexts), + testCaseIndices = fuzzedValues.indices, + ) + coverageService.generateCoverageReport() + return coverageService.getCoveredLines() to coverageService.resultList + } + + private fun makeScriptForBaseCoverage(covFunName: String, resFilePath: String): String { + return """ +$imports + +let json = {} +json.s = ${JsTestGenerationSettings.fileUnderTestAliases}.$covFunName().s +fs.writeFileSync("$resFilePath", JSON.stringify(json)) + """ + } + + private fun makeStringForRunJs( + fuzzedValue: List, + method: JsMethodId, + containingClass: String?, + covFunName: String, + index: Int, + resFilePath: String, + ): String { + val callString = makeCallFunctionString(fuzzedValue, method, containingClass, index) + return """ +let json$index = {} +json$index.is_inf = false +json$index.is_nan = false +json$index.is_error = false +json$index.spec_sign = 1 +let res$index +try { + $callString + check_value(res$index, json$index) +} catch(e) { + res$index = e.message + json$index.is_error = true +} +json$index.type = getType(res$index) +json$index.result = getRes(res$index, json$index.type) +json$index.index = $index +json$index.s = ${JsTestGenerationSettings.fileUnderTestAliases}.$covFunName().s + +fs.writeFileSync("$resFilePath$index.json", JSON.stringify(json$index)) + """ + } + + private fun makeCallFunctionString( + fuzzedValue: List, + method: JsMethodId, + containingClass: String?, + index: Int + ): String { + val paramsInit = initParams(fuzzedValue) + val actualParams = description.thisInstance?.let { fuzzedValue.drop(1) } ?: fuzzedValue + val initClass = containingClass?.let { + if (!method.isStatic) { + description.thisInstance?.let { fuzzedValue[0].initModelAsString() } + ?: "new ${JsTestGenerationSettings.fileUnderTestAliases}.${it}()" + } else "${JsTestGenerationSettings.fileUnderTestAliases}.$it" + } ?: JsTestGenerationSettings.fileUnderTestAliases + var callString = "$initClass.${method.name}" + callString = List(actualParams.size) { idx -> "param$idx" }.joinToString( + prefix = "res$index = $callString(", + postfix = ")", + ) + return paramsInit + callString + } + + private fun initParams(fuzzedValue: List): String { + val actualParams = description.thisInstance?.let { fuzzedValue.drop(1) } ?: fuzzedValue + return actualParams.mapIndexed { index, param -> + val varName = "param$index" + buildString { + appendLine("let $varName = ${param.initModelAsString()}") + (param as? UtAssembleModel)?.initModificationsAsString(this, varName) + } + }.joinToString(separator = "\n") + } + + private fun Any.quoteWrapIfNecessary(): String = + when (this) { + is String -> "`$this`" + else -> "$this" + } + + private val symbolsToEscape = setOf("`", Pattern.quote("\\")) + + private fun Any.escapeSymbolsIfNecessary(): Any = + when (this) { + is String -> this.replace(Regex(symbolsToEscape.joinToString(separator = "|")), "") + else -> this + } + + private fun UtAssembleModel.toParamString(): String { + val importPrefix = "new ${JsTestGenerationSettings.fileUnderTestAliases}.".takeIf { + (classId as JsClassId).isExportable + } ?: "new " + val callConstructorString = importPrefix + classId.name + val paramsString = instantiationCall.params.joinToString( + prefix = "(", + postfix = ")", + ) { + it.initModelAsString() + } + return callConstructorString + paramsString + } + + private fun UtArrayModel.toParamString(): String { + val paramsString = stores.values.joinToString( + prefix = "[", + postfix = "]", + ) { + it.initModelAsString() + } + return paramsString + } + + private fun UtModel.initModelAsString(): String = + when (this) { + is UtAssembleModel -> this.toParamString() + is UtArrayModel -> this.toParamString() + is UtNullModel -> "null" + else -> { + (this as JsPrimitiveModel).value.escapeSymbolsIfNecessary().quoteWrapIfNecessary() + } + } + + private fun UtAssembleModel.initModificationsAsString(stringBuilder: StringBuilder, varName: String) { + with(stringBuilder) { + this@initModificationsAsString.modificationsChain.forEach { + if (it is UtExecutableCallModel) { + val exec = it.executable as JsMethodId + appendLine( + it.params.joinToString( + prefix = "$varName.${exec.name}(", + postfix = ")" + ) { model -> + model.initModelAsString() + } + ) + } + } + } + } +} diff --git a/utbot-js/src/main/kotlin/service/coverage/FastCoverageService.kt b/utbot-js/src/main/kotlin/service/coverage/FastCoverageService.kt new file mode 100644 index 0000000000..a2c3c0f05a --- /dev/null +++ b/utbot-js/src/main/kotlin/service/coverage/FastCoverageService.kt @@ -0,0 +1,43 @@ +package service.coverage + +import java.io.File +import mu.KotlinLogging +import org.json.JSONObject +import service.ServiceContext +import settings.JsTestGenerationSettings.fuzzingThreshold +import settings.JsTestGenerationSettings.tempFileName +import utils.JsCmdExec +import utils.data.ResultData + +private val logger = KotlinLogging.logger {} + +class FastCoverageService( + context: ServiceContext, + baseCoverage: Map, + scriptTexts: List, + private val testCaseIndices: IntRange, +) : CoverageService(context, baseCoverage, scriptTexts) { + + override fun generateCoverageReport() { + val (_, errorText) = JsCmdExec.runCommand( + cmd = arrayOf("\"${settings.pathToNode}\"", "\"$utbotDirPath/$tempFileName" + "0.js\""), + dir = projectPath, + shouldWait = true, + timeout = settings.timeout, + ) + for (i in 0..minOf(fuzzingThreshold - 1, testCaseIndices.last)) { + val resFile = File("$utbotDirPath/$tempFileName$i.json") + val rawResult = resFile.readText() + resFile.delete() + val json = JSONObject(rawResult) + val index = json.getInt("index") + if (index != i) logger.error { "Index $index != i $i" } + coverageList.add(index to json.getJSONObject("s")) + val resultData = ResultData(json) + _resultList.add(resultData) + } + if (errorText.isNotEmpty()) { + logger.error { errorText } + } + } +} diff --git a/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt b/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt new file mode 100644 index 0000000000..c985b2454f --- /dev/null +++ b/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt @@ -0,0 +1,11 @@ +package settings + +import service.coverage.CoverageMode + +data class JsDynamicSettings( + val pathToNode: String = "node", + val pathToNYC: String = "nyc", + val pathToNPM: String = "npm", + val timeout: Long = 15L, + val coverageMode: CoverageMode = CoverageMode.FAST +) diff --git a/utbot-js/src/main/kotlin/settings/JsExportsSettings.kt b/utbot-js/src/main/kotlin/settings/JsExportsSettings.kt new file mode 100644 index 0000000000..2c30195953 --- /dev/null +++ b/utbot-js/src/main/kotlin/settings/JsExportsSettings.kt @@ -0,0 +1,8 @@ +package settings + +object JsExportsSettings { + + // Anchors for exports in user's code. Used in regexes to modify this section on demand. + const val startComment = "// Start of exports generated by UTBot" + const val endComment = "// End of exports generated by UTBot" +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/settings/JsPackagesSettings.kt b/utbot-js/src/main/kotlin/settings/JsPackagesSettings.kt new file mode 100644 index 0000000000..ebea20a6ab --- /dev/null +++ b/utbot-js/src/main/kotlin/settings/JsPackagesSettings.kt @@ -0,0 +1,127 @@ +package settings + +import java.io.File +import org.utbot.common.PathUtil.replaceSeparator +import service.PackageJsonService +import settings.JsPackagesSettings.mochaData +import settings.JsPackagesSettings.nycData +import settings.JsPackagesSettings.ternData +import utils.JsCmdExec +import utils.OsProvider + +object JsPackagesSettings { + val mochaData: PackageData = PackageData("mocha", NpmListFlag.L) + val nycData: PackageData = PackageData("nyc", NpmListFlag.G) + val ternData: PackageData = PackageData("tern", NpmListFlag.L) +} + +val jsPackagesList = listOf( + mochaData, + nycData, + ternData +) + +enum class NpmListFlag { + L { + override fun toString(): String = "-l" + }, + G { + override fun toString(): String = "-g" + } +} + +data class PackageData( + val packageName: String, + val npmListFlag: NpmListFlag +) { + + fun findPackagePath(): String? { + val (inputText, _) = JsCmdExec.runCommand( + dir = null, + shouldWait = true, + timeout = 10, + cmd = arrayOf(OsProvider.getProviderByOs().getAbstractivePathTool(), packageName) + ) + + return inputText.split(System.lineSeparator()).first().takeIf { it.contains(packageName) } + } +} + +class PackageDataService( + filePathToInference: String, + private val projectPath: String, + private val pathToNpm: String, +) { + private val packageJson = PackageJsonService(filePathToInference, File(projectPath)).findClosestConfig() + + companion object { + var nycPath: String = "" + private set + } + + fun findPackage(packageData: PackageData): Boolean = with(packageData) { + when (npmListFlag) { + NpmListFlag.G -> { + val (inputText, _) = JsCmdExec.runCommand( + dir = projectPath, + shouldWait = true, + timeout = 10, + cmd = arrayOf("\"$pathToNpm\"", "list", npmListFlag.toString()) + ) + var result = inputText.contains(packageName) + if (!result || this == nycData) { + val packagePath = this.findPackagePath() + nycPath = packagePath?.let { + replaceSeparator(it) + OsProvider.getProviderByOs().npmPackagePostfix + } ?: "Nyc was not found" + if (!result) { + result = this.findPackagePath()?.isNotBlank() ?: false + } + } + return result + } + + NpmListFlag.L -> { + packageJson.deps.contains(packageName) + } + } + } + + fun installMissingPackages(packages: List): Pair { + var inputTextAllPackages = "" + var errorTextAllPackages = "" + if (packages.isEmpty()) return inputTextAllPackages to errorTextAllPackages + + val localPackageNames = packages.filter { it.npmListFlag == NpmListFlag.L } + .map { it.packageName }.toTypedArray() + val globalPackageNames = packages.filter { it.npmListFlag == NpmListFlag.G } + .map { it.packageName }.toTypedArray() + + // Local packages installation + if (localPackageNames.isNotEmpty()) { + val (inputText, errorText) = JsCmdExec.runCommand( + dir = projectPath, + shouldWait = true, + timeout = 10, + cmd = arrayOf("\"$pathToNpm\"", "install", NpmListFlag.L.toString(), *localPackageNames) + ) + inputTextAllPackages += inputText + errorTextAllPackages += errorText + } + // Global packages installation + if (globalPackageNames.isNotEmpty()) { + val (inputText, errorText) = JsCmdExec.runCommand( + dir = projectPath, + shouldWait = true, + timeout = 10, + cmd = arrayOf("\"$pathToNpm\"", "install", NpmListFlag.G.toString(), *globalPackageNames) + ) + inputTextAllPackages += inputText + errorTextAllPackages += errorText + } + // Find path to nyc execution file after installation + if (packages.contains(nycData)) findPackage(nycData) + + return Pair(inputTextAllPackages, errorTextAllPackages) + } +} diff --git a/utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt b/utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt new file mode 100644 index 0000000000..37922d5dfe --- /dev/null +++ b/utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt @@ -0,0 +1,21 @@ +package settings + +object JsTestGenerationSettings { + + // Used for toplevel functions in IDEA plugin. + const val dummyClassName = "toplevelHack" + + // Default timeout for Node.js try to run a single testcase. + const val defaultTimeout = 10L + + const val fuzzingTimeout = 30_000L + + // Name of file under test when importing it. + const val fileUnderTestAliases = "fileUnderTest" + + // Name of temporary files created. + const val tempFileName = "temp" + + // Number of test cases that can fit in one temporary file for Fast coverage mode + const val fuzzingThreshold = 300 +} diff --git a/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt new file mode 100644 index 0000000000..54149f9202 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt @@ -0,0 +1,76 @@ +package utils + +import com.google.javascript.rhino.Node +import framework.api.js.JsClassId +import framework.api.js.JsConstructorId +import framework.api.js.JsMethodId +import framework.api.js.util.jsUndefinedClassId +import parser.JsParserUtils.getAbstractFunctionName +import parser.JsParserUtils.getClassMethods +import parser.JsParserUtils.getClassName +import parser.JsParserUtils.isStatic +import service.TernService + +fun JsClassId.constructClass( + ternService: TernService, + classNode: Node? = null, + functions: List = emptyList() +): JsClassId { + val className = classNode?.getClassName() + val methods = constructMethods(classNode, ternService, className, functions) + + val constructor = classNode?.let { + JsConstructorId( + JsClassId(name), + ternService.processConstructor(it), + ) + } + val newClassId = JsClassId( + jsName = name, + methods = methods, + constructor = constructor, + classPackagePath = ternService.projectPath, + classFilePath = ternService.filePathToInference.first(), + ) + methods.forEach { + it.classId = newClassId + } + constructor?.classId = newClassId + return newClassId +} + +private fun JsClassId.constructMethods( + classNode: Node?, + ternService: TernService, + className: String?, + functions: List +): Sequence { + with(this) { + val methods = classNode?.getClassMethods()?.map { methodNode -> + val types = ternService.processMethod(className, methodNode) + JsMethodId( + classId = JsClassId(name), + name = methodNode.getAbstractFunctionName(), + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = emptyList(), + staticModifier = methodNode.isStatic(), + lazyReturnType = types.returnType, + lazyParameters = types.parameters, + ) + }?.asSequence() ?: + // used for toplevel functions + functions.map { funcNode -> + val types = ternService.processMethod(className, funcNode, true) + JsMethodId( + classId = JsClassId(name), + name = funcNode.getAbstractFunctionName(), + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = emptyList(), + staticModifier = true, + lazyReturnType = types.returnType, + lazyParameters = types.parameters, + ) + }.asSequence() + return methods + } +} diff --git a/utbot-js/src/main/kotlin/utils/JsCmdExec.kt b/utbot-js/src/main/kotlin/utils/JsCmdExec.kt new file mode 100644 index 0000000000..c94a4377ca --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/JsCmdExec.kt @@ -0,0 +1,35 @@ +package utils + +import java.io.File +import java.util.concurrent.TimeUnit +import org.utbot.framework.plugin.api.TimeoutException +import settings.JsTestGenerationSettings.defaultTimeout + +object JsCmdExec { + + fun runCommand( + dir: String? = null, + shouldWait: Boolean = false, + timeout: Long = defaultTimeout, + vararg cmd: String, + ): Pair { + val builder = ProcessBuilder(*OsProvider.getProviderByOs().getCmdPrefix(), *cmd) + dir?.let { + builder.directory(File(it)) + } + val process = builder.start() + if (shouldWait) { + if (!process.waitFor(timeout, TimeUnit.SECONDS)) { + process.descendants().forEach { + it.destroy() + } + process.destroy() + throw TimeoutException("") + } + } + return Pair( + process.inputStream.bufferedReader().use { it.readText() }, + process.errorStream.bufferedReader().use { it.readText() } + ) + } +} diff --git a/utbot-js/src/main/kotlin/utils/JsOsUtils.kt b/utbot-js/src/main/kotlin/utils/JsOsUtils.kt new file mode 100644 index 0000000000..0b8c295f6d --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/JsOsUtils.kt @@ -0,0 +1,36 @@ +package utils + +import java.util.Locale + +abstract class OsProvider { + + abstract fun getCmdPrefix(): Array + abstract fun getAbstractivePathTool(): String + + abstract val npmPackagePostfix: String + + companion object { + + fun getProviderByOs(): OsProvider { + val osData = System.getProperty("os.name").lowercase(Locale.getDefault()) + return when { + osData.contains("windows") -> WindowsProvider() + else -> LinuxProvider() + } + } + } +} + +class WindowsProvider : OsProvider() { + override fun getCmdPrefix() = emptyArray() + override fun getAbstractivePathTool() = "where" + + override val npmPackagePostfix = ".cmd" +} + +class LinuxProvider : OsProvider() { + override fun getCmdPrefix() = emptyArray() + override fun getAbstractivePathTool() = "which" + + override val npmPackagePostfix = "" +} diff --git a/utbot-js/src/main/kotlin/utils/PathResolver.kt b/utbot-js/src/main/kotlin/utils/PathResolver.kt new file mode 100644 index 0000000000..8e1730db97 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/PathResolver.kt @@ -0,0 +1,12 @@ +package utils + +import java.nio.file.Paths + +object PathResolver { + + fun getRelativePath(to: String, from: String): String { + val toPath = Paths.get(to) + val fromPath = Paths.get(from) + return toPath.relativize(fromPath).toString().replace("\\", "/") + } +} diff --git a/utbot-js/src/main/kotlin/utils/ValueUtil.kt b/utbot-js/src/main/kotlin/utils/ValueUtil.kt new file mode 100644 index 0000000000..0c9480d01e --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/ValueUtil.kt @@ -0,0 +1,89 @@ +package utils + +import framework.api.js.JsClassId +import framework.api.js.util.isJsStdStructure +import framework.api.js.util.jsBooleanClassId +import framework.api.js.util.jsDoubleClassId +import framework.api.js.util.jsErrorClassId +import framework.api.js.util.jsNumberClassId +import framework.api.js.util.jsStringClassId +import framework.api.js.util.jsUndefinedClassId +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import utils.data.ResultData + +fun ResultData.toJsAny(returnType: JsClassId = jsUndefinedClassId): Pair { + try { + this.buildUniqueValue()?.let { return it } + with(this.rawString) { + return when { + isError -> this to jsErrorClassId + this == "true" || this == "false" -> toBoolean() to jsBooleanClassId + this == "null" || this == "undefined" -> null to jsUndefinedClassId + returnType.isJsStdStructure -> + makeStructure(this, returnType) to returnType + returnType == jsStringClassId || this@toJsAny.type == jsStringClassId.name -> + this.replace("\"", "") to jsStringClassId + + else -> { + if (contains('.')) { + (toDoubleOrNull() ?: toBigDecimal()) to jsDoubleClassId + } else { + val value = toByteOrNull() ?: toShortOrNull() ?: toIntOrNull() ?: toLongOrNull() + ?: toBigIntegerOrNull() ?: toDoubleOrNull() + if (value != null) value to jsNumberClassId else { + val obj = makeObject(this) + obj!! to returnType + } + } + } + } + } + } catch(e: Exception) { + if (e is IllegalStateException) throw e else + throw IllegalStateException("Could not make JavaScript value from $this value with type ${returnType.name}") + } +} + +private fun ResultData.buildUniqueValue(): Pair? { + return when { + isInf -> specSign * Double.POSITIVE_INFINITY to jsDoubleClassId + isNan -> Double.NaN to jsDoubleClassId + else -> null + } +} + +private fun makeObject(objString: String): Map? { + return try { + val trimmed = objString.substringAfter(" ") + val json = JSONObject(trimmed) + val resMap = mutableMapOf() + json.keySet().forEach { + resMap[it] = ResultData(json.get(it).toString(), index = 0).toJsAny().first as Any + } + resMap + } catch (e: JSONException) { + null + } +} + +private fun makeStructure(structString: String, type: JsClassId): List { + val json = JSONArray(structString) + return when (type.name) { + "Array", "Set" -> { + json.map { jsonObj -> + ResultData(jsonObj as JSONObject).toJsAny().first + } + } + "Map" -> { + json.map { jsonObj -> + val name = (jsonObj as JSONObject).get("name") + name to ResultData(jsonObj.getJSONObject("json")).toJsAny().first + } + } + else -> throw UnsupportedOperationException( + "Can't make JavaScript structure from $structString with type ${type.name}" + ) + } +} diff --git a/utbot-js/src/main/kotlin/utils/data/CoverageData.kt b/utbot-js/src/main/kotlin/utils/data/CoverageData.kt new file mode 100644 index 0000000000..b64fdfdaf7 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/data/CoverageData.kt @@ -0,0 +1,5 @@ +package utils.data + +data class CoverageData( + val additionalCoverage: Set +) diff --git a/utbot-js/src/main/kotlin/utils/data/MethodTypes.kt b/utbot-js/src/main/kotlin/utils/data/MethodTypes.kt new file mode 100644 index 0000000000..cea1c403ee --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/data/MethodTypes.kt @@ -0,0 +1,8 @@ +package utils.data + +import framework.api.js.JsClassId + +data class MethodTypes( + val parameters: Lazy>, + val returnType: Lazy, +) \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/utils/data/ResultData.kt b/utbot-js/src/main/kotlin/utils/data/ResultData.kt new file mode 100644 index 0000000000..c3cc1d68ea --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/data/ResultData.kt @@ -0,0 +1,33 @@ +package utils.data + +import org.json.JSONObject + +/** + * Represents results after running function with arguments using Node.js + * @param rawString raw result as [String]. + * @param type result JavaScript type as [String]. + * @param index result index according to fuzzed parameters index. + * @param isNan true if the result is JavaScript NaN. + * @param isInf true if the result is JavaScript Infinity. + * @param isError true if the result contains JavaScript error text. + * @param specSign used for -Infinity and Infinity. + */ +data class ResultData( + val rawString: String, + val type: String = "string", + val index: Int, + val isNan: Boolean = false, + val isInf: Boolean = false, + val isError: Boolean = false, + val specSign: Byte = 1 +) { + constructor(json: JSONObject) : this( + rawString = if (json.has("result")) json.get("result").toString() else "undefined", + type = json.get("type").toString(), + index = json.getInt("index"), + isNan = json.optBoolean("is_nan", false), + isInf = json.optBoolean("is_inf", false), + isError = json.optBoolean("is_error", false), + specSign = json.optInt("spec_sign", 1).toByte() + ) +} diff --git a/utbot-junit-contest/README.md b/utbot-junit-contest/README.md new file mode 100644 index 0000000000..63717d7d24 --- /dev/null +++ b/utbot-junit-contest/README.md @@ -0,0 +1,47 @@ +# Contest estimator + +Contest estimator runs UnitTestBot on the provided projects and returns the generation statistics such as instruction coverage. + +There are two entry points: +- [ContestEstimator.kt][ep 1] is the main entry point. It runs UnitTestBot on the specified projects, calculates statistics for the target classes and projects, and outputs them to a console. +- [StatisticsMonitoring.kt][ep 2] is an additional entry point, which does the same as the previous one but can be configured from a file and dumps the resulting statistics to a file. +It is used to [monitor and chart][monitoring] statistics nightly. + + +[ep 1]: src/main/kotlin/org/utbot/contest/ContestEstimator.kt +[ep 2]: src/main/kotlin/org/utbot/monitoring/StatisticsMonitoring.kt +[monitoring]: ../docs/NightStatisticsMonitoring.md + +## Key functions + +| Function name | File name | Description | +|---------------|---------------------|----------------------------------------------------------------------------------------| +| runGeneration | Contest.kt | Runs UnitTestBot and manages its work | +| runEstimator | ContestEstimator.kt | Configures a project classpath, runs the main generation loop, and collects statistics | + + +## Projects + +The projects are provided to Contest estimator in advance. + +### Structure +All available projects are placed in the [resources][resources] folder, which contains: +- [projects][projects] consisting of the folders with the project JAR files in them. +- [classes][classes] consisting of the folders — each named after the project and containing the `list` file with the fully qualified class names. +It also may contain an `exceptions` file with the description of the expected exceptions, that utbot should find. +Description is presented in the format: `.: ...`. +For example, see this [file](src/main/resources/classes/codeforces/exceptions). + +### How to add a new project +You should add both the JAR files to the `projects` folder and the file with a list of classes to the `classes` folder. + +[resources]: src/main/resources +[projects]: src/main/resources/projects +[classes]: src/main/resources/classes + +## Statistics +Statistics are collected and memorized by the corresponding classes placed in [Statistics.kt][statistics]. +Then [monitoring][ep 2] dumps them using auxiliary classes that are defined in [MonitoringReport.kt][report] — they describe the format of output data. + +[statistics]: src/main/kotlin/org/utbot/contest/Statistics.kt +[report]: src/main/kotlin/org/utbot/monitoring/MonitoringReport.kt diff --git a/utbot-junit-contest/build.gradle b/utbot-junit-contest/build.gradle index a14cf5b38d..0b9d5c0d8e 100644 --- a/utbot-junit-contest/build.gradle +++ b/utbot-junit-contest/build.gradle @@ -1,49 +1,147 @@ +plugins { + id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.20' +} apply plugin: 'jacoco' +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + configurations { fetchInstrumentationJar + approximations + usvmApproximationsApi + usvmInstrumentationCollector + usvmInstrumentationRunner + generatedTestCompile } +def approximationsRepo = "com.github.UnitTestBot.java-stdlib-approximations" +def approximationsVersion = "bfce4eedde" + +def usvmRepo = "com.github.UnitTestBot.usvm" +def usvmVersion = "72924ad" + compileJava { options.compilerArgs << '-XDignore.symbol.file' } compileTestJava { options.fork = true - options.forkOptions.executable = 'javac' - options.compilerArgs << "-XDignore.symbol.file" + options.forkOptions.executable = "javac" + options.forkOptions.javaHome = file(System.getProperty("java.home")) + options.compilerArgs << "-XDignore.symbol.file=true" } +def testProjects = [ + 'build/output/test/antlr', + 'build/output/test/codeforces', + 'build/output/test/fastjson-1.2.50', + 'build/output/test/fescar', + 'build/output/test/guava', + 'build/output/test/guava-26.0', + 'build/output/test/guava-30.0', + 'build/output/test/pdfbox', + 'build/output/test/seata', + 'build/output/test/seata-core-0.5.0', + 'build/output/test/spoon', + 'build/output/test/spoon-core-7.0.0', + 'build/output/test/samples', +] + sourceSets { test { java { - srcDir('build/output/test/antlr') - srcDir('build/output/test/custom') - srcDir('build/output/test/guava') - srcDir('build/output/test/fescar') - srcDir('build/output/test/pdfbox') - srcDir('build/output/test/seata') - srcDir('build/output/test/spoon') - srcDir('build/output/test/samples') - srcDir('build/output/test/utbottest') + testProjects.forEach { + srcDir(it) + } } } } test { useJUnit() - // set heap size for the test JVM(s) - minHeapSize = "128m" - maxHeapSize = "3072m" - - // set JVM arguments for the test JVM(s) - jvmArgs '-XX:MaxHeapSize=3072m' - + if (JavaVersion.current() < JavaVersion.VERSION_1_9) { + jvmArgs = [] + } else { + jvmArgs = [ + "--add-opens", "java.base/java.util.concurrent.atomic=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.invoke=ALL-UNNAMED", + "--add-opens", "java.base/java.util.concurrent=ALL-UNNAMED", + "--add-opens", "java.base/java.util.concurrent.locks=ALL-UNNAMED", + "--add-opens", "java.base/java.text=ALL-UNNAMED", + "--add-opens", "java.base/java.time=ALL-UNNAMED", + "--add-opens", "java.base/java.io=ALL-UNNAMED", + "--add-opens", "java.base/java.nio=ALL-UNNAMED", + "--add-opens", "java.base/java.nio.file=ALL-UNNAMED", + "--add-opens", "java.base/java.net=ALL-UNNAMED", + "--add-opens", "java.base/sun.security.util=ALL-UNNAMED", + "--add-opens", "java.base/sun.reflect.generics.repository=ALL-UNNAMED", + "--add-opens", "java.base/sun.net.util=ALL-UNNAMED", + "--add-opens", "java.base/sun.net.fs=ALL-UNNAMED", + "--add-opens", "java.base/java.security=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.ref=ALL-UNNAMED", + "--add-opens", "java.base/java.math=ALL-UNNAMED", + "--add-opens", "java.base/java.util.stream=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED", + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens", "java.base/sun.security.provider=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.event=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.jimage=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.jimage.decompressor=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.jmod=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.jtrfs=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.loader=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.logger=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.math=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.module=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.commons=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.signature=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.tree=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.tree.analysis=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.objectweb.asm.util=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.xml.sax=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.org.xml.sax.helpers=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.perf=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.platform=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.ref=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.reflect=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.util=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.util.jar=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.util.xml=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.util.xml.impl=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.vm=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.vm.annotation=ALL-UNNAMED" + ] + } finalizedBy jacocoTestReport } jacocoTestReport { + afterEvaluate { + def r = testProjects.collect { + fileTree(dir: it) + }.findAll { + it.dir.exists() + } + sourceDirectories.setFrom(r.collect {files(it) }) + classDirectories.setFrom( + r.collect { + fileTree(dir: it.dir.toPath().parent.resolveSibling("unzipped").resolve(it.dir.name)) + }.findAll { + it.dir.exists() + }.collect { + files(it) + } + ) + } + reports { + csv.enabled = true html.enabled = true } } @@ -51,20 +149,56 @@ jacocoTestReport { dependencies { implementation project(":utbot-framework") implementation project(":utbot-analytics") + implementation project(":utbot-usvm") - implementation "com.github.UnitTestBot:soot:${sootCommitHash}" + implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { + exclude group:'com.google.guava', module:'guava' + } implementation group: 'org.apache.commons', name: 'commons-exec', version: '1.2' implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version implementation group: 'org.jsoup', name: 'jsoup', version: '1.6.2' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1' + implementation group: 'com.google.guava', name: 'guava', version: guavaVersion // need for tests - implementation group: 'org.mockito', name: 'mockito-core', version: '4.2.0' - implementation group: 'org.mockito', name: 'mockito-inline', version: '4.2.0' + implementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + implementation group: 'org.mockito', name: 'mockito-inline', version: mockitoInlineVersion implementation 'junit:junit:4.13.2' + + generatedTestCompile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + generatedTestCompile group: 'org.mockito', name: 'mockito-inline', version: mockitoInlineVersion + generatedTestCompile 'junit:junit:4.13.2' + + implementation "org.burningwave:core:12.62.7" + + implementation "$usvmRepo:usvm-core:$usvmVersion" + implementation "$usvmRepo:usvm-jvm:$usvmVersion" + implementation "$usvmRepo:usvm-jvm-api:$usvmVersion" + implementation "$usvmRepo:usvm-jvm-instrumentation:$usvmVersion" + implementation "$usvmRepo:usvm-jvm-instrumentation-collectors:$usvmVersion" + + implementation group: "org.jacodb", name: "jacodb-core", version: jacoDbVersion + implementation group: "org.jacodb", name: "jacodb-analysis", version: jacoDbVersion + implementation group: "org.jacodb", name: "jacodb-approximations", version: jacoDbVersion + + // TODO uvms-sbft-hack: UtBot has `fastutil:8.3.0` on the classpath that overrides classes from + // `fastutil-core:8.5.11` that USVM adds. Solution: bump `fastutil` version to `8.5.11` + runtimeOnly("it.unimi.dsi:fastutil:8.5.11") + testImplementation fileTree(dir: 'src/main/resources/projects/', include: '*/*.jar') testImplementation files('src/main/resources/evosuite/evosuite-1.2.0.jar') testImplementation files('src/main/resources/evosuite/evosuite-standalone-runtime-1.2.0.jar') fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration: 'instrumentationArchive') + + //TODO: currently unused; their jars should be added to resources/lib if we want to switch to USVM instrumentation + approximations "$approximationsRepo:approximations:$approximationsVersion" + + usvmApproximationsApi "$usvmRepo:usvm-jvm-api:$usvmVersion" + usvmInstrumentationCollector "$usvmRepo:usvm-jvm-instrumentation-collectors:$usvmVersion" + usvmInstrumentationRunner "$usvmRepo:usvm-jvm-instrumentation:$usvmVersion" + usvmInstrumentationRunner "$usvmRepo:usvm-jvm-instrumentation-collectors:$usvmVersion" } processResources { @@ -84,6 +218,10 @@ jar { attributes 'JAR-Type': 'Fat JAR' } + processResources.exclude("classes/**") + processResources.exclude("projects/**") + processResources.exclude("evosuite/**") + version '1.0' dependsOn configurations.runtimeClasspath @@ -119,3 +257,100 @@ task monitoringJar(type: Jar) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } + +// TODO usvm-sbft-saloed: replace with runner from usvm (unavailable due to huge jar size) +task usvmInstrumentationRunnerJar(type: Jar) { + archiveBaseName = "usvm-instrumentation-runner" + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + manifest { + attributes( + "Main-Class": "org.usvm.instrumentation.rd.InstrumentedProcessKt", + "Premain-Class": "org.usvm.instrumentation.agent.Agent", + "Can-Retransform-Classes": "true", + "Can-Redefine-Classes": "true" + ) + } + + from { + configurations.usvmInstrumentationRunner.collect { + it.isDirectory() ? it : zipTree(it) + } + } +} + +task run(type: JavaExec) { + mainClass.set("org.utbot.contest.ContestEstimatorKt") + classpath = sourceSets.main.runtimeClasspath + workingDir = project.rootProject.projectDir + + def usvmApproximationJarPath = configurations.approximations.resolvedConfiguration.files.find() + def usvmApproximationApiJarPath = configurations.usvmApproximationsApi.resolvedConfiguration.files.find() + environment "usvm.jvm.api.jar.path", usvmApproximationApiJarPath.absolutePath + environment "usvm.jvm.approximations.jar.path", usvmApproximationJarPath.absolutePath + systemProperty("org.jacodb.impl.storage.defaultBatchSize", 2000) + + def usvmInstrumentationCollectorJarPath = configurations.usvmInstrumentationCollector.resolvedConfiguration.files.find() + environment "usvm-jvm-collectors-jar", usvmInstrumentationCollectorJarPath.absolutePath + + dependsOn(usvmInstrumentationRunnerJar) + environment "usvm-jvm-instrumentation-jar", usvmInstrumentationRunnerJar.outputs.files.singleFile + + // "JAVA_HOME" specifies Java path for instrumented process and JacoDB, + // while `System.getProperty('java.home')` is Java used by this process. + // We want both of them to be the same and we also need JDK (not JRE), since we use `javac` to compile tests. + def javaHome = System.getProperty('java.home') + def jreSuffix = "${File.separatorChar}jre" + if (javaHome.endsWith(jreSuffix)) javaHome = javaHome.dropRight(jreSuffix.length()) + environment "JAVA_HOME", javaHome +} + +tasks.register("generateRuntool") { + dependsOn(jar, usvmInstrumentationRunnerJar) + + doLast { + def distDir = buildDir.toPath().resolve("utbot-usvm-runtool").toFile() + copy { + from jar.outputs + into distDir + rename { "utbot-usvm-tool.jar" } + } + + copy { + from configurations.usvmApproximationsApi.resolvedConfiguration.files.find() + into distDir + rename { "usvm-api.jar" } + } + + copy { + from configurations.approximations.resolvedConfiguration.files.find() + into distDir + rename { "usvm-approximations.jar" } + } + + copy { + from configurations.usvmInstrumentationCollector.resolvedConfiguration.files.find() + into distDir + rename { "usvm-jvm-collectors.jar" } + } + + copy { + from usvmInstrumentationRunnerJar.outputs + into distDir + rename { "usvm-jvm-instrumentation.jar" } + } + + copy { + from projectDir.toPath().resolve("usvm-runtool") + into distDir + rename { "runtool" } + } + + def libsDir = distDir.toPath().resolve("lib").toFile() + configurations.generatedTestCompile.resolvedConfiguration.files.forEach { f -> + copy { + from f + into libsDir + } + } + } +} diff --git a/utbot-junit-contest/runtool b/utbot-junit-contest/runtool index 43e3becd6c..b191e8a6fc 100644 --- a/utbot-junit-contest/runtool +++ b/utbot-junit-contest/runtool @@ -1,10 +1,10 @@ #!/bin/bash # switch to environment JVM as needed -JAVA_HOME=/usr +JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 -APACHE_EXECS_LIB=lib/org/apache/commons/commons-exec/1.2/commons-exec-1.2.jar -TOOL=lib/runtool-1.0.0.jar +TOOL=lib/utbot-junit-contest-1.0.jar +export UTBOT_EXTRA_PARAMS=-Xmx4g export JAVA_HOME=$JAVA_HOME -$JAVA_HOME/bin/java -cp $TOOL:$APACHE_EXECS_LIB sbst.runtool.Main +$JAVA_HOME/bin/java -cp $TOOL sbst.runtool.Main diff --git a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ClassUnderTest.kt b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ClassUnderTest.kt index cf0898df6c..b7252e493a 100644 --- a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ClassUnderTest.kt +++ b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ClassUnderTest.kt @@ -1,8 +1,8 @@ package org.utbot.contest import org.utbot.common.FileUtil -import org.utbot.framework.codegen.model.util.createTestClassName import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.nameWithEnclosingClassesAsContigousString import org.utbot.framework.plugin.api.util.utContext import java.io.File import java.nio.file.Paths @@ -33,11 +33,12 @@ class ClassUnderTest( // val classpathDir : File get() = FileUtil.locateClassPath(kotlinClass)?.absoluteFile !! - val packageName: String get() = fqn.substringBeforeLast('.', "") - val simpleName: String get() = createTestClassName(fqn) - + /** + * These properties should be obtained only with utContext set + */ + private val packageName: String get() = classId.packageName + val simpleName: String get() = classId.nameWithEnclosingClassesAsContigousString val testClassSimpleName: String get() = simpleName + "Test" - val generatedTestFile: File get() = Paths.get( generatedTestsSourcesDir.canonicalPath, @@ -52,6 +53,4 @@ class ClassUnderTest( "\n generatedTestsSourcesDir: $generatedTestsSourcesDir" + "\n]" } - - } \ No newline at end of file diff --git a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/Contest.kt b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/Contest.kt index 691faa4b64..9382af1ece 100644 --- a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/Contest.kt +++ b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/Contest.kt @@ -3,25 +3,13 @@ package org.utbot.contest import mu.KotlinLogging import org.objectweb.asm.Type import org.utbot.common.FileUtil -import org.utbot.common.bracket +import org.utbot.common.measureTime import org.utbot.common.filterWhen import org.utbot.common.info import org.utbot.common.isAbstract import org.utbot.engine.EngineController import org.utbot.framework.TestSelectionStrategyType import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.junitByVersion -import org.utbot.framework.codegen.model.CodeGenerator -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.Coverage -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.framework.plugin.api.TestCaseGenerator -import org.utbot.framework.plugin.api.UtError -import org.utbot.framework.plugin.api.UtExecution -import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.executableId import org.utbot.framework.plugin.api.util.id @@ -29,7 +17,7 @@ import org.utbot.framework.plugin.api.util.jClass import org.utbot.framework.plugin.api.util.utContext import org.utbot.framework.plugin.api.util.withUtContext import org.utbot.framework.plugin.services.JdkInfoService -import org.utbot.framework.util.isKnownSyntheticMethod +import org.utbot.framework.util.isKnownImplicitlyDeclaredMethod import org.utbot.fuzzer.UtFuzzedExecution import org.utbot.instrumentation.ConcreteExecutor import org.utbot.instrumentation.ConcreteExecutorPool @@ -59,7 +47,19 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.coroutines.yield +import org.utbot.contest.usvm.createJcContainer +import org.utbot.contest.usvm.runUsvmGeneration +import org.utbot.framework.SummariesGenerationType +import org.utbot.framework.codegen.domain.* +import org.utbot.framework.codegen.generator.CodeGenerator +import org.utbot.framework.codegen.generator.CodeGeneratorParams +import org.utbot.framework.codegen.services.language.CgLanguageAssistant +import org.utbot.framework.minimization.minimizeExecutions +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.isSynthetic +import org.utbot.framework.util.jimpleBody +import org.utbot.summary.summarizeAll +import org.utbot.usvm.jc.JcContainer internal const val junitVersion = 4 private val logger = KotlinLogging.logger {} @@ -110,13 +110,39 @@ fun main(args: Array) { println("Started UtBot Contest, classfileDir = $classfileDir, classpathString=$classpathString, outputDir=$outputDir, mocks=$mockStrategyApi") + // TODO: tmp hack to retrieve tmp dir wrt contest rules + val tmpDir = outputDir.resolveSibling("data") + val cpEntries = classpathString.split(File.pathSeparator).map { File(it) } - val classLoader = URLClassLoader(cpEntries.map { it.toUrl() }.toTypedArray()) + val classLoader = URLClassLoader(cpEntries.map { it.toUrl() }.toTypedArray(), null) val context = UtContext(classLoader) + val testCompileCp = System.getenv("UTBOT_CONTEST_TEST_COMPILE_CP") + var classpathStringForTestCompile = classpathString + if (!testCompileCp.isNullOrBlank()) { + classpathStringForTestCompile = "$classpathStringForTestCompile${File.pathSeparator}$testCompileCp" + } withUtContext(context) { - logger.info().bracket("warmup: kotlin reflection :: init") { + when (mainTool) { + Tool.USVM -> { + // Initialize the JacoDB and start executor before contest is started. + // This saves the time budget for real work instead of initialization. + runBlocking { + createJcContainer( + tmpDir = tmpDir, + classpathFiles = classpathString.split(File.pathSeparator).map { File(it) } + ).runner.ensureRunnerAlive() + } + } + Tool.UtBot -> { + // Initialize the soot before contest is started. + // This saves the time budget for real work instead of soot initialization. + TestCaseGenerator(listOf(classfileDir), classpathString, dependencyPath, JdkInfoService.provide()) + } + } + + logger.info().measureTime({ "warmup: kotlin reflection :: init" }) { prepareClass(ConcreteExecutorPool::class.java, "") prepareClass(Warmup::class.java, "") } @@ -137,40 +163,88 @@ fun main(args: Array) { val timeBudgetSec = cmd[2].toLong() val cut = ClassUnderTest(classLoader.loadClass(classUnderTestName).id, outputDir, classfileDir.toFile()) - runGeneration(cut, timeBudgetSec, classpathString, runFromEstimator = false, methodNameFilter = null) + when (mainTool) { + Tool.USVM -> runUsvmGeneration( + project = "Contest", + cut, + timeBudgetSec, + classpathString, + runFromEstimator = false, + expectedExceptions = ExpectedExceptionsForClass(), + tmpDir = tmpDir, + methodNameFilter = null + ) + Tool.UtBot -> runGeneration( + project = "Contest", + cut, + timeBudgetSec, + fuzzingRatio = 0.1, + classpathString, + runFromEstimator = false, + expectedExceptions = ExpectedExceptionsForClass(), + methodNameFilter = null + ) + } + + val compiledClassFileDir = File(outputDir.absolutePath, "compiledClassFiles") + compiledClassFileDir.mkdirs() + compileClassAndRemoveUncompilableTests( + testDir = compiledClassFileDir.absolutePath, + classPath = classpathStringForTestCompile, + testClass = cut.generatedTestFile.absolutePath + ) + compiledClassFileDir.deleteRecursively() + println("${ContestMessage.READY}") } } + ConcreteExecutor.defaultPool.close() + JcContainer.close() } fun setOptions() { Settings.defaultConcreteExecutorPoolSize = 1 UtSettings.useFuzzing = true UtSettings.classfilesCanChange = false - UtSettings.useAssembleModelGenerator = false - UtSettings.enableMachineLearningModule = false + // We need to use assemble model generator to increase readability + UtSettings.useAssembleModelGenerator = true + UtSettings.summaryGenerationType = SummariesGenerationType.LIGHT + when(mainTool){ + Tool.USVM -> { + UtSettings.enableTestNamesGeneration = true + UtSettings.enableDisplayNameGeneration = false + UtSettings.enableJavaDocGeneration = true + } + Tool.UtBot -> { + UtSettings.enableDisplayNameGeneration = true + } + } + UtSettings.preferredCexOption = false UtSettings.warmupConcreteExecution = true UtSettings.testMinimizationStrategyType = TestSelectionStrategyType.COVERAGE_STRATEGY + UtSettings.maxUnknownCoverageExecutionsPerMethodPerResultType = 10 UtSettings.ignoreStringLiterals = true UtSettings.maximizeCoverageUsingReflection = true + UtSettings.useSandbox = false + UtSettings.addTestMethodMarkers = true } @ObsoleteCoroutinesApi @SuppressWarnings fun runGeneration( + project: String, cut: ClassUnderTest, timeLimitSec: Long, + fuzzingRatio: Double, classpathString: String, runFromEstimator: Boolean, + expectedExceptions: ExpectedExceptionsForClass, methodNameFilter: String? = null // For debug purposes you can specify method name ): StatsForClass = runBlocking { - - - - val testSets: MutableList = mutableListOf() + val testsByMethod: MutableMap> = mutableMapOf() val currentContext = utContext val timeBudgetMs = timeLimitSec * 1000 @@ -185,10 +259,10 @@ fun runGeneration( if (runFromEstimator) { setOptions() //will not be executed in real contest - logger.info().bracket("warmup: 1st optional soot initialization and executor warmup (not to be counted in time budget)") { - TestCaseGenerator(listOf(cut.classfileDir.toPath()), classpathString, dependencyPath, JdkInfoService.provide()) + logger.info().measureTime({ "warmup: 1st optional soot initialization and executor warmup (not to be counted in time budget)" }) { + TestCaseGenerator(listOf(cut.classfileDir.toPath()), classpathString, dependencyPath, JdkInfoService.provide(), forceSootReload = false) } - logger.info().bracket("warmup (first): kotlin reflection :: init") { + logger.info().measureTime({ "warmup (first): kotlin reflection :: init" }) { prepareClass(ConcreteExecutorPool::class.java, "") prepareClass(Warmup::class.java, "") } @@ -204,19 +278,24 @@ fun runGeneration( logger.error("Seems like classloader for cut not valid (maybe it was backported to system): ${cut.classLoader}") } - val statsForClass = StatsForClass() + val statsForClass = StatsForClass(project, cut.fqn) val codeGenerator = CodeGenerator( + CodeGeneratorParams( cut.classId, + projectType = ProjectType.PureJvm, testFramework = junitByVersion(junitVersion), staticsMocking = staticsMocking, forceStaticMocking = forceStaticMocking, - generateWarningsForStaticMocking = false + generateWarningsForStaticMocking = false, + cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(CodegenLanguage.defaultItem), + runtimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.PASS, ) + ) - logger.info().bracket("class ${cut.fqn}", { statsForClass }) { + logger.info().measureTime({ "class ${cut.fqn}" }, { statsForClass }) { - val filteredMethods = logger.info().bracket("preparation class ${cut.clazz}: kotlin reflection :: run") { + val filteredMethods = logger.info().measureTime({ "preparation class ${cut.clazz}: kotlin reflection :: run" }) { prepareClass(cut.clazz, methodNameFilter) } @@ -226,8 +305,8 @@ fun runGeneration( if (filteredMethods.isEmpty()) return@runBlocking statsForClass val testCaseGenerator = - logger.info().bracket("2nd optional soot initialization") { - TestCaseGenerator(listOf(cut.classfileDir.toPath()), classpathString, dependencyPath, JdkInfoService.provide()) + logger.info().measureTime({ "2nd optional soot initialization" }) { + TestCaseGenerator(listOf(cut.classfileDir.toPath()), classpathString, dependencyPath, JdkInfoService.provide(), forceSootReload = false) } @@ -246,7 +325,10 @@ fun runGeneration( val methodJob = currentCoroutineContext().job logger.debug { " ... " } - val statsForMethod = StatsForMethod("${method.classId.simpleName}#${method.name}") + val statsForMethod = StatsForMethod( + "${method.classId.simpleName}#${method.name}", + expectedExceptions.getForMethod(method.name).exceptionNames + ) statsForClass.statsForMethods.add(statsForMethod) @@ -275,9 +357,11 @@ fun runGeneration( val budgetForSymbolicExecution = max(0, budgetForMethod - budgetForLastSolverRequestAndConcreteExecutionRemainingStates) + UtSettings.utBotGenerationTimeoutInMillis = budgetForMethod + UtSettings.fuzzingTimeoutInMillis = (budgetForMethod * fuzzingRatio).toLong() //start controller that will activate symbolic execution - GlobalScope.launch { + GlobalScope.launch(currentContext) { delay(budgetForSymbolicExecution) if (methodJob.isActive) { @@ -294,7 +378,7 @@ fun runGeneration( var testsCounter = 0 - logger.info().bracket("method $method", { statsForMethod }) { + logger.info().measureTime({ "method $method" }, { statsForMethod }) { logger.info { " -- Remaining time budget: $remainingBudget ms, " + "#remaining_methods: $remainingMethodsCount, " + @@ -315,6 +399,9 @@ fun runGeneration( val className = Type.getInternalName(method.classId.jClass) logger.debug { "--new testCase collected, to generate: $testMethodName" } statsForMethod.testsGeneratedCount++ + result.result.exceptionOrNull()?.let { exception -> + statsForMethod.detectedExceptionFqns += exception::class.java.name + } result.coverage?.let { statsForClass.updateCoverage( newCoverage = it, @@ -324,8 +411,7 @@ fun runGeneration( } statsForClass.testedClassNames.add(className) - //TODO: it is a strange hack to create fake test case for one [UtResult] - testSets.add(UtMethodTestSet(method, listOf(result))) + testsByMethod.getOrPut(method) { mutableListOf() } += result } catch (e: Throwable) { //Here we need isolation logger.error(e) { "Code generation failed" } @@ -342,7 +428,7 @@ fun runGeneration( //hack if (statsForMethod.isSuspicious && (ConcreteExecutor.lastSendTimeMs - ConcreteExecutor.lastReceiveTimeMs) > 5000) { - logger.error { "HEURISTICS: close child process, because it haven't responded for long time: ${ConcreteExecutor.lastSendTimeMs - ConcreteExecutor.lastReceiveTimeMs}" } + logger.error { "HEURISTICS: close instrumented process, because it haven't responded for long time: ${ConcreteExecutor.lastSendTimeMs - ConcreteExecutor.lastReceiveTimeMs}" } ConcreteExecutor.defaultPool.close() } } @@ -350,15 +436,18 @@ fun runGeneration( controller.job = job //don't start other methods while last method still in progress - while (job.isActive) - yield() + try { + job.join() + } catch (t: Throwable) { + logger.error(t) { "Internal job error" } + } remainingMethodsCount-- } } - val cancellator = GlobalScope.launch { + val cancellator = GlobalScope.launch(currentContext) { delay(remainingBudget()) if (engineJob.isActive) { logger.warn { "Cancelling job because timeout $generationTimeout ms elapsed (real cancellation can take time)" } @@ -377,7 +466,11 @@ fun runGeneration( } cancellator.cancel() - logger.info().bracket("Flushing tests for [${cut.simpleName}] on disk") { + val testSets = testsByMethod.map { (method, executions) -> + UtMethodTestSet(method, minimizeExecutions(executions), jimpleBody(method)) + }.summarizeAll(cut.classfileDir.toPath(), sourceFile = null) + + logger.info().measureTime({ "Flushing tests for [${cut.simpleName}] on disk" }) { writeTestClass(cut, codeGenerator.generateAsString(testSets)) } //write classes @@ -386,7 +479,7 @@ fun runGeneration( statsForClass } -private fun prepareClass(javaClazz: Class<*>, methodNameFilter: String?): List { +fun prepareClass(javaClazz: Class<*>, methodNameFilter: String?): List { //1. all methods from cut val methods = javaClazz.declaredMethods .filterNot { it.isAbstract } @@ -402,7 +495,9 @@ private fun prepareClass(javaClazz: Class<*>, methodNameFilter: String?): List { + val regex = """package ([\d\w.]+) is declared in module ([\d\w.]+), which does not export it to the unnamed module""".toRegex() + return regex.findAll(report).map { + val pack = it.groupValues[1] + val module = it.groupValues[2] + UnnamedPackageInfo(pack, module) + }.toList().distinct() +} + +private fun findErrorLines(report: String): List { + // (\d+) is line number + // (:(\d+)) is optional column number + val regex = """\.java:(\d+)(:(\d+))?: error""".toRegex() + return regex.findAll(report).map { + it.groupValues[1].toInt() - 1 + }.toList().distinct() +} + +fun compileClassAndRemoveUncompilableTests(testDir: String, classPath: String, testClass: String): Int = try { + val exports = mutableSetOf() + var exitCode = 0 + + repeat(compileAttempts) { attemptNumber -> + val cmd = arrayOf( + javacCmd, + *exports.flatMap { + listOf("--add-exports", "${it.module}/${it.pack}=ALL-UNNAMED") + }.toTypedArray(), + "-d", testDir, + "-cp", classPath, + "-nowarn", + "-XDignore.symbol.file", + testClass + ) + logger.debug { "Compile attempt ${attemptNumber + 1}" } + + logger.trace { cmd.toText() } + + val process = Runtime.getRuntime().exec(cmd) + + val errors = process.errorStream.reader().buffered().readText() + + exitCode = process.waitFor() + if (exitCode == 0) { + return 0 + } else { + if (errors.isNotEmpty()) + logger.error { "Compilation errors: $errors" } + exports += findAllNotExportedPackages(errors) + if (attemptNumber >= 1) { + val testFile = File(testClass) + val testClassLines = testFile.readLines() + val testStarts = testClassLines.withIndex() + .filter { it.value.contains(CgAbstractRenderer.TEST_METHOD_START_MARKER) } + .map { it.index } + val testEnds = testClassLines.withIndex() + .filter { it.value.contains(CgAbstractRenderer.TEST_METHOD_END_MARKER) } + .map { it.index } + val errorLines = findErrorLines(errors) + val errorRanges = errorLines.map { errorLine -> + // if error is outside test method, we can't fix that by removing test methods + val testStart = testStarts.filter { it <= errorLine }.maxOrNull() ?: return exitCode + val testEnd = testEnds.filter { it >= errorLine }.minOrNull() ?: return exitCode + testStart..testEnd + }.distinct() + if (errorRanges.size > testStarts.size / 5.0) { + logger.error { "Over 20% of test are uncompilable" } + logger.error { "Speculating that something is wrong with compilation settings, keeping all tests" } + return exitCode + } + val linesToRemove = mutableSetOf() + errorRanges.forEach { linesToRemove.addAll(it) } + val removedText = testClassLines.withIndex() + .filter { it.index in linesToRemove } + .joinToString("\n") { "${it.index}: ${it.value}" } + logger.info { "Removed uncompilable tests:\n$removedText" } + testFile.writeText(testClassLines.filterIndexed { i, _ -> i !in linesToRemove }.joinToString("\n")) + } + } + } + + exitCode +} catch (e: Throwable) { + logger.error(e) { "compileClass failed" } + 1 +} finally { + val testFile = File(testClass) + testFile.writeText(testFile.readLines().filter { + !it.contains(CgAbstractRenderer.TEST_METHOD_START_MARKER) + && !it.contains(CgAbstractRenderer.TEST_METHOD_END_MARKER) + }.joinToString("\n")) +} + fun Array.toText() = joinToString(separator = ",") @Suppress("unused") @@ -76,65 +180,65 @@ object Paths { } @Suppress("unused") -enum class Tool { - UtBot { - @Suppress("EXPERIMENTAL_API_USAGE") +interface Tool { + sealed class UtBotBasedTool : Tool { + abstract fun runGeneration( + project: ProjectToEstimate, + cut: ClassUnderTest, + timeLimit: Long, + fuzzingRatio: Double, + methodNameFilter: String?, + statsForProject: StatsForProject, + compiledTestDir: File, + classFqn: String, + expectedExceptions: ExpectedExceptionsForClass + ) : StatsForClass + override fun run( project: ProjectToEstimate, cut: ClassUnderTest, timeLimit: Long, + fuzzingRatio: Double, methodNameFilter: String?, - globalStats: GlobalStats, + statsForProject: StatsForProject, compiledTestDir: File, - classFqn: String - ) { + classFqn: String, + expectedExceptions: ExpectedExceptionsForClass + ) = withUtContext(ContextManager.createNewContext(project.classloader)) { val classStats: StatsForClass = try { - withUtContext(UtContext(project.classloader)) { - runGeneration( - cut, - timeLimit, - project.sootClasspathString, - runFromEstimator = true, - methodNameFilter - ) - } + runGeneration( + project, + cut, + timeLimit, + fuzzingRatio, + methodNameFilter, + statsForProject, + compiledTestDir, + classFqn, + expectedExceptions + ) } catch (e: CancellationException) { logger.info { "[$classFqn] finished with CancellationException" } return } catch (e: Throwable) { + logger.error(e) { "ISOLATION: $e" } logger.info { "ISOLATION: $e" } logger.info { "continue without compilation" } return } - globalStats.statsForClasses.add(classStats) + statsForProject.statsForClasses.add(classStats) try { val testClass = cut.generatedTestFile classStats.testClassFile = testClass - val cmd = arrayOf( - javacCmd, - "-d", compiledTestDir.absolutePath, - "-cp", project.compileClasspathString, - "-nowarn", - "-XDignore.symbol.file", - testClass.absolutePath - ) - - logger.info().bracket("Compiling class ${testClass.absolutePath}") { - - logger.trace { cmd.toText() } - val process = Runtime.getRuntime().exec(cmd) - - thread { - val errors = process.errorStream.reader().buffered().readText() - if (errors.isNotEmpty()) - logger.error { "Compilation errors: $errors" } - }.join() - - - val exitCode = process.waitFor() + logger.info().measureTime({ "Compiling class ${testClass.absolutePath}" }) { + val exitCode = compileClassAndRemoveUncompilableTests( + compiledTestDir.absolutePath, + project.compileClasspathString, + testClass.absolutePath + ) if (exitCode != 0) { logger.error { "Failed to compile test class ${cut.testClassSimpleName}" } classStats.failedToCompile = true @@ -161,16 +265,75 @@ enum class Tool { override fun moveProducedFilesIfNeeded() { // don't do anything } - }, - EvoSuite { + } + + object UtBot : UtBotBasedTool() { + @OptIn(ObsoleteCoroutinesApi::class) + @Suppress("EXPERIMENTAL_API_USAGE") + override fun runGeneration( + project: ProjectToEstimate, + cut: ClassUnderTest, + timeLimit: Long, + fuzzingRatio: Double, + methodNameFilter: String?, + statsForProject: StatsForProject, + compiledTestDir: File, + classFqn: String, + expectedExceptions: ExpectedExceptionsForClass + ): StatsForClass { + return runGeneration( + project.name, + cut, + timeLimit, + fuzzingRatio, + project.sootClasspathString, + runFromEstimator = true, + expectedExceptions, + methodNameFilter + ) + } + } + + object USVM : UtBotBasedTool() { + @OptIn(ObsoleteCoroutinesApi::class) + @Suppress("EXPERIMENTAL_API_USAGE") + override fun runGeneration( + project: ProjectToEstimate, + cut: ClassUnderTest, + timeLimit: Long, + fuzzingRatio: Double, + methodNameFilter: String?, + statsForProject: StatsForProject, + compiledTestDir: File, + classFqn: String, + expectedExceptions: ExpectedExceptionsForClass + ): StatsForClass = runUsvmGeneration( + project.name, + cut, + timeLimit, + project.sootClasspathString, + runFromEstimator = true, + expectedExceptions = expectedExceptions, + tmpDir = File("."), + methodNameFilter = methodNameFilter + ) + + override fun close() { + JcContainer.close() + } + } + + object EvoSuite : Tool { override fun run( project: ProjectToEstimate, cut: ClassUnderTest, timeLimit: Long, + fuzzingRatio: Double, methodNameFilter: String?, - globalStats: GlobalStats, + statsForProject: StatsForProject, compiledTestDir: File, - classFqn: String + classFqn: String, + expectedExceptions: ExpectedExceptionsForClass ) { // EvoSuite has several phases, the variable below is responsible for assert generation // timeout. We want to give 10s for a big time budgets and timeLimit / 5 for small budgets. @@ -233,20 +396,28 @@ enum class Tool { } }; - abstract fun run( + fun run( project: ProjectToEstimate, cut: ClassUnderTest, timeLimit: Long, + fuzzingRatio: Double, // maybe create some specific settings methodNameFilter: String?, - globalStats: GlobalStats, + statsForProject: StatsForProject, compiledTestDir: File, - classFqn: String + classFqn: String, + expectedExceptions: ExpectedExceptionsForClass ) - abstract fun moveProducedFilesIfNeeded() + fun moveProducedFilesIfNeeded() + + fun close() {} } fun main(args: Array) { + // See https://dzone.com/articles/how-to-export-all-modules-to-all-modules-at-runtime-in-java?preview=true + // `Modules` is `null` on JDK 8 (see comment to StaticComponentContainer.Modules) + org.burningwave.core.assembler.StaticComponentContainer.Modules?.exportAllToAll() + val estimatorArgs: Array val methodFilter: String? val projectFilter: List? @@ -254,9 +425,10 @@ fun main(args: Array) { val tools: List // very special case when you run your project directly from IntellijIDEA omitting command line arguments - if (args.isEmpty() && System.getProperty("os.name")?.run { contains("win", ignoreCase = true) } == true) { + if (args.isEmpty()) { processedClassesThreshold = 9999 //change to change number of classes to run - val timeLimit = 20 // increase if you want to debug something + val timeLimit = 30 // increase if you want to debug something + val fuzzingRatio = 0.1 // sets fuzzing ratio to total test generation // Uncomment it for debug purposes: // you can specify method for test generation in format `classFqn.methodName` @@ -275,20 +447,21 @@ fun main(args: Array) { // tools = listOf(Tool.EvoSuite) // config for SBST 2022 - methodFilter = null - projectFilter = listOf("fastjson-1.2.50", "guava-26.0", "seata-core-0.5.0", "spoon-core-7.0.0") - tools = listOf(Tool.UtBot) + methodFilter = "com.alibaba.fastjson.asm.ByteVector.*" + projectFilter = listOf("fastjson-1.2.50") + tools = listOf(mainTool) estimatorArgs = arrayOf( classesLists, jarsDir, "$timeLimit", + "$fuzzingRatio", outputDir, moduleTestDir ) } else { - require(args.size == 6) { - "Wrong arguments: + Try UnitTestBot online demo to see how it generates tests for your code in real time. +
    + Contribute to UnitTestBot via GitHub. +
    + Found a bug? File an issue. +
    + Have an idea? Start a discussion. + ]]> + + diff --git a/utbot-python-pycharm/src/main/resources/META-INF/pluginIcon.svg b/utbot-python-pycharm/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000000..d24574d6dd --- /dev/null +++ b/utbot-python-pycharm/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/utbot-python-pycharm/src/main/resources/META-INF/withPython.xml b/utbot-python-pycharm/src/main/resources/META-INF/withPython.xml new file mode 100644 index 0000000000..f272fd7601 --- /dev/null +++ b/utbot-python-pycharm/src/main/resources/META-INF/withPython.xml @@ -0,0 +1,4 @@ + + + com.intellij.modules.python + \ No newline at end of file diff --git a/utbot-python-pycharm/src/main/resources/application.properties b/utbot-python-pycharm/src/main/resources/application.properties new file mode 100644 index 0000000000..f5af5a168d --- /dev/null +++ b/utbot-python-pycharm/src/main/resources/application.properties @@ -0,0 +1,5 @@ +# suppress inspection "HttpUrlsUsage" for whole file +backing.service.possibleEndpoints=http://utbot.org +backing.service.urlPath=/utbot/errors +backing.service.port=11000 +request.timeoutMillis=5000 \ No newline at end of file diff --git a/utbot-python-pycharm/src/main/resources/log4j2.xml b/utbot-python-pycharm/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..6a9ae540c8 --- /dev/null +++ b/utbot-python-pycharm/src/main/resources/log4j2.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-python-pycharm/src/main/resources/settings.properties b/utbot-python-pycharm/src/main/resources/settings.properties new file mode 100644 index 0000000000..657e42f4f4 --- /dev/null +++ b/utbot-python-pycharm/src/main/resources/settings.properties @@ -0,0 +1,607 @@ +# Copyright (c) 2023 utbot.org +# +# 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. + +# +# Setting to disable coroutines debug explicitly. +# Set it to false if debug info is required. +# +# Default value is [true] +#disableCoroutinesDebug=true + +# +# Make `true` for interactive mode (like Intellij plugin). If `false` UTBot can apply certain optimizations. +# +# Default value is [true] +#classfilesCanChange=true + +# +# Timeout for Z3 solver.check calls. +# Set it to 0 to disable timeout. +# +# Default value is [1000] +#checkSolverTimeoutMillis=1000 + +# +# Timeout for symbolic execution +# +# Default value is [60000] +#utBotGenerationTimeoutInMillis=60000 + +# +# Random seed in path selector. +# Set null to disable random. +# +# Default value is [42] +#seedInPathSelector=42 + +# +# Type of path selector. +# +# COVERED_NEW_SELECTOR: [CoveredNewSelector] +# INHERITORS_SELECTOR: [InheritorsSelector] +# BFS_SELECTOR: [BFSSelector] +# SUBPATH_GUIDED_SELECTOR: [SubpathGuidedSelector] +# CPI_SELECTOR: [CPInstSelector] +# FORK_DEPTH_SELECTOR: [ForkDepthSelector] +# ML_SELECTOR: [MLSelector] +# TORCH_SELECTOR: [TorchSelector] +# RANDOM_SELECTOR: [RandomSelector] +# RANDOM_PATH_SELECTOR: [RandomPathSelector] +# +# Default value is [INHERITORS_SELECTOR] +#pathSelectorType=INHERITORS_SELECTOR + +# +# Type of MLSelector recalculation. +# +# WITH_RECALCULATION: [MLSelectorWithRecalculation] +# WITHOUT_RECALCULATION: [MLSelectorWithoutRecalculation] +# +# Default value is [WITHOUT_RECALCULATION] +#mlSelectorRecalculationType=WITHOUT_RECALCULATION + +# +# Type of [MLPredictor]. +# +# MLP: [MultilayerPerceptronPredictor] +# LINREG: [LinearRegressionPredictor] +# +# Default value is [MLP] +#mlPredictorType=MLP + +# +# Steps limit for path selector. +# +# Default value is [3500] +#pathSelectorStepsLimit=3500 + +# +# Determines whether path selector should save remaining states for concrete execution after stopping by strategy. +# False for all framework tests by default. +#saveRemainingStatesForConcreteExecution=true + +# +# Use debug visualization. +# Set it to true if debug visualization is needed. +# +# Default value is [false] +#useDebugVisualization=false + +# +# Set the value to true to show library classes' graphs in visualization. +# +# Default value is [false] +#showLibraryClassesInVisualization=false + +# +# Use simplification of UtExpressions. +# Set it to false to disable expression simplification. +# +# Default value is [true] +#useExpressionSimplification=true + +# +# Enable the Summarization module to generate summaries for methods under test. +# Note: if it is [SummariesGenerationType.NONE], +# all the execution for a particular method will be stored at the same nameless region. +# +# FULL: All possible analysis actions are taken +# LIGHT: Analysis actions based on sources are NOT taken +# NONE: No summaries are generated +# +# Default value is [FULL] +#summaryGenerationType=FULL + +# +# If True test comments will be generated. +# +# Default value is [true] +#enableJavaDocGeneration=true + +# +# If True cluster comments will be generated. +# +# Default value is [true] +#enableClusterCommentsGeneration=true + +# +# If True names for tests will be generated. +# +# Default value is [true] +#enableTestNamesGeneration=true + +# +# If True display names for tests will be generated. +# +# Default value is [true] +#enableDisplayNameGeneration=true + +# +# If True display name in from -> to style will be generated. +# +# Default value is [true] +#useDisplayNameArrowStyle=true + +# +# Generate summaries using plugin's custom JavaDoc tags. +# +# Default value is [true] +#useCustomJavaDocTags=true + +# +# This option regulates which [NullPointerException] check should be performed for nested methods. +# Set an option in true if you want to perform NPE check in the corresponding situations, otherwise set false. +# +# Default value is [true] +#checkNpeInNestedMethods=true + +# +# This option regulates which [NullPointerException] check should be performed for nested not private methods. +# Set an option in true if you want to perform NPE check in the corresponding situations, otherwise set false. +# +# Default value is [false] +#checkNpeInNestedNotPrivateMethods=false + +# +# This option determines whether we should generate [NullPointerException] checks for final or non-public fields +# in non-application classes. Set by true, this option highly decreases test's readability in some cases +# because of using reflection API for setting final/non-public fields in non-application classes. +# NOTE: With false value loses some executions with NPE in system classes, but often most of these executions +# are not expected by user. +# +# Default value is [false] +#maximizeCoverageUsingReflection=false + +# +# Activate or deactivate substituting static fields values set in static initializer +# with symbolic variable to try to set them another value than in initializer. +# +# Default value is [true] +#substituteStaticsWithSymbolicVariable=true + +# +# Use concrete execution. +# +# Default value is [true] +#useConcreteExecution=true + +# +# Enable code generation tests with every possible configuration +# for every method in samples. +# Important: is enabled generation requires enormous amount of time. +# +# Default value is [false] +#checkAllCombinationsForEveryTestInSamples=false + +# +# Enable transformation UtCompositeModels into UtAssembleModels using AssembleModelGenerator. +# Note: false doesn't mean that there will be no assemble models, it means that the generator will be turned off. +# Assemble models will present for lists, sets, etc. +# +# Default value is [true] +#useAssembleModelGenerator=true + +# +# Test related files from the temp directory that are older than [daysLimitForTempFiles] +# will be removed at the beginning of the test run. +# +# Default value is [3] +#daysLimitForTempFiles=3 + +# +# Enables soft constraints in the engine. +# +# Default value is [true] +#preferredCexOption=true + +# +# Type of test minimization strategy. +# +# DO_NOT_MINIMIZE_STRATEGY: Always adds new test +# COVERAGE_STRATEGY: Adds new test only if it increases coverage +# +# Default value is [COVERAGE_STRATEGY] +#testMinimizationStrategyType=COVERAGE_STRATEGY + +# +# Set to true to start fuzzing if symbolic execution haven't return anything +# +# Default value is [true] +#useFuzzing=true + +# +# Set the total attempts to improve coverage by fuzzer. +# +# Default value is [2147483647] +#fuzzingMaxAttempts=2147483647 + +# +# Fuzzer tries to generate and run tests during this time. +# +# Default value is [3000] +#fuzzingTimeoutInMillis=3000 + +# +# Find implementations of interfaces and abstract classes to fuzz. +# +# Default value is [true] +#fuzzingImplementationOfAbstractClasses=true + +# +# Use methods to mutate fields of classes different from class under test or not. +# +# Default value is [false] +#tryMutateOtherClassesFieldsWithMethods=false + +# +# Generate tests that treat possible overflows in arithmetic operations as errors +# that throw Arithmetic Exception. +# +# Default value is [false] +#treatOverflowAsError=false + +# +# Generate tests that treat assertions as error suits. +# +# Default value is [true] +#treatAssertAsErrorSuite=true + +# +# Instrument all classes before start +# +# Default value is [false] +#warmupConcreteExecution=false + +# +# Ignore string literals during the code analysis to make possible to analyze antlr. +# It is a hack and must be removed after the competition. +# +# Default value is [false] +#ignoreStringLiterals=false + +# +# Timeout for specific concrete execution (in milliseconds). +# +# Default value is [1000] +#concreteExecutionDefaultTimeoutInInstrumentedProcessMillis=1000 + +# +# Enable taint analysis or not. +# +# Default value is [false] +#useTaintAnalysis=false + +# +# Path to custom log4j2 configuration file for EngineProcess. +# By default utbot-intellij/src/main/resources/log4j2.xml is used. +# Also default value is used if provided value is not a file. +#engineProcessLogConfigFile="" + +# +# The property is useful only for the IntelliJ IDEs. +# If the property is set in true the engine process opens a debug port. +# @see runInstrumentedProcessWithDebug +# @see org.utbot.intellij.plugin.process.EngineProcess +# +# Default value is [false] +#runEngineProcessWithDebug=false + +# +# The engine process JDWP agent's port of the engine process. +# A debugger attaches to the port in order to debug the process. +# +# Default value is [5005] +#engineProcessDebugPort=5005 + +# +# Value of the suspend mode for the JDWP agent of the engine process. +# If the value is true, the engine process will suspend until a debugger attaches to it. +# +# Default value is [true] +#suspendEngineProcessExecutionInDebugMode=true + +# +# The property is useful only for the IntelliJ IDEs. +# If the property is set in true the spring analyzer process opens a debug port. +# @see runInstrumentedProcessWithDebug +# @see org.utbot.spring.process.SpringAnalyzerProcess +# +# Default value is [false] +#runSpringAnalyzerProcessWithDebug=false + +# +# The spring analyzer process JDWP agent's port. +# A debugger attaches to the port in order to debug the process. +# +# Default value is [5007] +#springAnalyzerProcessDebugPort=5007 + +# +# Value of the suspend mode for the JDWP agent of the spring analyzer process. +# If the value is true, the spring analyzer process will suspend until a debugger attaches to it. +# +# Default value is [true] +#suspendSpringAnalyzerProcessExecutionInDebugMode=true + +# +# The instrumented process JDWP agent's port of the instrumented process. +# A debugger attaches to the port in order to debug the process. +# +# Default value is [5006] +#instrumentedProcessDebugPort=5006 + +# +# Value of the suspend mode for the JDWP agent of the instrumented process. +# If the value is true, the instrumented process will suspend until a debugger attaches to it. +# +# Default value is [true] +#suspendInstrumentedProcessExecutionInDebugMode=true + +# +# If true, runs the instrumented process with the ability to attach a debugger. +# To debug the instrumented process, set the breakpoint in the +# [org.utbot.instrumentation.rd.InstrumentedProcess.Companion.invoke] +# and in the instrumented process's main function and run the main process. +# Then run the remote JVM debug configuration in IDEA. +# If you see the message in console about successful connection, then +# the debugger is attached successfully. +# Now you can put the breakpoints in the instrumented process and debug +# both processes simultaneously. +# @see [org.utbot.instrumentation.rd.InstrumentedProcess.Companion.invoke] +# +# Default value is [false] +#runInstrumentedProcessWithDebug=false + +# +# Number of branch instructions using for clustering executions in the test minimization phase. +# +# Default value is [4] +#numberOfBranchInstructionsForClustering=4 + +# +# Determines should we choose only one crash execution with "minimal" model or keep all. +# +# Default value is [true] +#minimizeCrashExecutions=true + +# +# Enable it to calculate unsat cores for hard constraints as well. +# It may be usefull during debug. +# Note: it might highly impact performance, so do not enable it in release mode. +# +# Default value is [false] +#enableUnsatCoreCalculationForHardConstraints=false + +# +# Enable it to process states with unknown solver status +# from the queue to concrete execution. +# +# Default value is [true] +#processUnknownStatesDuringConcreteExecution=true + +# +# 2^{this} will be the length of observed subpath. +# See [SubpathGuidedSelector] +# +# Default value is [1] +#subpathGuidedSelectorIndex=1 + +# +# Flag that indicates whether feature processing for execution states enabled or not +# +# Default value is [false] +#enableFeatureProcess=false + +# +# Path to deserialized ML models +# +# Default value is [../models/0] +#modelPath=../models/0 + +# +# Full class name of the class containing the configuration for the ML models to solve path selection task. +# +# Default value is [org.utbot.AnalyticsConfiguration] +#analyticsConfigurationClassPath=org.utbot.AnalyticsConfiguration + +# +# Full class name of the class containing the configuration for the ML models exported from the PyTorch to solve path selection task. +# +# Default value is [org.utbot.AnalyticsTorchConfiguration] +#analyticsTorchConfigurationClassPath=org.utbot.AnalyticsTorchConfiguration + +# +# Number of model iterations that will be used during ContestEstimator +# +# Default value is [1] +#iterations=1 + +# +# Path for state features dir +# +# Default value is [eval/secondFeatures/antlr/INHERITORS_SELECTOR] +#featurePath=eval/secondFeatures/antlr/INHERITORS_SELECTOR + +# +# Counter for tests during testGeneration for one project in ContestEstimator +# +# Default value is [0] +#testCounter=0 + +# +# Flag that indicates whether tests for synthetic (see [Executable.isSynthetic]) and implicitly declared methods (like values, valueOf in enums) should be generated, or not +# +# Default value is [true] +#skipTestGenerationForSyntheticAndImplicitlyDeclaredMethods=true + +# +# Flag that indicates whether should we branch on and set static fields from trusted libraries or not. +# @see [org.utbot.common.WorkaroundReason.IGNORE_STATICS_FROM_TRUSTED_LIBRARIES] +# +# Default value is [true] +#ignoreStaticsFromTrustedLibraries=true + +# +# Use the sandbox in the instrumented process. +# If true, the sandbox will prevent potentially dangerous calls, e.g., file access, reading +# or modifying the environment, calls to `Unsafe` methods etc. +# If false, all these operations will be enabled and may lead to data loss during code analysis +# and test generation. +# +# Default value is [true] +#useSandbox=true + +# +# Transform bytecode in the instrumented process. +# If true, bytecode transformation will help fuzzing to find interesting input data, but the size of bytecode can increase. +# If false, bytecode won`t be changed. +# +# Default value is [false] +#useBytecodeTransformation=false + +# +# Limit for number of generated tests per method (in each region) +# +# Default value is [50] +#maxTestsPerMethodInRegion=50 + +# +# Max file length for generated test file +# +# Default value is [1000000] +#maxTestFileSize=1000000 + +# +# If this options set in true, all soot classes will be removed from a Soot Scene, +# therefore, you will be unable to test soot classes. +# +# Default value is [true] +#removeSootClassesFromHierarchy=true + +# +# If this options set in true, all UtBot classes will be removed from a Soot Scene, +# therefore, you will be unable to test UtBot classes. +# +# Default value is [true] +#removeUtBotClassesFromHierarchy=true + +# +# Use this option to enable calculation and logging of MD5 for dropped states by statistics. +# Example of such logging: +# Dropping state (lastStatus=UNDEFINED) by the distance statistics. MD5: 5d0bccc242e87d53578ca0ef64aa5864 +# +# Default value is [false] +#enableLoggingForDroppedStates=false + +# +# If this option set in true, depending on the number of possible types for +# a particular object will be used either type system based on conjunction +# or on bit vectors. +# @see useBitVecBasedTypeSystem +# +# Default value is [true] +#useBitVecBasedTypeSystem=true + +# +# The number of types on which the choice of the type system depends. +# +# Default value is [64] +#maxTypeNumberForEnumeration=64 + +# +# The threshold for numbers of types for which they will be encoded into solver. +# It is used to do not encode big type storages due to significand performance degradation. +# +# Default value is [512] +#maxNumberOfTypesToEncode=512 + +# +# The behaviour of further analysis if tests generation cancellation is requested. +# +# NONE: Do not react on cancellation +# CANCEL_EVERYTHING: Clear all generated test classes +# SAVE_PROCESSED_RESULTS: Show already processed test classes +# +# Default value is [SAVE_PROCESSED_RESULTS] +#cancellationStrategyType=SAVE_PROCESSED_RESULTS + +# +# Depending on this option, sections might be analyzed or not. +# Note that some clinit sections still will be initialized using runtime information. +# +# Default value is [true] +#enableClinitSectionsAnalysis=true + +# +# Process all clinit sections concretely. +# If [enableClinitSectionsAnalysis] is false, it disables effect of this option as well. +# Note that values processed concretely won't be replaced with unbounded symbolic variables. +# +# Default value is [false] +#processAllClinitSectionsConcretely=false + +# +# In cases where we don't have a body for a method, we can either throw an exception +# or treat this a method as a source of an unbounded symbolic variable returned as a result. +# If this option is set in true, instead of analysis we will return an unbounded symbolic +# variable with a corresponding type. Otherwise, an exception will be thrown. +# Default value is false since it is not a common situation when you cannot retrieve a body +# from a regular method. Setting this option in true might be suitable in situations when +# it is more important not to fall at all rather than work precisely. +#treatAbsentMethodsAsUnboundedValue=false + +# +# A maximum size for any array in the program. Note that input arrays might be less than this value +# due to the symbolic engine limitation, see `org.utbot.engine.Traverser.softMaxArraySize`. +# +# Default value is [1024] +#maxArraySize=1024 + +# +# A maximum size for any array in the program. Note that input arrays might be less than this value +# due to the symbolic engine limitation, see `org.utbot.engine.Traverser.softMaxArraySize`. +# +# Default value is [false] +#disableUnsatChecking=false + +# +# When generating integration tests we only partially reset context in between executions to save time. +# For example, entity id generators do not get reset. It may lead to non-reproduceable results if +# IDs leak to the output of the method under test. +# To cope with that, we rerun executions that are left after minimization, fully resetting Spring context +# between executions. However, full context reset is slow, so we use this setting to limit number of +# tests per method that are rerun with full context reset in case minimization outputs too many tests. +# +# Default value is [25] +#maxSpringContextResetsPerMethod=25 diff --git a/utbot-python/.gitignore b/utbot-python/.gitignore new file mode 100644 index 0000000000..39be1a3893 --- /dev/null +++ b/utbot-python/.gitignore @@ -0,0 +1,2 @@ +*.swp +local_pip_setup/ \ No newline at end of file diff --git a/utbot-python/build.gradle.kts b/utbot-python/build.gradle.kts new file mode 100644 index 0000000000..ac1e3564e6 --- /dev/null +++ b/utbot-python/build.gradle.kts @@ -0,0 +1,32 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject +val log4j2Version: String? by rootProject +val moshiVersion: String? by rootProject +val pythonTypesAPIHash: String? by rootProject + +dependencies { + api(project(":utbot-fuzzing")) + api(project(":utbot-framework")) + api(project(":utbot-python-parser")) + api(project(":utbot-python-executor")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(group = "org.apache.commons", name = "commons-lang3", version = "3.12.0") + implementation(group = "commons-io", name = "commons-io", version = "2.11.0") + implementation("com.squareup.moshi:moshi:$moshiVersion") + implementation("com.squareup.moshi:moshi-kotlin:$moshiVersion") + implementation("com.squareup.moshi:moshi-adapters:$moshiVersion") + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation("org.functionaljava:functionaljava:5.0") + implementation("org.functionaljava:functionaljava-quickcheck:5.0") + implementation("org.functionaljava:functionaljava-java-core:5.0") + implementation(group = "org.apache.commons", name = "commons-text", version = apacheCommonsTextVersion) + implementation(group = "org.apache.logging.log4j", name = "log4j-core", version = log4j2Version) + implementation(group = "org.apache.logging.log4j", name = "log4j-api", version = log4j2Version) + implementation("com.github.UnitTestBot:PythonTypesAPI:$pythonTypesAPIHash") +} \ No newline at end of file diff --git a/utbot-python/docs/CLI.md b/utbot-python/docs/CLI.md new file mode 100644 index 0000000000..71aa95e76a --- /dev/null +++ b/utbot-python/docs/CLI.md @@ -0,0 +1,109 @@ +## Build + +.jar file can be built in Github Actions with script `publish-plugin-and-cli-from-branch`. + +## Requirements + + - Required Java version: 11. + + - Prefered Python version: 3.8+. + + Make sure that your Python has `pip` installed (this is usually the case). [Read more about pip installation](https://pip.pypa.io/en/stable/installation/). + + Before running utbot install pip requirements (or use `--install-requirements` flag in `generate_python` command): + + python -m pip install utbot_executor utbot_mypy_runner + +## Basic usage + +Generate tests: + + java -jar utbot-cli.jar generate_python dir/file_with_sources.py -p -o generated_tests.py -s dir + +This will generate tests for top-level functions from `file_with_sources.py`. + +Run generated tests: + + java -jar utbot-cli.jar run_python generated_tests.py -p + +### `generate_python` options + +- `-s, --sys-path ,` + + (required) Directories to add to `sys.path`. One of directories must contain the file with the methods under test. + + `sys.path` is a list of strings that specifies the search path for modules. It must include paths for all user modules that are used in imports. + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `-o, --output ` + + (required) File for generated tests. + +- `--coverage ` + + File to write coverage report. + +- `-c, --class ` + + Specify top-level (ordinary, not nested) class under test. Without this option tests will be generated for top-level functions. + +- `-m, --methods ,` + + Specify methods under test. + +- `--install-requirements` + + Install Python requirements if missing. + +- `--do-not-minimize` + + Turn off minimization of the number of generated tests. + +- `--do-not-check-requirements` + + Turn off Python requirements check (to speed up). + +- `-t, --timeout INT` + + Specify the maximum time in milliseconds to spend on generating tests (60000 by default). + +- `--timeout-for-run INT` + + Specify the maximum time in milliseconds to spend on one function run (2000 by default). + +- `--test-framework [pytest|Unittest]` + + Test framework to be used. + +- `--do-not-generate-regression-suite` + + Do not generate regression test suite. + +- `--runtime-exception-behaviour [PASS|FAIL]` + + Runtime exception behaviour (assert exceptions or not). + +- `--coverage` + + File to save coverage report. + +### `run_python` options + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `--test-framework [pytest|Unittest]` + + Test framework of tests to run. + +- `-o, --output ` + + Specify file for report. + +## Problems + +- Unittest can not run tests from parent directories diff --git a/utbot-python/docs/docs.md b/utbot-python/docs/docs.md new file mode 100644 index 0000000000..96e0bce318 --- /dev/null +++ b/utbot-python/docs/docs.md @@ -0,0 +1,54 @@ +# UtBot-Python +__Task__: implement utbot for Python using fuzzing to generate tests. + +Subtasks: +* Get list of functions to be tested +* Generate input parameters for this functions +* Compute return values for this parameters +* Render tests + +## Getting list of functions + +We get list of functions to be tested from Intellij IDEA plugin. Other information we get from source code. + +Information about functions: +* Name +* List of parameters +* Source code +* Declaration file +* Type annotations for parameters and return type (optional) + +## Input parameters generation + +### Problem + +If we do not have type annotation, we have to find suitable types for this parameter. + +### Solution +Gather information about Python built-in types (by 'built-in types' we mean types that are implemented in C): + +* Name +* Methods: name + parameters (+ annotations) +* How to generate instances of this type (default, random, using constants from code) + +We can use CPython code and tests for it to gather this. + +For user class we need to initialize its fields recursively. Possible problems: getting types of fields, dynamic addition of new fields. + +To find suitable types for parameter we can look for them only in given and imported files. + +To narrow down the search of suitable types we can gather constraints for function parameters. For that we can analyze AST to see which attributes of parameter are used. + +## Run function with generated parameters + +After generating parameters for fuzzing we pass them on into the function under test and run it in a separate process. This approach is called concrete execution. + +To run the function we need to generate code that imports and calls it and saves result. + +## Get return value + +We write serialized return value in file. To serialize values of the most used built-in types we can use json module. For other types we will have to do it manually. + +## Test generation + +First we build AST of test code and then render it. diff --git a/utbot-python/docs/python_packages.md b/utbot-python/docs/python_packages.md new file mode 100644 index 0000000000..415d410d28 --- /dev/null +++ b/utbot-python/docs/python_packages.md @@ -0,0 +1,65 @@ +# UTBot Python packages managements + +Current UTBot Python packages: + +- `utbot_mypy_runner`: https://pypi.org/project/utbot-mypy-runner/ +- `utbot_executor`: https://pypi.org/project/utbot-executor/ + +To be able to publish new releases on pypi, ask @tochilinak ot @tamarinvs19 to give you permissions. + +## Gradle tasks + +To use Gradle tasks for Python packages, add the following properties in `gradle.properties` in your `GRADLE_USER_HOME` directory (about: https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home): + +- `pythonInterpreter` (for example, `python3`) +- `pypiToken`(about: https://pypi.org/help/#apitoken) + +## utbot_mypy_runner + +How this module is stored in a separate repository: https://github.com/UnitTestBot/PythonTypesAPI. + +### Version + +Write version in file `src/main/resources/utbot_mypy_runner_version`. + +Gradle task `setVersion` will update `pyproject.toml`. + +If you want to use some other version locally (for example, version that is not yet published), set the version +in file `utbot-python/src/main/resources/local_pip_setup/local_utbot_mypy_version`. + +### Usage of local version + +Add the following files locally (they are listed in `.gitignore`): + +- `utbot-python/src/main/resources/local_pip_setup/local_utbot_mypy_path` + + Add here absolute path to `utbot_mypy_runner/dist` directory. + +- `utbot-python/src/main/resources/local_pip_setup/local_utbot_mypy_version` + + Add here the version of local package. This will override the version from module `PythonTypesAPI`. + +- `utbot-python/src/main/resources/local_pip_setup/use_local_python_packages` + + Write here `true` if you want to build `utbot-python` that uses local versions of UTBot Python packages. + +## utbot_executor + +### Version + +Write version in file `utbot-python-executor/src/main/resources/utbot_executor_version`. + +Gradle task `utbot-python-executor:setVersion` will update `pyproject.toml`. + +### Usage of local version + +Add the following files locally (they are listed in `.gitignore`): + +- `utbot-python/src/main/resources/local_pip_setup/local_utbot_executor_path` + + Add here absolute path to `utbot_executor/dist` directory. + + +- `utbot-python/src/main/resources/local_pip_setup/use_local_python_packages` + + Write here `true` if you want to build `utbot-python` that uses local versions of UTBot Python packages. diff --git a/utbot-python/samples/.gitignore b/utbot-python/samples/.gitignore new file mode 100644 index 0000000000..766adac5a5 --- /dev/null +++ b/utbot-python/samples/.gitignore @@ -0,0 +1,11 @@ +.tmp/ +utbot_tests/ +utbot_coverage/ +RUN_RESULT +COVERAGE_RESULT +cli_test_dir/ +utbot-cli.jar +__pycache__/ +.venv/ +venv/ +.coverage diff --git a/utbot-python/samples/inference/inference_example.md b/utbot-python/samples/inference/inference_example.md new file mode 100644 index 0000000000..8f048f3842 --- /dev/null +++ b/utbot-python/samples/inference/inference_example.md @@ -0,0 +1,180 @@ +## Библиотека loguru + +### \_colorama.py + +#### Old +```python +def should_colorize(stream: types.NoneType): pass +def should_colorize(stream: builtins.bool): pass +def should_colorize(stream: builtins.float): pass +def should_colorize(stream: builtins.complex): pass +def should_colorize(stream: builtins.bytearray): pass +def should_colorize(stream: typing.Dict[typing.Any, typing.Any]): pass +def should_colorize(stream: builtins.frozenset): pass +def should_colorize(stream: typing.List[typing.Any]): pass +def should_colorize(stream: typing.Dict[typing.List[typing.Any], builtins.str]): pass +def should_colorize(stream: typing.List[typing.List[typing.Any]]): pass +def should_colorize(stream: builtins.object): pass +def should_colorize(stream: typing.Dict[typing.List[typing.Any], typing.List[typing.Any]]): pass +``` +#### New +``` +typing.Callable[[io.TextIOWrapper], typing.Any] +typing.Callable[[io.StringIO], typing.Any] +typing.Callable[[codecs.StreamReaderWriter], typing.Any] +typing.Callable[[io.IOBase], typing.Any] +typing.Callable[[io.TextIOBase], typing.Any] +typing.Callable[[io.FileIO], typing.Any] +typing.Callable[[io.BytesIO], typing.Any] +typing.Callable[[io.BufferedReader], typing.Any] +typing.Callable[[io.BufferedWriter], typing.Any] +typing.Callable[[io.BufferedRandom], typing.Any] +typing.Callable[[codecs.StreamRecoder], typing.Any] +typing.Callable[[io.RawIOBase], typing.Any] +typing.Callable[[io.BufferedIOBase], typing.Any] +typing.Callable[[io.BufferedRWPair], typing.Any] +typing.Callable[[types.SimpleNamespace], typing.Any] +typing.Callable[[types.ModuleType], typing.Any] +typing.Callable[[types.MethodType], typing.Any] +typing.Callable[[types.GenericAlias], typing.Any] +``` + +### \_datetime.py + +TODO + +### \_filters.py + +#### Old + +```python +def filter_none(record: builtins.str): pass +def filter_none(record: builtins.bytes): pass +def filter_none(record: typing.Dict[typing.Any, typing.Any]): pass +def filter_none(record: typing.Dict[builtins.str, builtins.str]): pass +def filter_none(record: typing.Dict[builtins.str, builtins.int]): pass +def filter_none(record: typing.List[builtins.str]): pass +def filter_none(record: builtins.memoryview): pass +def filter_none(record: typing.Dict[builtins.str, builtins.bool]): pass +def filter_none(record: typing.List[builtins.bool]): pass +def filter_none(record: typing.Set[typing.Any]): pass +``` + +#### New +``` +typing.Callable[[builtins.dict[typing.Any, typing.Any]], typing.Any] +typing.Callable[[builtins.dict[builtins.str, builtins.int]], typing.Any] +typing.Callable[[builtins.dict[builtins.str, builtins.list[typing.Any]]], typing.Any] +typing.Callable[[ctypes.CDLL], typing.Any] +typing.Callable[[ctypes.PyDLL], typing.Any] +typing.Callable[[builtins.dict[builtins.str, builtins.list[builtins.int]]], typing.Any] +typing.Callable[[builtins.dict[builtins.str, builtins.list[builtins.list[typing.Any]]]], typing.Any] +typing.Callable[[builtins.dict[builtins.str, builtins.list[builtins.str]]], typing.Any] +typing.Callable[[email.message.Message], typing.Any] +typing.Callable[[builtins.dict[builtins.str, builtins.list[builtins.list[builtins.int]]]], typing.Any] +typing.Callable[[email.message.EmailMessage], typing.Any] +typing.Callable[[builtins.dict[builtins.str, builtins.str]], typing.Any] +typing.Callable[[builtins.dict[builtins.str, builtins.list[builtins.list[builtins.list[typing.Any]]]]], typing.Any] +typing.Callable[[types.MappingProxyType[typing.Any, typing.Any]], typing.Any] +typing.Callable[[builtins.dict[builtins.str, builtins.list[builtins.list[builtins.list[builtins.int]]]]], typing.Any] +... +``` + +### \_string_parsers.py + +Actually, the only correct variant is str. The code is just messy. + +#### Old + +```python +def parse_size(size: builtins.int): pass +def parse_size(size: builtins.str): pass +def parse_size(size: builtins.range): pass +def parse_size(size: builtins.BaseException): pass +def parse_size(size: builtins.bytes): pass +def parse_size(size: typing.Dict[builtins.int, builtins.int]): pass +def parse_size(size: typing.Dict[builtins.int, builtins.str]): pass +def parse_size(size: typing.List[builtins.int]): pass +def parse_size(size: builtins.memoryview): pass +def parse_size(size: typing.Dict[builtins.int, typing.Dict[typing.Any, typing.Any]]): pass +def parse_size(size: typing.List[typing.Dict[typing.Any, typing.Any]]): pass +def parse_size(size: typing.Set[typing.Any]): pass +``` + +#### New + +``` +typing.Callable[[builtins.str], typing.Any] +typing.Callable[[builtins.int], typing.Any] +``` + +## Проект django-cms + +### cms/api.py + +#### Old + + +```python +def _verify_apphook(apphook: builtins.type, namespace: builtins.int): pass +def _verify_apphook(apphook: builtins.type, namespace: builtins.bool): pass +def _verify_apphook(apphook: build.lib.cms.plugin_base.CMSPluginBase, namespace: builtins.int): pass +def _verify_apphook(apphook: builtins.type, namespace: builtins.str): pass +def _verify_apphook(apphook: build.lib.cms.plugin_base.CMSPluginBase, namespace: builtins.bool): pass +def _verify_apphook(apphook: build.lib.cms.models.pluginmodel.CMSPlugin, namespace: builtins.int): pass +def _verify_apphook(apphook: builtins.type, namespace: builtins.float): pass +def _verify_apphook(apphook: build.lib.cms.plugin_base.CMSPluginBase, namespace: builtins.str): pass +def _verify_apphook(apphook: build.lib.cms.models.pluginmodel.CMSPlugin, namespace: builtins.bool): pass +def _verify_apphook(apphook: builtins.type, namespace: builtins.range): pass +def _verify_apphook(apphook: build.lib.cms.plugin_base.CMSPluginBase, namespace: builtins.float): pass +def _verify_apphook(apphook: build.lib.cms.models.pluginmodel.CMSPlugin, namespace: builtins.str): pass +def _verify_apphook(apphook: builtins.type, namespace: builtins.complex): pass +def _verify_apphook(apphook: build.lib.cms.plugin_base.CMSPluginBase, namespace: builtins.range): pass +def _verify_apphook(apphook: build.lib.cms.models.pluginmodel.CMSPlugin, namespace: builtins.float): pass +def _verify_apphook(apphook: cms.plugin_base.CMSPluginBase, namespace: builtins.int): pass +def _verify_apphook(apphook: builtins.type, namespace: builtins.BaseException): pass +def _verify_apphook(apphook: build.lib.cms.plugin_base.CMSPluginBase, namespace: builtins.complex): pass +def _verify_apphook(apphook: build.lib.cms.models.pluginmodel.CMSPlugin, namespace: builtins.range): pass +def _verify_apphook(apphook: cms.plugin_base.CMSPluginBase, namespace: builtins.bool): pass +def _verify_apphook(apphook: cms.models.pluginmodel.CMSPlugin, namespace: builtins.int): pass +... +``` + +#### New + +``` +typing.Callable[[builtins.str, builtins.int], typing.Any] +``` + +### cms/toolbar/items.py + +#### Old + +```python +def may_be_lazy(thing: builtins.int): pass +def may_be_lazy(thing: builtins.bool): pass +def may_be_lazy(thing: builtins.str): pass +def may_be_lazy(thing: builtins.float): pass +def may_be_lazy(thing: builtins.range): pass +def may_be_lazy(thing: builtins.complex): pass +def may_be_lazy(thing: builtins.BaseException): pass +def may_be_lazy(thing: builtins.bytearray): pass +def may_be_lazy(thing: builtins.bytes): pass +... +``` + +#### New + +``` +typing.Callable[[builtins.int], typing.Any] +typing.Callable[[builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.list[builtins.int]], typing.Any] +typing.Callable[[builtins.str], typing.Any] +typing.Callable[[builtins.list[builtins.list[typing.Any]]], typing.Any] +typing.Callable[[builtins.bool], typing.Any] +typing.Callable[[builtins.float], typing.Any] +typing.Callable[[builtins.list[builtins.str]], typing.Any] +... +``` + +#### find_first: TODO \ No newline at end of file diff --git a/utbot-python/samples/inference/inference_testing.md b/utbot-python/samples/inference/inference_testing.md new file mode 100644 index 0000000000..b09f832362 --- /dev/null +++ b/utbot-python/samples/inference/inference_testing.md @@ -0,0 +1,395 @@ +## File 1 + +Source: https://github.com/AtsushiSakai/PythonRobotics/blob/master/PathPlanning/DynamicWindowApproach/dynamic_window_approach.py + +Top-level functions (with only positional arguments): + +- [ ] `def dwa_control(x, config, goal, ob)` +- [x] `def motion(x, u, dt)` +- [x] `def calc_dynamic_window(x, config)` +- [x] `def predict_trajectory(x_init, v, y, config)` +- [x] `def calc_control_and_trajectory(x, dw, config, goal, ob)` +- [x] `def calc_obstacle_cost(trajectory, ob, config)` +- [x] `def calc_to_goal_cost(trajectory, goal)` +- [x] `def plot_robot(x, y, yaw, config)` + +Used time limit: 25 seconds. + +Command: + + java -jar utbot-cli-python-2023.02-SNAPSHOT.jar infer_types -p python3 -s /home/tochilinak/Documents/projects/utbot/PythonRobotics -t 25000 "/home/tochilinak/Documents/projects/utbot/PythonRobotics/PathPlanning/DynamicWindowApproach/dynamic_window_approach.py" + + +### OK: `def motion(x, u, dt)` + +``` +typing.Callable[[builtins.list[typing.Any], builtins.list[typing.Any], builtins.float], typing.Any] +typing.Callable[[builtins.dict[typing.Any, typing.Any], builtins.list[typing.Any], builtins.float], typing.Any] +typing.Callable[[builtins.list[typing.Any], builtins.dict[typing.Any, typing.Any], builtins.float], typing.Any] +typing.Callable[[builtins.list[builtins.float], builtins.list[builtins.int], builtins.float], typing.Any] +typing.Callable[[builtins.dict[typing.Any, typing.Any], builtins.dict[typing.Any, typing.Any], builtins.float], typing.Any] +typing.Callable[[ctypes.pointer[typing.Any], builtins.list[typing.Any], builtins.float], typing.Any] +typing.Callable[[builtins.list[typing.Any], ctypes.pointer[typing.Any], builtins.float], typing.Any] +typing.Callable[[array.array[typing.Any], builtins.list[typing.Any], builtins.float], typing.Any] +typing.Callable[[builtins.list[builtins.float], builtins.dict[builtins.int, builtins.int], builtins.float], typing.Any] +typing.Callable[[builtins.dict[builtins.int, builtins.float], builtins.list[builtins.int], builtins.float], typing.Any] +``` + + +### OK: `def calc_dynamic_window(x, config)` + +``` +typing.Callable[[builtins.list[typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.int], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.dict[typing.Any, typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.bool], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.float], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.dict[builtins.int, builtins.int], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[ctypes.pointer[typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[array.array[typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[types.MappingProxyType[typing.Any, typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +... +``` + + +### OK: `def predict_trajectory(x_init, v, y, config)` + +``` +typing.Callable[[builtins.int, builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[typing.Any], builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.int], builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.list[typing.Any]], builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.str], builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.int, builtins.list[typing.Any], builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.int, builtins.list[builtins.int], builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.int, builtins.int, builtins.list[typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.list[builtins.int]], builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.int, builtins.int, builtins.list[builtins.int], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.int, builtins.list[builtins.list[typing.Any]], builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.list[builtins.list[typing.Any]]], builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.int, builtins.int, builtins.list[builtins.list[typing.Any]], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +... +``` + +### OK: `def calc_control_and_trajectory(x, dw, config, goal, ob)` +``` +typing.Callable[[builtins.list[typing.Any], builtins.list[typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config, builtins.list[typing.Any], builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.list[builtins.int], builtins.list[builtins.list[typing.Any]], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config, builtins.list[builtins.list[typing.Any]], builtins.list[builtins.list[typing.Any]]], typing.Any] +typing.Callable[[builtins.list[builtins.list[typing.Any]], builtins.list[builtins.int], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config, builtins.list[builtins.list[typing.Any]], builtins.list[builtins.list[typing.Any]]], typing.Any] +typing.Callable[[builtins.list[builtins.int], builtins.list[builtins.list[builtins.list[typing.Any]]], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config, builtins.list[builtins.list[builtins.list[typing.Any]]], builtins.list[builtins.list[builtins.list[typing.Any]]]], typing.Any] +typing.Callable[[builtins.list[typing.Any], builtins.str, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config, builtins.list[typing.Any], builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.list[builtins.int], builtins.str, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config, builtins.list[builtins.list[typing.Any]], builtins.list[builtins.list[typing.Any]]], typing.Any] +typing.Callable[[builtins.list[typing.Any], builtins.list[typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config, builtins.int, builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.list[typing.Any], builtins.list[typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config, builtins.list[typing.Any], builtins.int], typing.Any] +... +``` + +### OK: `def calc_obstacle_cost(trajectory, ob, config)` + +``` +typing.Callable[[builtins.dict[typing.Any, typing.Any], builtins.dict[typing.Any, typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[numpy.lib.arrayterator.Arrayterator[typing.Any, typing.Any], builtins.dict[typing.Any, typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.dict[typing.Any, typing.Any], numpy.lib.arrayterator.Arrayterator[typing.Any, typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[numpy.lib.arrayterator.Arrayterator[typing.Any, typing.Any], numpy.lib.arrayterator.Arrayterator[typing.Any, typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[numpy.ndarray[typing.Any, typing.Any], builtins.dict[typing.Any, typing.Any], PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +``` + +### OK: `def calc_to_goal_cost(trajectory, goal)` + +``` +typing.Callable[[builtins.dict[typing.Any, typing.Any], builtins.dict[typing.Any, typing.Any]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[typing.Any, ...], builtins.float], builtins.dict[builtins.float, builtins.float]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[builtins.float, ...], builtins.float], builtins.dict[builtins.float, builtins.float]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[builtins.int, ...], builtins.float], builtins.dict[builtins.float, builtins.float]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[typing.Any, ...], builtins.int], builtins.dict[builtins.float, builtins.float]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[builtins.float, ...], builtins.int], builtins.dict[builtins.float, builtins.float]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[builtins.int, ...], builtins.int], builtins.dict[builtins.float, builtins.float]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[typing.Any, ...], builtins.float], builtins.dict[builtins.int, builtins.float]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[typing.Any, ...], builtins.float], builtins.dict[builtins.float, builtins.int]], typing.Any] +typing.Callable[[numpy.ma.core.MaskedConstant, builtins.dict[typing.Any, typing.Any]], typing.Any] +typing.Callable[[builtins.dict[typing.Any, typing.Any], builtins.str], typing.Any] +typing.Callable[[numpy.ma.core.MaskedConstant, builtins.dict[builtins.float, builtins.float]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[builtins.float, ...], builtins.float], builtins.dict[builtins.float, builtins.int]], typing.Any] +typing.Callable[[builtins.dict[builtins.tuple[builtins.int, ...], builtins.float], builtins.dict[builtins.float, builtins.int]], typing.Any] +... +``` + +### OK: `def plot_robot(x, y, yaw, config)` + +`list` is strange here, but without stubs for matplotlib mypy doesn't consider this a mistake. + +``` +typing.Callable[[builtins.int, builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[typing.Any], builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.int, builtins.list[typing.Any], builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.int, builtins.list[builtins.int], builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.int], builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.list[builtins.list[typing.Any]], builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.int, builtins.int, builtins.bool, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +typing.Callable[[builtins.str, builtins.int, builtins.int, PathPlanning.DynamicWindowApproach.dynamic_window_approach.Config], typing.Any] +... +``` + +### EXTRA: `def dwa_control(x, config, goal, ob)` + +Found annotations (why?): +``` +typing.Callable[[builtins.object, builtins.object, builtins.object, builtins.object], typing.Any] +typing.Callable[[builtins.str, builtins.object, builtins.object, builtins.object], typing.Any] +typing.Callable[[builtins.object, builtins.str, builtins.object, builtins.object], typing.Any] +typing.Callable[[builtins.object, builtins.object, builtins.str, builtins.object], typing.Any] +typing.Callable[[builtins.object, builtins.object, builtins.object, builtins.str], typing.Any] +typing.Callable[[numpy.str_, builtins.object, builtins.object, builtins.object], typing.Any] +typing.Callable[[builtins.object, numpy.str_, builtins.object, builtins.object], typing.Any] +typing.Callable[[builtins.object, builtins.object, numpy.str_, builtins.object], typing.Any] +typing.Callable[[builtins.object, builtins.object, builtins.object, numpy.str_], typing.Any] +... +``` + +## File 2 + +Source: https://github.com/AtsushiSakai/PythonRobotics/blob/master/PathPlanning/DubinsPath/dubins_path_planner.py + +Top-level functions (with only positional arguments): +- [ ] `def _mod2pi(theta)` +- [x] `def _calc_trig_funcs(alpha, beta)` +- [x] `def _LSL(alpha, beta, d)` +- [x] `def _RSR(alpha, beta, d)` +- [ ] `def _LSR(alpha, beta, d)` +- [x] `def _RSL(alpha, beta, d)` +- [x] `def _RLR(alpha, beta, d)` +- [ ] `def _LRL(alpha, beta, d)` +- [x] `def _dubins_path_planning_from_origin(end_x, end_y, end_yaw, curvature, step_size, planning_funcs)` +- [x] `def _interpolate(length, mode, max_curvature, origin_x, origin_y, origin_yaw, path_x, path_y, path_yaw)` +- [x] `def _generate_local_course(lengths, modes, max_curvature, step_size)` + +Used time limit: 25 seconds. + +Command: + + java -jar utbot-cli-python-2023.02-SNAPSHOT.jar infer_types -p python3 -s /home/tochilinak/Documents/projects/utbot/PythonRobotics -t 25000 "/home/tochilinak/Documents/projects/utbot/PythonRobotics/PathPlanning/DubinsPath/dubins_path_planner.py" + +### OK: `def _calc_trig_funcs(alpha, beta)` + +``` +typing.Callable[[builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.bool, builtins.float], typing.Any] +typing.Callable[[builtins.float, builtins.bool], typing.Any] +typing.Callable[[builtins.int, builtins.float], typing.Any] +typing.Callable[[builtins.bool, builtins.bool], typing.Any] +typing.Callable[[builtins.float, builtins.int], typing.Any] +typing.Callable[[builtins.int, builtins.bool], typing.Any] +typing.Callable[[builtins.bool, builtins.int], typing.Any] +typing.Callable[[numpy.timedelta64, builtins.bool], typing.Any] +typing.Callable[[builtins.int, builtins.int], typing.Any] +typing.Callable[[builtins.bool, numpy.timedelta64], typing.Any] +typing.Callable[[enum.IntEnum, builtins.float], typing.Any] +... +``` + +### OK: `def _LSL(alpha, beta, d)` + +``` +typing.Callable[[builtins.float, builtins.float, builtins.int], typing.Any] +typing.Callable[[builtins.bool, builtins.float, builtins.int], typing.Any] +typing.Callable[[enum.IntEnum, builtins.float, builtins.int], typing.Any] +typing.Callable[[enum.auto, builtins.float, builtins.int], typing.Any] +typing.Callable[[enum.IntFlag, builtins.float, builtins.int], typing.Any] +typing.Callable[[_pytest.config.ExitCode, builtins.float, builtins.int], typing.Any] +typing.Callable[[signal.Signals, builtins.float, builtins.int], typing.Any] +typing.Callable[[signal.Handlers, builtins.float, builtins.int], typing.Any] +typing.Callable[[signal.Sigmasks, builtins.float, builtins.int], typing.Any] +typing.Callable[[builtins.complex, builtins.float, builtins.int], typing.Any] +``` + + +### OK: `def _RSR(alpha, beta, d)` + +``` +typing.Callable[[builtins.float, builtins.float, builtins.int], typing.Any] +typing.Callable[[builtins.float, builtins.int, builtins.int], typing.Any] +typing.Callable[[builtins.float, builtins.str, builtins.int], typing.Any] +typing.Callable[[builtins.float, builtins.bool, builtins.int], typing.Any] +typing.Callable[[builtins.float, enum.IntEnum, builtins.int], typing.Any] +typing.Callable[[builtins.float, enum.auto, builtins.int], typing.Any] +typing.Callable[[builtins.float, enum.IntFlag, builtins.int], typing.Any] +typing.Callable[[builtins.float, _pytest.config.ExitCode, builtins.int], typing.Any] +typing.Callable[[builtins.float, signal.Signals, builtins.int], typing.Any] +typing.Callable[[builtins.float, signal.Handlers, builtins.int], typing.Any] +typing.Callable[[builtins.float, signal.Sigmasks, builtins.int], typing.Any] +typing.Callable[[builtins.float, builtins.complex, builtins.int], typing.Any] +``` + +### OK: `def _RSL(alpha, beta, d)` + +``` +typing.Callable[[builtins.float, builtins.float, builtins.int], typing.Any] +typing.Callable[[builtins.float, builtins.float, builtins.bool], typing.Any] +typing.Callable[[builtins.float, builtins.float, enum.IntEnum], typing.Any] +typing.Callable[[builtins.float, builtins.float, enum.auto], typing.Any] +typing.Callable[[builtins.float, builtins.float, enum.IntFlag], typing.Any] +typing.Callable[[builtins.float, builtins.float, _pytest.config.ExitCode], typing.Any] +typing.Callable[[builtins.float, builtins.float, signal.Signals], typing.Any] +typing.Callable[[builtins.float, builtins.float, signal.Handlers], typing.Any] +typing.Callable[[builtins.float, builtins.float, signal.Sigmasks], typing.Any] +typing.Callable[[builtins.float, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.float, builtins.float, _decimal.Decimal], typing.Any] +typing.Callable[[builtins.float, builtins.float, builtins.complex], typing.Any] +``` + +### OK: `def _RLR(alpha, beta, d)` + +``` +typing.Callable[[builtins.float, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.bool, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.float, builtins.bool, builtins.float], typing.Any] +typing.Callable[[builtins.int, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.float, builtins.int, builtins.float], typing.Any] +typing.Callable[[enum.IntEnum, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.float, enum.IntEnum, builtins.float], typing.Any] +typing.Callable[[enum.auto, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.float, enum.auto, builtins.float], typing.Any] +typing.Callable[[enum.IntFlag, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.float, enum.IntFlag, builtins.float], typing.Any] +... +``` + +### OK: `def _dubins_path_planning_from_origin(end_x, end_y, end_yaw, curvature, step_size, planning_funcs)` + +``` +typing.Callable[[builtins.int, builtins.int, builtins.int, builtins.float, builtins.int, builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.bool, builtins.int, builtins.int, builtins.float, builtins.int, builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.int, builtins.bool, builtins.int, builtins.float, builtins.int, builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.int, builtins.int, builtins.bool, builtins.float, builtins.int, builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.int, builtins.int, builtins.int, builtins.float, builtins.list[typing.Any], builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.int, builtins.int, builtins.int, builtins.float, builtins.int, builtins.list[builtins.staticmethod[typing.Any]]], typing.Any] +typing.Callable[[builtins.int, builtins.int, builtins.int, builtins.float, builtins.int, builtins.list[builtins.staticmethod[builtins.list[typing.Any]]]], typing.Any] +typing.Callable[[builtins.int, builtins.bool, builtins.int, builtins.float, builtins.int, builtins.list[builtins.staticmethod[typing.Any]]], typing.Any] +typing.Callable[[builtins.float, builtins.int, builtins.int, builtins.float, builtins.int, builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.bool, builtins.bool, builtins.int, builtins.float, builtins.int, builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.int, builtins.bool, builtins.int, builtins.float, builtins.int, builtins.list[builtins.staticmethod[builtins.list[typing.Any]]]], typing.Any] +typing.Callable[[builtins.bool, builtins.int, builtins.int, builtins.float, builtins.int, builtins.list[builtins.staticmethod[typing.Any]]], typing.Any] +typing.Callable[[builtins.int, builtins.int, builtins.int, builtins.float, builtins.int, builtins.list[builtins.staticmethod[builtins.list[builtins.float]]]], typing.Any] +... +``` + + +### OK: `def _interpolate(length, mode, max_curvature, origin_x, origin_y, origin_yaw, path_x, path_y, path_yaw)` + +``` +typing.Callable[[builtins.float, builtins.str, builtins.float, builtins.float, builtins.float, builtins.float, builtins.list[typing.Any], builtins.list[typing.Any], builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.float, builtins.str, builtins.float, builtins.float, builtins.float, builtins.float, builtins.list[builtins.float], builtins.list[builtins.float], builtins.list[builtins.float]], typing.Any] +typing.Callable[[builtins.float, builtins.str, builtins.float, builtins.float, builtins.float, builtins.float, builtins.list[builtins.object], builtins.list[builtins.float], builtins.list[builtins.float]], typing.Any] +typing.Callable[[builtins.float, builtins.str, builtins.float, builtins.float, builtins.float, builtins.float, array.array[typing.Any], builtins.list[typing.Any], builtins.list[typing.Any]], typing.Any] +typing.Callable[[builtins.float, builtins.str, builtins.float, builtins.float, builtins.float, builtins.float, array.array[builtins.float], builtins.list[builtins.float], builtins.list[builtins.float]], typing.Any] +``` + +### OK: `def _generate_local_course(lengths, modes, max_curvature, step_size)` + +``` +typing.Callable[[builtins.list[typing.Any], builtins.list[typing.Any], builtins.int, builtins.int], typing.Any] +typing.Callable[[builtins.list[builtins.int], builtins.list[builtins.int], builtins.int, builtins.int], typing.Any] +typing.Callable[[builtins.list[typing.Any], builtins.list[typing.Any], builtins.list[typing.Any], builtins.int], typing.Any] +typing.Callable[[builtins.list[typing.Any], builtins.list[typing.Any], builtins.int, builtins.bool], typing.Any] +typing.Callable[[builtins.dict[typing.Any, typing.Any], builtins.list[typing.Any], builtins.int, builtins.int], typing.Any] +typing.Callable[[builtins.dict[builtins.int, builtins.int], builtins.list[builtins.int], builtins.int, builtins.int], typing.Any] +typing.Callable[[builtins.list[builtins.int], builtins.list[builtins.int], builtins.int, builtins.bool], typing.Any] +typing.Callable[[builtins.list[builtins.int], builtins.list[builtins.list[typing.Any]], builtins.int, builtins.int], typing.Any] +typing.Callable[[builtins.list[builtins.int], builtins.list[builtins.list[builtins.int]], builtins.int, builtins.int], typing.Any] +... +``` + + +### EXTRA: `def _LSR(alpha, beta, d)` + +``` +typing.Callable[[builtins.float, builtins.object, builtins.int], typing.Any] +typing.Callable[[builtins.object, builtins.float, builtins.int], typing.Any] +typing.Callable[[builtins.float, builtins.float, builtins.int], typing.Any] +typing.Callable[[builtins.bool, builtins.object, builtins.int], typing.Any] +typing.Callable[[builtins.int, builtins.float, builtins.int], typing.Any] +typing.Callable[[builtins.float, builtins.int, builtins.int], typing.Any] +typing.Callable[[enum.IntEnum, builtins.object, builtins.int], typing.Any] +typing.Callable[[builtins.bool, builtins.float, builtins.int], typing.Any] +typing.Callable[[builtins.int, builtins.str, builtins.int], typing.Any] +typing.Callable[[builtins.str, builtins.int, builtins.int], typing.Any] +typing.Callable[[builtins.float, builtins.bool, builtins.int], typing.Any] +typing.Callable[[enum.auto, builtins.object, builtins.int], typing.Any] +typing.Callable[[enum.IntEnum, builtins.float, builtins.int], typing.Any] +``` + +### EXTRA: `def _LRL(alpha, beta, d)` + +``` +typing.Callable[[builtins.float, builtins.object, builtins.float], typing.Any] +typing.Callable[[builtins.bool, builtins.object, builtins.float], typing.Any] +typing.Callable[[builtins.float, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.int, builtins.object, builtins.float], typing.Any] +typing.Callable[[builtins.bool, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.float, builtins.str, builtins.float], typing.Any] +typing.Callable[[enum.IntEnum, builtins.object, builtins.float], typing.Any] +typing.Callable[[builtins.int, builtins.float, builtins.float], typing.Any] +typing.Callable[[builtins.bool, builtins.str, builtins.float], typing.Any] +typing.Callable[[builtins.float, builtins.bool, builtins.float], typing.Any] +typing.Callable[[enum.auto, builtins.object, builtins.float], typing.Any] +typing.Callable[[enum.IntEnum, builtins.float, builtins.float], typing.Any] +... +``` + + +### OK or ERROR? : `def _mod2pi(theta)` + +``` +typing.Callable[[builtins.bool], typing.Any] +typing.Callable[[builtins.object], typing.Any] +typing.Callable[[builtins.int], typing.Any] +typing.Callable[[sys.UnraisableHookArgs], typing.Any] +typing.Callable[[pathlib.PurePath], typing.Any] +typing.Callable[[pathlib.PurePosixPath], typing.Any] +typing.Callable[[pathlib.PureWindowsPath], typing.Any] +typing.Callable[[pathlib.Path], typing.Any] +typing.Callable[[pathlib.PosixPath], typing.Any] +typing.Callable[[pathlib.WindowsPath], typing.Any] +typing.Callable[[builtins.function], typing.Any] +typing.Callable[[builtins.staticmethod[typing.Any]], typing.Any] +... +``` + + +## File 3 + +Source: https://github.com/AtsushiSakai/PythonRobotics/blob/master/utils/angle.py + +Top-level functions: +- [ ] `def rot_mat_2d(angle)` +- [ ] `def angle_mod(x, zero_2_2pi, degree)` + +### EXTRA: `def rot_mat_2d(angle)` + +Incorrect annotations: +``` +typing.Callable[[builtins.object], typing.Any] +typing.Callable[[builtins.int], typing.Any] +typing.Callable[[builtins.str], typing.Any] +typing.Callable[[numpy.str_], typing.Any] +typing.Callable[[builtins.bool], typing.Any] +typing.Callable[[enum.IntEnum], typing.Any] +typing.Callable[[enum.auto], typing.Any] +typing.Callable[[enum.IntFlag], typing.Any] +typing.Callable[[signal.Signals], typing.Any] +typing.Callable[[signal.Handlers], typing.Any] +typing.Callable[[signal.Sigmasks], typing.Any] +typing.Callable[[argparse.FileType], typing.Any] +typing.Callable[[_operator.methodcaller], typing.Any] +typing.Callable[[builtins.function], typing.Any] +typing.Callable[[builtins.staticmethod[typing.Any]], typing.Any] +... +``` + +### FAIL (TODO): `def angle_mod(x, zero_2_2pi, degree)` + +??? + +## File 4 + +!!!!!!!!!!!! https://github.com/AllAlgorithms/python/blob/master/algorithms/dynamic-programming/kadanes_algorithm.py diff --git a/utbot-python/samples/run_tests.py b/utbot-python/samples/run_tests.py new file mode 100644 index 0000000000..c426fed41c --- /dev/null +++ b/utbot-python/samples/run_tests.py @@ -0,0 +1,209 @@ +""" +Example command + run_tests.py generate -c test_configuration.json + -p -o + + run_tests.py run -p -t + -c +""" +import argparse +import contextlib +import json +import os +import shutil +from subprocess import Popen, PIPE +import sys +import threading +import typing +import tqdm +from tqdm.contrib import DummyTqdmFile +import pathlib + + +def parse_arguments(): + parser = argparse.ArgumentParser( + prog="UtBot Python test", description="Generate tests for example files" + ) + subparsers = parser.add_subparsers(dest="command") + parser_generate = subparsers.add_parser("generate", help="Generate tests") + parser_generate.add_argument("java") + parser_generate.add_argument("jar") + parser_generate.add_argument("path_to_test_dir") + parser_generate.add_argument("-c", "--config_file", required=True) + parser_generate.add_argument("-p", "--python_path", required=True) + parser_generate.add_argument("-o", "--output_dir", required=True) + parser_generate.add_argument("-i", "--coverage_output_dir", required=True) + parser_run = subparsers.add_parser("run", help="Run tests") + parser_run.add_argument("-p", "--python_path", required=True) + parser_run.add_argument("-t", "--test_directory", required=True) + parser_run.add_argument("-c", "--code_directory", required=True) + parser_coverage = subparsers.add_parser("check_coverage", help="Check coverage") + parser_coverage.add_argument("-i", "--coverage_output_dir", required=True) + parser_coverage.add_argument("-c", "--config_file", required=True) + return parser.parse_args() + + +def parse_config(config_path: str): + with open(config_path, "r") as fin: + return json.loads(fin.read()) + + +@contextlib.contextmanager +def std_out_err_redirect_tqdm(): + orig_out_err = sys.stdout, sys.stderr + try: + sys.stdout, sys.stderr = map(DummyTqdmFile, orig_out_err) + yield orig_out_err[0] + # Relay exceptions + except Exception as exc: + raise exc + # Always restore sys.stdout/err if necessary + finally: + sys.stdout, sys.stderr = orig_out_err + + +def generate_tests( + java: str, + jar_path: str, + sys_paths: list[str], + python_path: str, + file_under_test: str, + timeout: int, + output: str, + coverage_output: str, + class_names: typing.Optional[list[str]] = None, + method_names: typing.Optional[list[str]] = None, +): + command = f"{java} -jar {jar_path} generate_python {file_under_test}.py -p {python_path} -o {output} -s {' '.join(sys_paths)} --timeout {timeout * 1000} --install-requirements --runtime-exception-behaviour PASS --coverage={coverage_output}" + if class_names is not None: + command += f" -c {','.join(class_names)}" + if method_names is not None: + command += f" -m {','.join(method_names)}" + tqdm.tqdm.write("\n" + command) + + def stdout_printer(p): + for line in p.stdout: + tqdm.tqdm.write(line.rstrip().decode()) + + p = Popen(command.split(), stdout=PIPE) + t = threading.Thread(target=stdout_printer, args=(p,)) + t.run() + + +def run_tests( + python_path: str, + tests_dir: str, + samples_dir: str, +): + command = f'{python_path} -m coverage run --source={samples_dir} -m unittest discover -p "utbot_*" {tests_dir}' + tqdm.tqdm.write(command) + code = os.system(command) + return code + + +def check_coverage( + config_file: str, + coverage_output_dir: str, +): + config = parse_config(config_file) + report: typing.Dict[str, bool] = {} + coverage: typing.Dict[str, typing.Tuple[float, float]] = {} + for part in config["parts"]: + for file in part["files"]: + for i, group in enumerate(file["groups"]): + if i > 0: + suffix = f"_{i}" + else: + suffix = "" + + expected_coverage = group.get("coverage", 0) + + file_suffix = f"{part['path'].replace('/', '_')}_{file['name']}{suffix}" + coverage_output_file = pathlib.Path( + coverage_output_dir, f"coverage_{file_suffix}.json" + ) + if coverage_output_file.exists(): + with open(coverage_output_file, "rt") as fin: + actual_coverage_json = json.loads(fin.readline()) + actual_covered = sum( + lines["end"] - lines["start"] + 1 + for lines in actual_coverage_json["covered"] + ) + actual_not_covered = sum( + lines["end"] - lines["start"] + 1 + for lines in actual_coverage_json["notCovered"] + ) + if actual_covered + actual_not_covered == 0: + actual_coverage = 0 + else: + actual_coverage = round( + actual_covered / (actual_not_covered + actual_covered) * 100 + ) + else: + actual_coverage = 0 + + coverage[file_suffix] = (actual_coverage, expected_coverage) + report[file_suffix] = actual_coverage >= expected_coverage + if all(report.values()): + return True + print("-------------") + print("Bad coverage:") + print("-------------") + for file, good_coverage in report.items(): + if not good_coverage: + print(f"{file}: {coverage[file][0]}/{coverage[file][1]}") + return False + + +def main_test_generation(args): + config = parse_config(args.config_file) + if pathlib.Path(args.coverage_output_dir).exists(): + shutil.rmtree(args.coverage_output_dir) + with std_out_err_redirect_tqdm() as orig_stdout: + for part in tqdm.tqdm( + config["parts"], file=orig_stdout, dynamic_ncols=True, desc="Progress" + ): + for file in tqdm.tqdm( + part["files"], file=orig_stdout, dynamic_ncols=True, desc=part["path"] + ): + for i, group in enumerate(file["groups"]): + if i > 0: + suffix = f"_{i}" + else: + suffix = "" + + full_name = pathlib.PurePath( + args.path_to_test_dir, part["path"], file["name"] + ) + output_file = pathlib.PurePath( + args.output_dir, + f"utbot_tests_{part['path'].replace('/', '_')}_{file['name']}{suffix}.py", + ) + coverage_output_file = pathlib.PurePath( + args.coverage_output_dir, + f"coverage_{part['path'].replace('/', '_')}_{file['name']}{suffix}.json", + ) + generate_tests( + args.java, + args.jar, + [args.path_to_test_dir], + args.python_path, + str(full_name), + group["timeout"], + str(output_file), + str(coverage_output_file), + group["classes"], + group["methods"], + ) + + +if __name__ == "__main__": + arguments = parse_arguments() + if arguments.command == "generate": + main_test_generation(arguments) + elif arguments.command == "run": + run_tests( + arguments.python_path, arguments.test_directory, arguments.code_directory + ) + elif arguments.command == "check_coverage": + check_coverage(arguments.config_file, arguments.coverage_output_dir) diff --git a/utbot-python/samples/run_tests_all.sh b/utbot-python/samples/run_tests_all.sh new file mode 100755 index 0000000000..a22828dd4f --- /dev/null +++ b/utbot-python/samples/run_tests_all.sh @@ -0,0 +1,9 @@ +python_path=$1 + +rm -r utbot_coverage utbot_tests RUN_RESULT COVERAGE_RESULT +mkdir utbot_coverage +$python_path run_tests.py generate java ../../utbot-cli-python/build/libs/utbot-cli-python*.jar `pwd` -c test_configuration.json -p $python_path -o utbot_tests -i utbot_coverage + +$python_path run_tests.py run -p $python_path -c test_configuration.json -t utbot_tests > RUN_RESULT 2> RUN_RESULT + +$python_path run_tests.py check_coverage -i utbot_coverage/ -c test_configuration.json > COVERAGE_RESULT 2> COVERAGE_RESULT diff --git a/utbot-python/samples/samples/__init__.py b/utbot-python/samples/samples/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/algorithms/__init__.py b/utbot-python/samples/samples/algorithms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/algorithms/bfs.py b/utbot-python/samples/samples/algorithms/bfs.py new file mode 100644 index 0000000000..727bcb7ba2 --- /dev/null +++ b/utbot-python/samples/samples/algorithms/bfs.py @@ -0,0 +1,42 @@ +from __future__ import annotations +from collections import deque +from typing import List + + +class Node: + def __init__(self, name: str, children: List[Node]): + self.name = name + self.children = children + + def __repr__(self): + return f'' + + def __eq__(self, other): + if isinstance(other, Node): + return self.name == other.name + else: + return False + + +def bfs(nodes: List[Node]): + if len(nodes) == 0: + return [] + + visited = [] + queue = deque(nodes) + while len(queue) > 0: + node = queue.pop() + if node not in visited: + visited.append(node) + for child in node.children: + queue.append(child) + return visited + + +if __name__ == '__main__': + a = Node('a', []) + b = Node('b', []) + c = Node('c', []) + a.children.append(b) + b.children.append(c) + print(bfs([a, b, c])) diff --git a/utbot-python/samples/samples/algorithms/floyd_warshall.py b/utbot-python/samples/samples/algorithms/floyd_warshall.py new file mode 100644 index 0000000000..a85e042104 --- /dev/null +++ b/utbot-python/samples/samples/algorithms/floyd_warshall.py @@ -0,0 +1,42 @@ +import math + + +class Graph: + def __init__(self, n=0): # a graph with Node 0,1,...,N-1 + self.n = n + self.w = [ + [math.inf for j in range(0, n)] for i in range(0, n) + ] # adjacency matrix for weight + self.dp = [ + [math.inf for j in range(0, n)] for i in range(0, n) + ] # dp[i][j] stores minimum distance from i to j + + def add_edge(self, u, v, w): + self.dp[u][v] = w + + def floyd_warshall(self): + for k in range(0, self.n): + for i in range(0, self.n): + for j in range(0, self.n): + self.dp[i][j] = min(self.dp[i][j], self.dp[i][k] + self.dp[k][j]) + + def show_min(self, u, v): + return self.dp[u][v] + + +if __name__ == "__main__": + graph = Graph(5) + graph.add_edge(0, 2, 9) + graph.add_edge(0, 4, 10) + graph.add_edge(1, 3, 5) + graph.add_edge(2, 3, 7) + graph.add_edge(3, 0, 10) + graph.add_edge(3, 1, 2) + graph.add_edge(3, 2, 1) + graph.add_edge(3, 4, 6) + graph.add_edge(4, 1, 3) + graph.add_edge(4, 2, 4) + graph.add_edge(4, 3, 9) + graph.floyd_warshall() + graph.show_min(1, 4) + graph.show_min(0, 3) \ No newline at end of file diff --git a/utbot-python/samples/samples/algorithms/longest_subsequence.py b/utbot-python/samples/samples/algorithms/longest_subsequence.py new file mode 100644 index 0000000000..1200f8fb5a --- /dev/null +++ b/utbot-python/samples/samples/algorithms/longest_subsequence.py @@ -0,0 +1,27 @@ +from typing import List + + +def longest_subsequence(array: List[int]) -> List[int]: + array_length = len(array) + if array_length <= 1: + return array + pivot = array[0] + is_found = False + i = 1 + longest_subseq: List[int] = [] + while not is_found and i < array_length: + if array[i] < pivot: + is_found = True + temp_array = [element for element in array[i:] if element >= array[i]] + temp_array = longest_subsequence(temp_array) + if len(temp_array) > len(longest_subseq): + longest_subseq = temp_array + else: + i += 1 + + temp_array = [element for element in array[1:] if element >= pivot] + temp_array = [pivot] + longest_subsequence(temp_array) + if len(temp_array) > len(longest_subseq): + return temp_array + else: + return longest_subseq diff --git a/utbot-python/samples/samples/algorithms/quick_sort.py b/utbot-python/samples/samples/algorithms/quick_sort.py new file mode 100644 index 0000000000..a58f6d3bea --- /dev/null +++ b/utbot-python/samples/samples/algorithms/quick_sort.py @@ -0,0 +1,32 @@ +import random +from typing import List + + +def quick_sort(array: List[int]): + def partition(A, left_index, right_index): + pivot = A[left_index] + i = left_index + 1 + for j in range(left_index + 1, right_index): + if A[j] < pivot: + A[j], A[i] = A[i], A[j] + i += 1 + A[left_index], A[i - 1] = A[i - 1], A[left_index] + return i - 1 + + def quick_sort_random(A, left, right): + if left < right: + pivot = random.randint(left, right - 1) + A[pivot], A[left] = ( + A[left], + A[pivot], + ) # switches the pivot with the left most bound + pivot_index = partition(A, left, right) + quick_sort_random( + A, left, pivot_index + ) # recursive quicksort to the left of the pivot point + quick_sort_random( + A, pivot_index + 1, right + ) # recursive quicksort to the right of the pivot point + quick_sort_random(array, 0, len(array)) + return array + diff --git a/utbot-python/samples/samples/classes/__init__.py b/utbot-python/samples/samples/classes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/classes/constructors.py b/utbot-python/samples/samples/classes/constructors.py new file mode 100644 index 0000000000..aac8cd71c1 --- /dev/null +++ b/utbot-python/samples/samples/classes/constructors.py @@ -0,0 +1,36 @@ +class EmptyClass: + pass + + +class WithInitClass: + def __init__(self, x: int): + self.x = x + + +class EmptyInitClass: + def __init__(self): + pass + + +class BasicNewCass: + def __new__(cls, *args, **kwargs): + super().__new__(cls, *args, **kwargs) + + +class ParentEmptyInitClass(EmptyClass): + pass + + +class ParentWithInitClass(WithInitClass): + pass + + +class ParentEmptyClass(EmptyClass): + pass + + +def func(a: EmptyClass, b: WithInitClass, c: EmptyInitClass, d: BasicNewCass, e: ParentEmptyClass, + f: ParentWithInitClass, g: ParentEmptyInitClass): + return a.__class__.__name__ + str( + b.x) + c.__class__.__name__ + d.__class__.__name__ + e.__class__.__name__ + str( + f.x) + g.__class__.__name__ diff --git a/utbot-python/samples/samples/classes/dataclass.py b/utbot-python/samples/samples/classes/dataclass.py new file mode 100644 index 0000000000..0fd03ee182 --- /dev/null +++ b/utbot-python/samples/samples/classes/dataclass.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class C: + counter: int = 0 + + def inc(self): + self.counter += 1 diff --git a/utbot-python/samples/samples/classes/dicts.py b/utbot-python/samples/samples/classes/dicts.py new file mode 100644 index 0000000000..3b0e25a549 --- /dev/null +++ b/utbot-python/samples/samples/classes/dicts.py @@ -0,0 +1,35 @@ +from typing import List, Dict, Optional + + +class Word: + def __init__(self, translations: Dict[str, str]): + self.translations = translations + + def __eq__(self, other): + return self.translations == other.translations + + def keys(self): + return list(self.translations.keys()) + + +class Dictionary: + def __init__( + self, + languages: List[str], + words: List[Dict[str, str]], + ): + self.languages = languages + self.words = [Word(translations) for translations in words] + + def __eq__(self, other): + return self.languages == other.languages and self.words == other.words + + def translate(self, word: str, language: Optional[str]): + if language is not None: + for word_ in self.words: + if word_.translations[language] == word: + return word_ + else: + for word_ in self.words: + if word in word_.translations.values(): + return word_ diff --git a/utbot-python/samples/samples/classes/easy_class.py b/utbot-python/samples/samples/classes/easy_class.py new file mode 100644 index 0000000000..2e3d679714 --- /dev/null +++ b/utbot-python/samples/samples/classes/easy_class.py @@ -0,0 +1,9 @@ +class B: + def __init__(self, val: complex): + self.description = val + + def __eq__(self, other): + return self.description == other.description + + def sqrt(self): + return self.description ** 0.5 \ No newline at end of file diff --git a/utbot-python/samples/samples/classes/equals.py b/utbot-python/samples/samples/classes/equals.py new file mode 100644 index 0000000000..6ed34ec5fa --- /dev/null +++ b/utbot-python/samples/samples/classes/equals.py @@ -0,0 +1,23 @@ +class WithEqual: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + +class WithoutEqual: + def __init__(self, x: int): + self.x = x + + +class WithoutEqualChild(WithoutEqual): + def __init__(self, x: int): + super().__init__(x) + + def __eq__(self, other): + return self.x == other.x + + +def f1(a: WithEqual, b: WithoutEqual, c: WithoutEqualChild): + return a.x + b.x + c.x diff --git a/utbot-python/samples/samples/classes/inner_class.py b/utbot-python/samples/samples/classes/inner_class.py new file mode 100644 index 0000000000..e77d866b1a --- /dev/null +++ b/utbot-python/samples/samples/classes/inner_class.py @@ -0,0 +1,12 @@ +class Outer: + class Inner: + a = 1 + + def inc(self): + self.a += 1 + + def __init__(self): + self.inner = Outer.Inner() + + def inc1(self): + self.inner.inc() \ No newline at end of file diff --git a/utbot-python/samples/samples/classes/rename_self.py b/utbot-python/samples/samples/classes/rename_self.py new file mode 100644 index 0000000000..b4081820fe --- /dev/null +++ b/utbot-python/samples/samples/classes/rename_self.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + + +@dataclass +class Inner: + x: int + + +class A: + def __init__(self, x: Inner): + self.x = x + + def f(cls, self, x: Inner): + self.x += 1 + return cls.x.x, self, x diff --git a/utbot-python/samples/samples/classes/setstate_test.py b/utbot-python/samples/samples/classes/setstate_test.py new file mode 100644 index 0000000000..85dcb32e87 --- /dev/null +++ b/utbot-python/samples/samples/classes/setstate_test.py @@ -0,0 +1,20 @@ +import typing + + +class MyClass: + __slots__ = ['a', 'b'] + + def __init__(self, a: int, b: typing.Dict[int, str]): + self.a = a + self.b = b + + def __eq__(self, other): + return self.a == other.a and self.b == other.b + + def __setstate__(self, state): + self.a = state[1]['a'] + self.b = state[1]['b'] + + +def make_my_class(a: int, b: typing.Dict[int, str]) -> MyClass: + return MyClass(a, b) diff --git a/utbot-python/samples/samples/collection/__init__.py b/utbot-python/samples/samples/collection/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/collection/dicts.py b/utbot-python/samples/samples/collection/dicts.py new file mode 100644 index 0000000000..52651e8893 --- /dev/null +++ b/utbot-python/samples/samples/collection/dicts.py @@ -0,0 +1,24 @@ +import typing + + +class MyClass: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + def __hash__(self): + return hash(self.x) + + +def f(x: typing.Dict[int, int]): + if len(x) == 0: + return "Empty!" + return len(x) + + +def g(x: typing.Dict[MyClass, int]): + if len(x) == 0: + return "Empty!" + return len(x) diff --git a/utbot-python/samples/samples/collection/lists.py b/utbot-python/samples/samples/collection/lists.py new file mode 100644 index 0000000000..005f16401b --- /dev/null +++ b/utbot-python/samples/samples/collection/lists.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +import datetime +from typing import List + + +@dataclass +class Article: + title: str + author: str + content: str + created_at: datetime.datetime + + +def find_articles_with_author(articles: List[Article], author: str) -> List[Article]: + return [ + article for article in articles + if article.author == author + ] + + +def f(x: List[int]): + if len(x) == 0: + return "Empty!" + return sum(x) + + +if __name__ == '__main__': + print(find_articles_with_author([ + Article('a', 'a1', 'jfls', datetime.datetime.today()), + Article('b', 'a2', 'fjls', datetime.datetime.now()) + ], 'a1')) diff --git a/utbot-python/samples/samples/collection/recursive.py b/utbot-python/samples/samples/collection/recursive.py new file mode 100644 index 0000000000..5d2026f111 --- /dev/null +++ b/utbot-python/samples/samples/collection/recursive.py @@ -0,0 +1,19 @@ +import typing + + +def make_recursive_list(x: int): + xs = [1, 2] + xs.append(xs) + xs.append(x) + return xs + + +def make_recursive_dict(x: int, y: int): + d = {1: 2} + d[x] = d + d[y] = x + return d + + +if __name__ == '__main__': + make_recursive_dict(3, 4) \ No newline at end of file diff --git a/utbot-python/samples/samples/collection/sets.py b/utbot-python/samples/samples/collection/sets.py new file mode 100644 index 0000000000..2913fbcc58 --- /dev/null +++ b/utbot-python/samples/samples/collection/sets.py @@ -0,0 +1,24 @@ +import typing + + +class MyClass: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + def __hash__(self): + return hash(self.x) + + +def f(x: typing.Set[int]): + if len(x) == 0: + return "Empty!" + return len(x) + + +def g(x: typing.Set[MyClass]): + if len(x) == 0: + return "Empty!" + return len(x) diff --git a/utbot-python/samples/samples/collection/tuples.py b/utbot-python/samples/samples/collection/tuples.py new file mode 100644 index 0000000000..2ea141806e --- /dev/null +++ b/utbot-python/samples/samples/collection/tuples.py @@ -0,0 +1,36 @@ +import typing + + +class MyClass: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + def __hash__(self): + return hash(self.x) + + +def f(x: typing.Tuple[int, ...]): + if len(x) == 0: + return "Empty!" + return len(x) + + +def f1(x: typing.Tuple[int, int, int]): + if len(x) != 3: + return "Very bad input!!!" + return x[0] + x[1] + x[2] + + +def g(x: typing.Tuple[MyClass, ...]): + if len(x) == 0: + return "Empty!" + return len(x) + + +def g1(x: typing.Tuple[MyClass, MyClass]): + if len(x) != 2: + return "Very bad input!!!" + return x[0].x + x[1].x diff --git a/utbot-python/samples/samples/collection/using_collections.py b/utbot-python/samples/samples/collection/using_collections.py new file mode 100644 index 0000000000..e8a5e9e1ae --- /dev/null +++ b/utbot-python/samples/samples/collection/using_collections.py @@ -0,0 +1,17 @@ +import collections + + +def generate_collections(collection): + collection[0] = 100 + if isinstance(collection, collections.UserDict): + return collection.data + elements = list(collection.items()) + return [ + collection, + collections.Counter(collection), + elements + ] + + +if __name__ == '__main__': + print(generate_collections({1: 2})) diff --git a/utbot-python/samples/samples/controlflow/__init__.py b/utbot-python/samples/samples/controlflow/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/controlflow/arithmetic.py b/utbot-python/samples/samples/controlflow/arithmetic.py new file mode 100644 index 0000000000..6f26aa94f4 --- /dev/null +++ b/utbot-python/samples/samples/controlflow/arithmetic.py @@ -0,0 +1,17 @@ +import math + + +def calculate_function_value(x, y): + """ + Calculate value `f` + | sqrt(x - 2y) , x > 100 + f(x, y) = | (3x^2 - 2xy + y^2) / sin(x) , -100 < x <= 100 + | (0.01 * x) ^ log2(y) , x < -100 + """ + + if x > 100: + return math.sqrt(x - 2 * y) + elif -100 < x <= 100: + return (3*x**2 - 2*x*y + y**2) / math.sin(x) + else: + return (0.01 * x) ** math.log2(y) diff --git a/utbot-python/samples/samples/controlflow/conditions.py b/utbot-python/samples/samples/controlflow/conditions.py new file mode 100644 index 0000000000..3897f578b1 --- /dev/null +++ b/utbot-python/samples/samples/controlflow/conditions.py @@ -0,0 +1,18 @@ +def f(x): + if x == 10: + return 100 + if x == 20: + return 200 + if x == 30: + return 300 + if x == 40: + return 400 + if x == 50: + return 500 + if x == 60: + return 600 + if x < 0: + return x ** 2 + if x > 0: + return 239 + return 100500 diff --git a/utbot-python/samples/samples/controlflow/inner_conditions.py b/utbot-python/samples/samples/controlflow/inner_conditions.py new file mode 100644 index 0000000000..9a2740abfb --- /dev/null +++ b/utbot-python/samples/samples/controlflow/inner_conditions.py @@ -0,0 +1,12 @@ +def hard_function(x): + if x % 100 == 0: + return 1 + elif x + 100 < 400: + return 2 + else: + if x == complex(1, 2): + return x + elif len(str(x)) > 3: + return 3 + else: + return 4 diff --git a/utbot-python/samples/samples/controlflow/multi_conditions.py b/utbot-python/samples/samples/controlflow/multi_conditions.py new file mode 100644 index 0000000000..b7ba4bce58 --- /dev/null +++ b/utbot-python/samples/samples/controlflow/multi_conditions.py @@ -0,0 +1,14 @@ +def check_interval(x: float, left: float, right: float) -> str: + if left < x < right or right < x < left: + return "between" + elif x < left and x < right: + return "less" + elif x > left and x > right: + return "more" + elif left == right: + return "all equals" + elif x == left: + return "left" + elif x == right: + return "right" + return "what?" diff --git a/utbot-python/samples/samples/easy_samples/__init__.py b/utbot-python/samples/samples/easy_samples/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/easy_samples/deep_equals.py b/utbot-python/samples/samples/easy_samples/deep_equals.py new file mode 100644 index 0000000000..ee3eeeacd4 --- /dev/null +++ b/utbot-python/samples/samples/easy_samples/deep_equals.py @@ -0,0 +1,79 @@ +from __future__ import annotations +from typing import List + + +class ComparableClass: + def __init__(self, x): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + +class BadClass: + def __init__(self, x): + self.x = x + + +def return_bad_class(x: int): + return BadClass(x) + + +def return_comparable_class(x: int): + return ComparableClass(x) + + +def primitive_list(x: int): + return [x] * 10 + + +def primitive_set(x: int): + return set(x+i for i in range(5)) + + +def primitive_dict(x: str, y: int): + return {x: y} + + +def comparable_list(length: int): + return [ComparableClass(x) for x in range(min(length, 10))] + + +def bad_list(length: int): + return [BadClass(x) for x in range(min(length, 10))] + + +class Node: + name: str + children: List[Node] + + def __init__(self, name: str): + self.name = name + self.children = [] + + def __str__(self): + return f'' + + def __eq__(self, other): + if isinstance(other, Node): + return self.name == other.name + else: + return False + + +def cycle(x: str): + a = Node(x + '_a') + b = Node(x + '_b') + a.children.append(b) + b.children.append(a) + return a + + +def cycle2(x: str): + a = Node(x + '_a') + b = Node(x + '_b') + c = Node(x + '_c') + a.children.append(b) + b.children.append(c) + c.children.append(a) + return a diff --git a/utbot-python/samples/samples/easy_samples/deep_equals_2.py b/utbot-python/samples/samples/easy_samples/deep_equals_2.py new file mode 100644 index 0000000000..f34cb42c5b --- /dev/null +++ b/utbot-python/samples/samples/easy_samples/deep_equals_2.py @@ -0,0 +1,22 @@ +class ComparableClass: + def __init__(self, x): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + +class IncomparableClass: + def __init__(self, x): + self.x = x + + def __eq__(self, other): + return id(self) == id(other) + + +def comparable_list(length: int): + return [ComparableClass(x) for x in range(min(length, 10))] + + +def incomparable_list(length: int): + return [IncomparableClass(x) for x in range(min(length, 10))] diff --git a/utbot-python/samples/samples/easy_samples/dummy_with_eq.py b/utbot-python/samples/samples/easy_samples/dummy_with_eq.py new file mode 100644 index 0000000000..05ef7b822e --- /dev/null +++ b/utbot-python/samples/samples/easy_samples/dummy_with_eq.py @@ -0,0 +1,9 @@ +class Dummy: + def __init__(self, value: int): + self.field = value + + def __eq__(self, other): + return self.field == other.field + + def propagate(self): + return [self, self] diff --git a/utbot-python/samples/samples/easy_samples/dummy_without_eq.py b/utbot-python/samples/samples/easy_samples/dummy_without_eq.py new file mode 100644 index 0000000000..8df59b68d2 --- /dev/null +++ b/utbot-python/samples/samples/easy_samples/dummy_without_eq.py @@ -0,0 +1,3 @@ +class Dummy: + def propagate(self): + return [self, self] diff --git a/utbot-python/samples/samples/easy_samples/fully_annotated.py b/utbot-python/samples/samples/easy_samples/fully_annotated.py new file mode 100644 index 0000000000..15854099d1 --- /dev/null +++ b/utbot-python/samples/samples/easy_samples/fully_annotated.py @@ -0,0 +1,78 @@ +import heapq +from datetime import datetime +from typing import Any, List, Union, NoReturn + +""" +Default functions suite: fully annotated. +""" + + +def id_(x: Any) -> Any: + return x + + +def compare_with_5(x: int) -> bool: + return x > 5 + + +def add(x: int, y: int) -> int: + return x + y + + +def add_with_unused_param(x: int, y: int, unused: int) -> int: + return x + y + + +def append_exclamation_mark(s: str) -> str: + return s + "!" + + +def append_two_ints_to_typing_list(l: List[int]) -> List[int]: + return l + [1, -1] + + +def append_two_ints_to_builtin_list(l: list) -> list: + return l + [1, -1] + + +def append_ints_and_chars(l: List[Union[int, str]]) -> List[Union[int, str]]: + return l + [1, -1] + list("ab") + + +def format_data_labels(dates: List[datetime]) -> List[str]: + if all(x.hour == 0 and x.minute == 0 for x in dates): + return [x.strftime('%Y-%m-%d') for x in dates] + else: + return [x.strftime('%H:%M') for x in dates] + + +class ClassWithIntField: + def __init__(self, int_field_value): + self.int_field = int_field_value + + +class ClassWithAnnotatedIntField: + def __init__(self, int_field: int): + self.int_field = int_field + + +def inc_int_field(c: ClassWithIntField) -> int: + c.int_field += 1 + return c.int_field + + +def call_heapify(ints: List[int]) -> List[int]: + heapq.heapify(ints) + return ints + + +def concatenate_args(*args: str) -> str: + return "+".join(args) + + +def concatenate_args_and_kwargs(*args: str, **kwargs: str) -> str: + return "+".join(args) + ";" + "?".join(kwargs.values()) + + +def raise_exception(exc: Exception) -> NoReturn: + raise exc diff --git a/utbot-python/samples/samples/easy_samples/general.py b/utbot-python/samples/samples/easy_samples/general.py new file mode 100644 index 0000000000..40630c2550 --- /dev/null +++ b/utbot-python/samples/samples/easy_samples/general.py @@ -0,0 +1,200 @@ +import collections +import heapq +import typing +from socket import socket +from typing import List, Dict, Set, Optional, AbstractSet +from dataclasses import dataclass + + +class Dummy: + def propagate(self): + return [self, self] + + @staticmethod + def abs(x): + return abs(x) + + +def dict_f(x, a, b, c): + y = {1: 2} + if x == y: + return 1 + x = a + x = b + x = c + return 2 + + +def fact(n): + ans = 1 + for i in range(1, n + 1): + ans *= i + return ans + + +def empty_(): + pass + + +def conditions(x): + if x % 100 == 0: + return 1 + elif x + 100 < 400: + return 2 + else: + if x == complex(1, 2): + return x.real + elif len(str(x)) > 3: + return 3 + else: + return 4 + + +def test_call(x): + return repr_test(x) + + +def zero_division(x): + return x / x + + +def repr_test(x): + x *= 100 + return [1, x + 1, collections.UserList([1, 2, 3]), collections.Counter("flkafksdf"), collections.OrderedDict({1: 2, 4: "jflas"})] + + +def str_test(x): + x += '1"23' + x += "flskjd'jfslk" + if len(x.split('.')) == 1: + return '1"23' + else: + return """100''500""" + + +def return_socket(x: int): + return socket() + + +def empty(): + return 1 + + +def id_(x: AbstractSet[int]): + return x + + +def f(x, y, z, a, c, d, e, g, h, i): + if y % 2 == 0: + x = 1 + y + z += "aba" + a += [2] + list("str") + i + A = c < "abc" + B = "abc" == d + e = {1, 2, 3} + C = g == {1: 2} + h += int("777") + return x + y + + +def g(x: List[int], y: List): + y[0] += 1 + return x, y + + +def i(x: Dict[int, int]): + return x[0] + + +def j(x: Set[int]): + return x + + +def h(x): + if x < 123: + return 1 + return 2 + + +def a(x): + x.description += 1 + return x.description + + +def sqrt(x): + return x.sqrt() + + +@dataclass +class InventoryItem: + name: str + + +def inv(x): + return x.name + "aba" # interesting case with io.BytesIO + + +def b(x, y): + y = len(x) + return bytes(x, 'utf-8') + + +def c(x): + return heapq.heapify(x) + + +def d(x: Optional[int]): + return x + + +def k(x: typing.Any): + if x == complex(1): + return x + + +def constants(x): + if x == 1e5: + return "one" + elif (x > 1e4 - 2) and (x < 1e4): + return "two" + else: + return "three" + + +# interesting case with sets +def get_data_labels(dates): + if len(dates) == 0: + return None + if all(x.hour == 0 and x.minute == 0 for x in dates): + return [x.strftime('%Y-%m-%d') for x in dates] + else: + return [x.strftime('%H:%M') for x in dates] + +""" +# bad function +def m(x): + x = frozenset() + return len(x + 1) + + +# very bad function +def n(x, y): + y = (-x) + 1 + x *= 10 + # z = -x + print(x) + x = len([1]) + if y == len([1]): + y += print() + return x.description +""" + + +def list_of_list(x: List[List[InventoryItem]]): + return x + + +def make_tuple(x: list[int]): + if len(x) == 0: + return tuple() + return tuple(x) diff --git a/utbot-python/samples/samples/easy_samples/long_function_coverage.py b/utbot-python/samples/samples/easy_samples/long_function_coverage.py new file mode 100644 index 0000000000..64cbccc0c0 --- /dev/null +++ b/utbot-python/samples/samples/easy_samples/long_function_coverage.py @@ -0,0 +1,11 @@ +import time + + +def long_function(x: int): + x += 4 + x /= 2 + x += 100 + x *= 3 + x -= 15 + time.sleep(2000) + return x diff --git a/utbot-python/samples/samples/easy_samples/my_func.py b/utbot-python/samples/samples/easy_samples/my_func.py new file mode 100644 index 0000000000..27b63c830b --- /dev/null +++ b/utbot-python/samples/samples/easy_samples/my_func.py @@ -0,0 +1,6 @@ +def my_func(x: int, xs: list[int]): + if len(xs) == x: + return x ** 2 + elif not xs: + return x + return len(xs) diff --git a/utbot-python/samples/samples/exceptions/__init__.py b/utbot-python/samples/samples/exceptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/exceptions/exception_examples.py b/utbot-python/samples/samples/exceptions/exception_examples.py new file mode 100644 index 0000000000..c052935f66 --- /dev/null +++ b/utbot-python/samples/samples/exceptions/exception_examples.py @@ -0,0 +1,53 @@ +import typing + +from samples.exceptions.my_checked_exception import MyCheckedException + + +def init_array(n: int): + try: + a: typing.List[typing.Optional[int]] = [None] * n + a[n-1] = n + 1 + a[n-2] = n + 2 + return a[n-1] + a[n-2] + except ImportError: + return -1 + except IndexError: + return -2 + + +def nested_exceptions(i: int): + try: + return check_all(i) + except IndexError: + return 100 + except ValueError: + return -100 + + +def check_positive(i: int): + if i > 0: + raise IndexError("Positive") + return 0 + + +def check_all(i: int): + if i < 0: + raise ValueError("Negative") + return check_positive(i) + + +def throw_exception(i: int): + r = 1 + if i > 0: + r += 10 + r -= (i + r) / 0 + else: + r += 100 + return r + + +def throw_my_exception(i: int): + if i > 0: + raise MyCheckedException("i > 0") + return i ** 2 + diff --git a/utbot-python/samples/samples/exceptions/my_checked_exception.py b/utbot-python/samples/samples/exceptions/my_checked_exception.py new file mode 100644 index 0000000000..3be517e3a4 --- /dev/null +++ b/utbot-python/samples/samples/exceptions/my_checked_exception.py @@ -0,0 +1,6 @@ +class MyCheckedException(Exception): + def __init__(self, x: str): + self.x = x + + def method(self, y: str): + return self.x == y diff --git a/utbot-python/samples/samples/imports/__init__.py b/utbot-python/samples/samples/imports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/imports/builtins_module/__init__.py b/utbot-python/samples/samples/imports/builtins_module/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/imports/builtins_module/crypto.py b/utbot-python/samples/samples/imports/builtins_module/crypto.py new file mode 100644 index 0000000000..5315d61e37 --- /dev/null +++ b/utbot-python/samples/samples/imports/builtins_module/crypto.py @@ -0,0 +1,15 @@ +import hashlib +from collections import Counter + + +def f(word: str): + m = hashlib.sha256() + m.update(word.encode()) + code = m.hexdigest() + if len(code) > len(word): + return Counter(code) + return Counter(word) + + +if __name__ == '__main__': + print(f("fjasld")) diff --git a/utbot-python/samples/samples/imports/pack_1/__init__.py b/utbot-python/samples/samples/imports/pack_1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/imports/pack_1/inner_pack_1/__init__.py b/utbot-python/samples/samples/imports/pack_1/inner_pack_1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/imports/pack_1/inner_pack_1/inner_mod_1.py b/utbot-python/samples/samples/imports/pack_1/inner_pack_1/inner_mod_1.py new file mode 100644 index 0000000000..a8112a5831 --- /dev/null +++ b/utbot-python/samples/samples/imports/pack_1/inner_pack_1/inner_mod_1.py @@ -0,0 +1,7 @@ +from ..inner_pack_2 import inner_mod_2 + + +def inner_func_1(a: int): + if a > 1: + return inner_mod_2.inner_func_2(a) + return a ** 2 diff --git a/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_2.py b/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_2.py new file mode 100644 index 0000000000..8d349164a7 --- /dev/null +++ b/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_2.py @@ -0,0 +1,5 @@ +from ..inner_pack_2 import inner_mod_3 + + +def inner_func_2(a: int): + return inner_mod_3.inner_func_3(a) diff --git a/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_3.py b/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_3.py new file mode 100644 index 0000000000..60d7ab5ea2 --- /dev/null +++ b/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_3.py @@ -0,0 +1,2 @@ +def inner_func_3(x: float): + return int(x) diff --git a/utbot-python/samples/samples/mathematics/__init__.py b/utbot-python/samples/samples/mathematics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/named_arguments/__init__.py b/utbot-python/samples/samples/named_arguments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/named_arguments/method_named_arguments.py b/utbot-python/samples/samples/named_arguments/method_named_arguments.py new file mode 100644 index 0000000000..f86c3d623a --- /dev/null +++ b/utbot-python/samples/samples/named_arguments/method_named_arguments.py @@ -0,0 +1,29 @@ +def g(x): + return x ** 2 + + +class A: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other + + def __round__(self, n=None): + if n is not None: + return round(self.x, n) + return round(self.x) + + def __pow__(self, power, modulo=None): + if modulo is None: + return self.x ** power + return pow(self.x, power, modulo) + + def f1(self, x, y=1, *, z, t=2): + if y == 0: + return 100 * x + t + else: + x *= y + if x % 2 == 0: + y = g(x) + z + return x + y + 100 / y \ No newline at end of file diff --git a/utbot-python/samples/samples/named_arguments/named_arguments.py b/utbot-python/samples/samples/named_arguments/named_arguments.py new file mode 100644 index 0000000000..57f7c35b84 --- /dev/null +++ b/utbot-python/samples/samples/named_arguments/named_arguments.py @@ -0,0 +1,16 @@ +def f(x, y=1, *, z, t=2): + if y == 0: + return 100 * x + t + else: + x *= y + if x % 2 == 0: + y = g(x) + z + return x + y + 100 / y + + +def g(x): + return x ** 2 + + +if __name__ == '__main__': + print(f(1, y=2, z=3)) diff --git a/utbot-python/samples/samples/numpy/example_gauss.py b/utbot-python/samples/samples/numpy/example_gauss.py new file mode 100644 index 0000000000..067c97e9b5 --- /dev/null +++ b/utbot-python/samples/samples/numpy/example_gauss.py @@ -0,0 +1,71 @@ +import numpy as np + + +def solve_linear_system(matrix: np.ndarray) -> np.ndarray: + """ + Solve a linear system of equations using Gaussian elimination with partial pivoting + + Args: + - matrix: Coefficient matrix with the last column representing the constants. + + Returns: + - Solution vector. + + Raises: + - ValueError: If the matrix is not correct (i.e., singular). + + https://courses.engr.illinois.edu/cs357/su2013/lect.htm Lecture 7 + + Example: + >>> A = np.array([[2, 1, -1], [-3, -1, 2], [-2, 1, 2]], dtype=float) + >>> B = np.array([8, -11, -3], dtype=float) + >>> solution = solve_linear_system(np.column_stack((A, B))) + >>> np.allclose(solution, np.array([2., 3., -1.])) + True + >>> solve_linear_system(np.array([[0, 0], [0, 0]], dtype=float)) + array([nan, nan]) + """ + ab = np.copy(matrix) + num_of_rows = ab.shape[0] + num_of_columns = ab.shape[1] - 1 + x_lst: list[float] = [] + + for column_num in range(num_of_rows): + for i in range(column_num, num_of_columns): + if abs(ab[i][column_num]) > abs(ab[column_num][column_num]): + ab[[column_num, i]] = ab[[i, column_num]] + if ab[column_num, column_num] == 0.0: + raise ValueError("Matrix is not correct") + else: + pass + if column_num != 0: + for i in range(column_num, num_of_rows): + ab[i, :] -= ( + ab[i, column_num - 1] + / ab[column_num - 1, column_num - 1] + * ab[column_num - 1, :] + ) + + for column_num in range(num_of_rows): + for i in range(column_num, num_of_columns): + if abs(ab[i][column_num]) > abs(ab[column_num][column_num]): + ab[[column_num, i]] = ab[[i, column_num]] + if ab[column_num, column_num] == 0.0: + raise ValueError("Matrix is not correct") + else: + pass + if column_num != 0: + for i in range(column_num, num_of_rows): + ab[i, :] -= ( + ab[i, column_num - 1] + / ab[column_num - 1, column_num - 1] + * ab[column_num - 1, :] + ) + + for column_num in range(num_of_rows - 1, -1, -1): + x = ab[column_num, -1] / ab[column_num, column_num] + x_lst.insert(0, x) + for i in range(column_num - 1, -1, -1): + ab[i, -1] -= ab[i, column_num] * x + + return np.asarray(x_lst) \ No newline at end of file diff --git a/utbot-python/samples/samples/numpy/example_linear_algebra.py b/utbot-python/samples/samples/numpy/example_linear_algebra.py new file mode 100644 index 0000000000..b376fc4e73 --- /dev/null +++ b/utbot-python/samples/samples/numpy/example_linear_algebra.py @@ -0,0 +1,24 @@ +import numpy as np + + +def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + rows, columns = np.shape(table) + if rows != columns: + msg = ( + "'table' has to be of square shaped array but got a " + f"{rows}x{columns} array:\n{table}" + ) + raise ValueError(msg) + lower = np.zeros((rows, columns)) + upper = np.zeros((rows, columns)) + for i in range(columns): + for j in range(i): + total = np.sum(lower[i, :i] * upper[:i, j]) + if upper[j][j] == 0: + raise ArithmeticError("No LU decomposition exists") + lower[i][j] = (table[i][j] - total) / upper[j][j] + lower[i][i] = 1 + for j in range(i, columns): + total = np.sum(lower[i, :i] * upper[:i, j]) + upper[i][j] = table[i][j] - total + return lower, upper diff --git a/utbot-python/samples/samples/numpy/example_matrices.py b/utbot-python/samples/samples/numpy/example_matrices.py new file mode 100644 index 0000000000..0e3205f91e --- /dev/null +++ b/utbot-python/samples/samples/numpy/example_matrices.py @@ -0,0 +1,37 @@ +import numpy as np + + +def _is_matrix_spd(matrix: np.ndarray) -> bool: + """ + Returns True if input matrix is symmetric positive definite. + Returns False otherwise. + + For a matrix to be SPD, all eigenvalues must be positive. + + >>> import numpy as np + >>> matrix = np.array([ + ... [4.12401784, -5.01453636, -0.63865857], + ... [-5.01453636, 12.33347422, -3.40493586], + ... [-0.63865857, -3.40493586, 5.78591885]]) + >>> _is_matrix_spd(matrix) + True + >>> matrix = np.array([ + ... [0.34634879, 1.96165514, 2.18277744], + ... [0.74074469, -1.19648894, -1.34223498], + ... [-0.7687067 , 0.06018373, -1.16315631]]) + >>> _is_matrix_spd(matrix) + False + """ + # Ensure matrix is square. + assert np.shape(matrix)[0] == np.shape(matrix)[1] + + # If matrix not symmetric, exit right away. + if np.allclose(matrix, matrix.T) is False: + return False + + # Get eigenvalues and eignevectors for a symmetric matrix. + eigen_values, _ = np.linalg.eigh(matrix) + + # Check sign of all eigenvalues. + # np.all returns a value of type np.bool_ + return bool(np.all(eigen_values > 0)) \ No newline at end of file diff --git a/utbot-python/samples/samples/numpy/example_matrix_rank.py b/utbot-python/samples/samples/numpy/example_matrix_rank.py new file mode 100644 index 0000000000..21bdb552a7 --- /dev/null +++ b/utbot-python/samples/samples/numpy/example_matrix_rank.py @@ -0,0 +1,76 @@ +def rank_of_matrix(matrix: list[list[int | float]]) -> int: + """ + Finds the rank of a matrix. + Args: + matrix: The matrix as a list of lists. + Returns: + The rank of the matrix. + Example: + >>> matrix1 = [[1, 2, 3], + ... [4, 5, 6], + ... [7, 8, 9]] + >>> rank_of_matrix(matrix1) + 2 + >>> matrix2 = [[1, 0, 0], + ... [0, 1, 0], + ... [0, 0, 0]] + >>> rank_of_matrix(matrix2) + 2 + >>> matrix3 = [[1, 2, 3, 4], + ... [5, 6, 7, 8], + ... [9, 10, 11, 12]] + >>> rank_of_matrix(matrix3) + 2 + >>> rank_of_matrix([[2,3,-1,-1], + ... [1,-1,-2,4], + ... [3,1,3,-2], + ... [6,3,0,-7]]) + 4 + >>> rank_of_matrix([[2,1,-3,-6], + ... [3,-3,1,2], + ... [1,1,1,2]]) + 3 + >>> rank_of_matrix([[2,-1,0], + ... [1,3,4], + ... [4,1,-3]]) + 3 + >>> rank_of_matrix([[3,2,1], + ... [-6,-4,-2]]) + 1 + >>> rank_of_matrix([[],[]]) + 0 + >>> rank_of_matrix([[1]]) + 1 + >>> rank_of_matrix([[]]) + 0 + """ + + rows = len(matrix) + columns = len(matrix[0]) + rank = min(rows, columns) + + for row in range(rank): + # Check if diagonal element is not zero + if matrix[row][row] != 0: + # Eliminate all the elements below the diagonal + for col in range(row + 1, rows): + multiplier = matrix[col][row] / matrix[row][row] + for i in range(row, columns): + matrix[col][i] -= multiplier * matrix[row][i] + else: + # Find a non-zero diagonal element to swap rows + reduce = True + for i in range(row + 1, rows): + if matrix[i][row] != 0: + matrix[row], matrix[i] = matrix[i], matrix[row] + reduce = False + break + if reduce: + rank -= 1 + for i in range(rows): + matrix[i][row] = matrix[i][rank] + + # Reduce the row pointer by one to stay on the same row + row -= 1 + + return rank \ No newline at end of file diff --git a/utbot-python/samples/samples/numpy/example_norm.py b/utbot-python/samples/samples/numpy/example_norm.py new file mode 100644 index 0000000000..61b121bfec --- /dev/null +++ b/utbot-python/samples/samples/numpy/example_norm.py @@ -0,0 +1,23 @@ +import numpy as np +from numpy import ndarray + + +def norm_squared(vector: ndarray) -> float: + """ + Return the squared second norm of vector + norm_squared(v) = sum(x * x for x in v) + + Args: + vector (ndarray): input vector + + Returns: + float: squared second norm of vector + + >>> norm_squared([1, 2]) + 5 + >>> norm_squared(np.asarray([1, 2])) + 5 + >>> norm_squared([0, 0]) + 0 + """ + return np.dot(vector, vector) \ No newline at end of file diff --git a/utbot-python/samples/samples/numpy/example_simple.py b/utbot-python/samples/samples/numpy/example_simple.py new file mode 100644 index 0000000000..a5e2ef18c1 --- /dev/null +++ b/utbot-python/samples/samples/numpy/example_simple.py @@ -0,0 +1,7 @@ +import numpy as np + + +def function_to_test_default(a: np.ndarray): + if a.shape == (2, 1,): + return 2 + return a.shape diff --git a/utbot-python/samples/samples/primitives/__init__.py b/utbot-python/samples/samples/primitives/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/primitives/bytes_example.py b/utbot-python/samples/samples/primitives/bytes_example.py new file mode 100644 index 0000000000..f23a7fa92b --- /dev/null +++ b/utbot-python/samples/samples/primitives/bytes_example.py @@ -0,0 +1,7 @@ +def neg_bytes(b: bytes): + return not b + + +def sum_bytes(a: bytes, b: bytes): + c = a + b + return c diff --git a/utbot-python/samples/samples/primitives/numbers.py b/utbot-python/samples/samples/primitives/numbers.py new file mode 100644 index 0000000000..1305a369ab --- /dev/null +++ b/utbot-python/samples/samples/primitives/numbers.py @@ -0,0 +1,34 @@ +import copy +import math +import typing + + +def summ(a: typing.SupportsInt, b: typing.SupportsInt): + return int(a) + int(b) + + +def create_table(a: int): + table = [] + for i in range(a): + row = [] + for j in range(a): + row.append(i * j) + table.append(copy.deepcopy(row)) + return table + + +def operations(a: int): + if a > 1024: + return math.log2(a) + if a > 512: + return math.exp(a) + if a > 256: + return math.isinf(a) + if a > 128: + return math.e * a + return math.sqrt(a) + + +def check_order(a: int, b: int, c: int): + return a < b < c + diff --git a/utbot-python/samples/samples/primitives/primitive_types.py b/utbot-python/samples/samples/primitives/primitive_types.py new file mode 100644 index 0000000000..5fdadbf0ca --- /dev/null +++ b/utbot-python/samples/samples/primitives/primitive_types.py @@ -0,0 +1,16 @@ +import datetime + + +def pretty_print(x): + if isinstance(x, int): + return 'It is integer.\n' + 'Value ' + str(x) + elif isinstance(x, str): + return 'It is string.\n' + 'Value <<' + x + '>>' + elif isinstance(x, complex): + return 'It is complex.\n' + 'Value (' + str(x.real) + ' + ' + str(x.real) + 'i)' + elif isinstance(x, list): + return 'It is list.\n' + f'Value {x}' + elif isinstance(x, datetime.datetime): + return 'Date ' + x.isoformat() + else: + return 'I do not have any variants' diff --git a/utbot-python/samples/samples/primitives/regex.py b/utbot-python/samples/samples/primitives/regex.py new file mode 100644 index 0000000000..04e4592358 --- /dev/null +++ b/utbot-python/samples/samples/primitives/regex.py @@ -0,0 +1,15 @@ +import re + + +def check_regex(string: str) -> bool: + pattern = r"'(''|\\\\|\\'|[^'])*'" + if re.match(pattern, string): + return True + return False + + +def create_pattern(string: str): + if len(string) > 10: + return re.compile(rf"{string}") + else: + return re.compile(string) diff --git a/utbot-python/samples/samples/primitives/str_example.py b/utbot-python/samples/samples/primitives/str_example.py new file mode 100644 index 0000000000..e9e0a8d3c8 --- /dev/null +++ b/utbot-python/samples/samples/primitives/str_example.py @@ -0,0 +1,59 @@ +import dataclasses +import typing + + +@dataclasses.dataclass +class IntPair: + fst: int + snd: int + + +def concat(a: str, b: str): + return a + b + + +def concat_pair(pair: IntPair): + return pair.fst + pair.snd + + +def string_constants(s: str): + return "String('" + s + "')" + + +def contains(s: str, t: str): + return t in s + + +def const_contains(s: str): + return "ab" in s + + +def to_str(a: int, b: int): + if a > b: + return str(a) + else: + return str(b) + + +def starts_with(s: str): + if s.startswith("1234567890"): + s = s.replace("3", "A") + else: + s = s.strip() + + if s[0] == "x": + return s + else: + return s.upper() + + +def join_str(strings: typing.List[str]): + return "--".join(strings) + + +def separated_str(x: int): + if x == 1: + return r"fjalsdk\\nfjlask" + if 1 < x < 100: + return "fjalsd\n" * x + return x diff --git a/utbot-python/samples/samples/recursion/__init__.py b/utbot-python/samples/samples/recursion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/recursion/recursion.py b/utbot-python/samples/samples/recursion/recursion.py new file mode 100644 index 0000000000..09c25521c6 --- /dev/null +++ b/utbot-python/samples/samples/recursion/recursion.py @@ -0,0 +1,41 @@ +def factorial(n: int): + if n < 0: + raise ValueError() + if n == 0: + return 1 + return n * factorial(n-1) + + +def fib(n: int): + if n < 0: + raise ValueError + if n == 0: + return 0 + if n == 1: + return 1 + return fib(n-1) + fib(n-2) + + +def summ(fst: int, snd: int): + def signum(x: int): + return 0 if x == 0 else 1 if x > 0 else -1 + if snd == 0: + return fst + return summ(fst + signum(snd), snd - signum(snd)) + + +def recursion_with_exception(n: int): + if n < 42: + recursion_with_exception(n+1) + if n > 42: + recursion_with_exception(n-1) + raise ValueError + + +def first(n: int): + def second(n: int): + first(n) + if n < 4: + return + second(n) + diff --git a/utbot-python/samples/samples/structures/__init__.py b/utbot-python/samples/samples/structures/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/structures/boruvka.py b/utbot-python/samples/samples/structures/boruvka.py new file mode 100644 index 0000000000..1e757abcf8 --- /dev/null +++ b/utbot-python/samples/samples/structures/boruvka.py @@ -0,0 +1,106 @@ +class Graph: + def __init__(self, num_of_nodes: int) -> None: + """ + Arguments: + num_of_nodes - the number of nodes in the graph + Attributes: + m_num_of_nodes - the number of nodes in the graph. + m_edges - the list of edges. + m_component - the dictionary which stores the index of the component which + a node belongs to. + """ + + self.m_num_of_nodes = num_of_nodes + self.m_edges: list[list[int]] = [] + self.m_component: dict[int, int] = {} + + def add_edge(self, u_node: int, v_node: int, weight: int) -> None: + """Adds an edge in the format [first, second, edge weight] to graph.""" + + self.m_edges.append([u_node, v_node, weight]) + + def find_component(self, u_node: int) -> int: + """Propagates a new component throughout a given component.""" + + if self.m_component[u_node] == u_node: + return u_node + return self.find_component(self.m_component[u_node]) + + def set_component(self, u_node: int) -> None: + """Finds the component index of a given node""" + + if self.m_component[u_node] != u_node: + for k in self.m_component: + self.m_component[k] = self.find_component(k) + + def union(self, component_size: list[int], u_node: int, v_node: int) -> None: + """Union finds the roots of components for two nodes, compares the components + in terms of size, and attaches the smaller one to the larger one to form + single component""" + + if component_size[u_node] <= component_size[v_node]: + self.m_component[u_node] = v_node + component_size[v_node] += component_size[u_node] + self.set_component(u_node) + + elif component_size[u_node] >= component_size[v_node]: + self.m_component[v_node] = self.find_component(u_node) + component_size[u_node] += component_size[v_node] + self.set_component(v_node) + + def boruvka(self) -> None: + """Performs Borůvka's algorithm to find MST.""" + + # Initialize additional lists required to algorithm. + component_size = [] + mst_weight = 0 + + minimum_weight_edge: list = [-1] * self.m_num_of_nodes + + # A list of components (initialized to all of the nodes) + for node in range(self.m_num_of_nodes): + self.m_component.update({node: node}) + component_size.append(1) + + num_of_components = self.m_num_of_nodes + + while num_of_components > 1: + for edge in self.m_edges: + u, v, w = edge + + u_component = self.m_component[u] + v_component = self.m_component[v] + + if u_component != v_component: + """If the current minimum weight edge of component u doesn't + exist (is -1), or if it's greater than the edge we're + observing right now, we will assign the value of the edge + we're observing to it. + + If the current minimum weight edge of component v doesn't + exist (is -1), or if it's greater than the edge we're + observing right now, we will assign the value of the edge + we're observing to it""" + + for component in (u_component, v_component): + if ( + minimum_weight_edge[component] == -1 + or minimum_weight_edge[component][2] > w + ): + minimum_weight_edge[component] = [u, v, w] + + for edge in minimum_weight_edge: + if isinstance(edge, list): + u, v, w = edge + + u_component = self.m_component[u] + v_component = self.m_component[v] + + if u_component != v_component: + mst_weight += w + self.union(component_size, u_component, v_component) + print(f"Added edge [{u} - {v}]\nAdded weight: {w}\n") + num_of_components -= 1 + + minimum_weight_edge = [-1] * self.m_num_of_nodes + print(f"The total weight of the minimal spanning tree is: {mst_weight}") diff --git a/utbot-python/samples/samples/structures/deque.py b/utbot-python/samples/samples/structures/deque.py new file mode 100644 index 0000000000..87ea0e958e --- /dev/null +++ b/utbot-python/samples/samples/structures/deque.py @@ -0,0 +1,8 @@ +from collections import deque + + +def generate_people_deque(people_count: int): + names = ['Alex', 'Bob', 'Cate', 'Daisy', 'Ed'] + if people_count > 5: + people_count = 5 + return deque(sorted(names[:people_count])) \ No newline at end of file diff --git a/utbot-python/samples/samples/structures/graph_matrix.py b/utbot-python/samples/samples/structures/graph_matrix.py new file mode 100644 index 0000000000..785c295724 --- /dev/null +++ b/utbot-python/samples/samples/structures/graph_matrix.py @@ -0,0 +1,40 @@ +# An island in matrix is a group of linked areas, all having the same value. +# This code counts number of islands in a given matrix, with including diagonal +# connections. + + +class Matrix: # Public class to implement a graph + def __init__(self, row: int, col: int, graph: list[list[bool]]) -> None: + self.ROW = row + self.COL = col + self.graph = graph + + def __eq__(self, other): + return self.graph == other.graph + + def is_safe(self, i: int, j: int, visited: list[list[bool]]) -> bool: + return ( + 0 <= i < self.ROW + and 0 <= j < self.COL + and not visited[i][j] + and self.graph[i][j] + ) + + def diffs(self, i: int, j: int, visited: list[list[bool]]) -> None: + # Checking all 8 elements surrounding nth element + row_nbr = [-1, -1, -1, 0, 0, 1, 1, 1] # Coordinate order + col_nbr = [-1, 0, 1, -1, 1, -1, 0, 1] + visited[i][j] = True # Make those cells visited + for k in range(8): + if self.is_safe(i + row_nbr[k], j + col_nbr[k], visited): + self.diffs(i + row_nbr[k], j + col_nbr[k], visited) + + def count_islands(self) -> int: # And finally, count all islands. + visited = [[False for j in range(self.COL)] for i in range(self.ROW)] + count = 0 + for i in range(self.ROW): + for j in range(self.COL): + if visited[i][j] is False and self.graph[i][j] == 1: + self.diffs(i, j, visited) + count += 1 + return count \ No newline at end of file diff --git a/utbot-python/samples/samples/structures/matrix.py b/utbot-python/samples/samples/structures/matrix.py new file mode 100644 index 0000000000..b9b284d334 --- /dev/null +++ b/utbot-python/samples/samples/structures/matrix.py @@ -0,0 +1,82 @@ +from __future__ import annotations +from itertools import product +from typing import List + + +class MatrixException(Exception): + def __init__(self, description): + self.description = description + + +class Matrix: + def __init__(self, elements: List[List[float]]): + assert all(len(elements[i-1]) == len(row) for i, row in enumerate(elements)) + self.elements = elements + + @property + def dim(self) -> tuple[int, int]: + return ( + len(self.elements), + max(len(self.elements[i]) for i in range(len(self.elements))) + if len(self.elements) > 0 else 0 + ) + + def __repr__(self): + return str(self.elements) + + def __eq__(self, other): + if isinstance(other, Matrix): + return self.elements == other.elements + + def __add__(self, other: Matrix): + if self.dim == other.dim: + return Matrix([ + [ + elem + other_elem for elem, other_elem in + zip(self.elements[i], other.elements[i]) + ] + for i in range(self.dim[0]) + ]) + + def __mul__(self, other): + if isinstance(other, (int, float, complex)): + return Matrix([ + [ + elem * other for elem in + self.elements[i] + ] + for i in range(self.dim[0]) + ]) + else: + raise MatrixException("Wrong Type") + + def __matmul__(self, other: Matrix): + if isinstance(other, Matrix): + if self.dim[1] == other.dim[0]: + result = [[0 for _ in range(self.dim[0])] * other.dim[1]] + for i, j in product(range(self.dim[0]), range(other.dim[1])): + result[i][j] = sum( + self.elements[i][k] * other.elements[k][j] + for k in range(self.dim[1]) + ) + return Matrix(result) + else: + MatrixException("Wrong dimensions") + else: + raise MatrixException("Wrong Type") + + def is_diagonal(self) -> bool: + if self.dim[0] != self.dim[1]: + raise MatrixException("Bad matrix") + + for i, row in enumerate(self.elements): + for j, elem in enumerate(row): + if i != j and elem != 0: + return False + return True + + +if __name__ == '__main__': + a = Matrix([[1., 2.]]) + b = Matrix([[3.], [4.]]) + print(a @ b) diff --git a/utbot-python/samples/samples/structures/multi_level_feedback_queue.py b/utbot-python/samples/samples/structures/multi_level_feedback_queue.py new file mode 100644 index 0000000000..284c758b8f --- /dev/null +++ b/utbot-python/samples/samples/structures/multi_level_feedback_queue.py @@ -0,0 +1,312 @@ +from collections import deque + + +class Process: + def __init__(self, process_name: str, arrival_time: int, burst_time: int) -> None: + self.process_name = process_name # process name + self.arrival_time = arrival_time # arrival time of the process + # completion time of finished process or last interrupted time + self.stop_time = arrival_time + self.burst_time = burst_time # remaining burst time + self.waiting_time = 0 # total time of the process wait in ready queue + self.turnaround_time = 0 # time from arrival time to completion time + + +class MLFQ: + """ + MLFQ(Multi Level Feedback Queue) + https://en.wikipedia.org/wiki/Multilevel_feedback_queue + MLFQ has a lot of queues that have different priority + In this MLFQ, + The first Queue(0) to last second Queue(N-2) of MLFQ have Round Robin Algorithm + The last Queue(N-1) has First Come, First Served Algorithm + """ + + def __init__( + self, + number_of_queues: int, + time_slices: list[int], + queue: deque[Process], + current_time: int, + ) -> None: + # total number of mlfq's queues + self.number_of_queues = number_of_queues + # time slice of queues that round robin algorithm applied + self.time_slices = time_slices + # unfinished process is in this ready_queue + self.ready_queue = queue + # current time + self.current_time = current_time + # finished process is in this sequence queue + self.finish_queue: deque[Process] = deque() + + def calculate_sequence_of_finish_queue(self) -> list[str]: + """ + This method returns the sequence of finished processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_sequence_of_finish_queue() + ['P2', 'P4', 'P1', 'P3'] + """ + sequence = [] + for i in range(len(self.finish_queue)): + sequence.append(self.finish_queue[i].process_name) + return sequence + + def calculate_waiting_time(self, queue: list[Process]) -> list[int]: + """ + This method calculates waiting time of processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_waiting_time([P1, P2, P3, P4]) + [83, 17, 94, 101] + """ + waiting_times = [] + for i in range(len(queue)): + waiting_times.append(queue[i].waiting_time) + return waiting_times + + def calculate_turnaround_time(self, queue: list[Process]) -> list[int]: + """ + This method calculates turnaround time of processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_turnaround_time([P1, P2, P3, P4]) + [136, 34, 162, 125] + """ + turnaround_times = [] + for i in range(len(queue)): + turnaround_times.append(queue[i].turnaround_time) + return turnaround_times + + def calculate_completion_time(self, queue: list[Process]) -> list[int]: + """ + This method calculates completion time of processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_turnaround_time([P1, P2, P3, P4]) + [136, 34, 162, 125] + """ + completion_times = [] + for i in range(len(queue)): + completion_times.append(queue[i].stop_time) + return completion_times + + def calculate_remaining_burst_time_of_processes( + self, queue: deque[Process] + ) -> list[int]: + """ + This method calculate remaining burst time of processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> finish_queue, ready_queue = mlfq.round_robin(deque([P1, P2, P3, P4]), 17) + >>> mlfq.calculate_remaining_burst_time_of_processes(mlfq.finish_queue) + [0] + >>> mlfq.calculate_remaining_burst_time_of_processes(ready_queue) + [36, 51, 7] + >>> finish_queue, ready_queue = mlfq.round_robin(ready_queue, 25) + >>> mlfq.calculate_remaining_burst_time_of_processes(mlfq.finish_queue) + [0, 0] + >>> mlfq.calculate_remaining_burst_time_of_processes(ready_queue) + [11, 26] + """ + return [q.burst_time for q in queue] + + def update_waiting_time(self, process: Process) -> int: + """ + This method updates waiting times of unfinished processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> mlfq.current_time = 10 + >>> P1.stop_time = 5 + >>> mlfq.update_waiting_time(P1) + 5 + """ + process.waiting_time += self.current_time - process.stop_time + return process.waiting_time + + def first_come_first_served(self, ready_queue: deque[Process]) -> deque[Process]: + """ + FCFS(First Come, First Served) + FCFS will be applied to MLFQ's last queue + A first came process will be finished at first + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.first_come_first_served(mlfq.ready_queue) + >>> mlfq.calculate_sequence_of_finish_queue() + ['P1', 'P2', 'P3', 'P4'] + """ + finished: deque[Process] = deque() # sequence deque of finished process + while len(ready_queue) != 0: + cp = ready_queue.popleft() # current process + + # if process's arrival time is later than current time, update current time + if self.current_time < cp.arrival_time: + self.current_time += cp.arrival_time + + # update waiting time of current process + self.update_waiting_time(cp) + # update current time + self.current_time += cp.burst_time + # finish the process and set the process's burst-time 0 + cp.burst_time = 0 + # set the process's turnaround time because it is finished + cp.turnaround_time = self.current_time - cp.arrival_time + # set the completion time + cp.stop_time = self.current_time + # add the process to queue that has finished queue + finished.append(cp) + + self.finish_queue.extend(finished) # add finished process to finish queue + # FCFS will finish all remaining processes + return finished + + def round_robin( + self, ready_queue: deque[Process], time_slice: int + ) -> tuple[deque[Process], deque[Process]]: + """ + RR(Round Robin) + RR will be applied to MLFQ's all queues except last queue + All processes can't use CPU for time more than time_slice + If the process consume CPU up to time_slice, it will go back to ready queue + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> finish_queue, ready_queue = mlfq.round_robin(mlfq.ready_queue, 17) + >>> mlfq.calculate_sequence_of_finish_queue() + ['P2'] + """ + finished: deque[Process] = deque() # sequence deque of terminated process + # just for 1 cycle and unfinished processes will go back to queue + for _ in range(len(ready_queue)): + cp = ready_queue.popleft() # current process + + # if process's arrival time is later than current time, update current time + if self.current_time < cp.arrival_time: + self.current_time += cp.arrival_time + + # update waiting time of unfinished processes + self.update_waiting_time(cp) + # if the burst time of process is bigger than time-slice + if cp.burst_time > time_slice: + # use CPU for only time-slice + self.current_time += time_slice + # update remaining burst time + cp.burst_time -= time_slice + # update end point time + cp.stop_time = self.current_time + # locate the process behind the queue because it is not finished + ready_queue.append(cp) + else: + # use CPU for remaining burst time + self.current_time += cp.burst_time + # set burst time 0 because the process is finished + cp.burst_time = 0 + # set the finish time + cp.stop_time = self.current_time + # update the process' turnaround time because it is finished + cp.turnaround_time = self.current_time - cp.arrival_time + # add the process to queue that has finished queue + finished.append(cp) + + self.finish_queue.extend(finished) # add finished process to finish queue + # return finished processes queue and remaining processes queue + return finished, ready_queue + + def multi_level_feedback_queue(self) -> deque[Process]: + """ + MLFQ(Multi Level Feedback Queue) + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> finish_queue = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_sequence_of_finish_queue() + ['P2', 'P4', 'P1', 'P3'] + """ + + # all queues except last one have round_robin algorithm + for i in range(self.number_of_queues - 1): + finished, self.ready_queue = self.round_robin( + self.ready_queue, self.time_slices[i] + ) + # the last queue has first_come_first_served algorithm + self.first_come_first_served(self.ready_queue) + + return self.finish_queue + + +if __name__ == "__main__": + import doctest + + P1 = Process("P1", 0, 53) + P2 = Process("P2", 0, 17) + P3 = Process("P3", 0, 68) + P4 = Process("P4", 0, 24) + number_of_queues = 3 + time_slices = [17, 25] + queue = deque([P1, P2, P3, P4]) + + if len(time_slices) != number_of_queues - 1: + raise SystemExit(0) + + doctest.testmod(extraglobs={"queue": deque([P1, P2, P3, P4])}) + + P1 = Process("P1", 0, 53) + P2 = Process("P2", 0, 17) + P3 = Process("P3", 0, 68) + P4 = Process("P4", 0, 24) + number_of_queues = 3 + time_slices = [17, 25] + queue = deque([P1, P2, P3, P4]) + mlfq = MLFQ(number_of_queues, time_slices, queue, 0) + finish_queue = mlfq.multi_level_feedback_queue() + + # print total waiting times of processes(P1, P2, P3, P4) + print( + f"waiting time:\ + \t\t\t{MLFQ.calculate_waiting_time(mlfq, [P1, P2, P3, P4])}" + ) + # print completion times of processes(P1, P2, P3, P4) + print( + f"completion time:\ + \t\t{MLFQ.calculate_completion_time(mlfq, [P1, P2, P3, P4])}" + ) + # print total turnaround times of processes(P1, P2, P3, P4) + print( + f"turnaround time:\ + \t\t{MLFQ.calculate_turnaround_time(mlfq, [P1, P2, P3, P4])}" + ) + # print sequence of finished processes + print( + f"sequence of finished processes:\ + {mlfq.calculate_sequence_of_finish_queue()}" + ) \ No newline at end of file diff --git a/utbot-python/samples/samples/type_inference/__init__.py b/utbot-python/samples/samples/type_inference/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/type_inference/annotations.py b/utbot-python/samples/samples/type_inference/annotations.py new file mode 100644 index 0000000000..ad3b4606dd --- /dev/null +++ b/utbot-python/samples/samples/type_inference/annotations.py @@ -0,0 +1,71 @@ +from typing import * + + +def simple_annotations1(x: int, y: int): + if x > 1: + return x + y + else: + return y * 2 + + +def simple_annotations2(x: str, y: str): + if len(x) > 1: + return x + y + else: + return y * 2 + + +def simple_annotations3(x: str, y: bool): + if y: + return x + else: + return x * 2 + + +def simple_annotations4(x: float, y: int): + return x + y + + +def union(x: Union[int, str]): + if isinstance(x, int): + return 10 * x + else: + return x + "!" + + +def list_(x: List[int]): + return len(x) + + +def tuple_1(x: Tuple[int, str]): + return len(x) + + +def tuple_2(x: Tuple[int, ...]): + return len(x) + + +def dict_1(x: Dict[int, str]): + return len(x) + + +def dict_2(x: Dict[int, Union[str, bool]]): + return len(x) + + +def set_1(x: Set[int]): + return len(x) + + +def any_1(x: Any): + return x + + +class Node: + def __init__(self, name: str): + self.name = name + self.children = [] + + +def reduce_1(x: Node): + return x.name diff --git a/utbot-python/samples/samples/type_inference/annotations2.py b/utbot-python/samples/samples/type_inference/annotations2.py new file mode 100644 index 0000000000..3c34fa150f --- /dev/null +++ b/utbot-python/samples/samples/type_inference/annotations2.py @@ -0,0 +1,76 @@ +from __future__ import annotations +from typing import * +from enum import Enum + + +XXX = TypeVar("XXX", "A", int) + + +def f(x: int): + a = [x, XXX] + return a + + +class A(Generic[XXX]): + self_: XXX + + def f(self, a, b: A[int]): + self.y = b + self.self_.x = b + pass + + def g(self): + self.x = 1 + + +def square(collection: Iterable[int], x: XXX): + result = set() + for elem in collection: + result.add(elem ** 2) + return result + + +def not_annotated(x): + return x + + +def same_annotations(x: int, y: int, a: List[Any], b: List[Any], c: List[int]): + return x + y + + +def optional(x: Optional[int]): + return x + + +def literal(x: Literal["w", "r"]): + return x + + +class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + +def enum_literal(x: Literal[Color.RED, Color.GREEN]): + return x + + +def abstract_set(x: AbstractSet[int]): + return x + + +def mapping(x: Mapping[int, int]): + return x + + +def sequence(x: Sequence[object]): + return x + + +def supports_abs(x: SupportsAbs): + return abs(x) + + +def tuple_(x: Tuple[int, str]): + return x[1] + str(x[0]) diff --git a/utbot-python/samples/samples/type_inference/generics.py b/utbot-python/samples/samples/type_inference/generics.py new file mode 100644 index 0000000000..e0939f3e4e --- /dev/null +++ b/utbot-python/samples/samples/type_inference/generics.py @@ -0,0 +1,24 @@ +from typing import TypeVar, Generic +from logging import Logger + +T = TypeVar('T', bound=bool) +U = TypeVar('U', str, object) + + +class LoggedVar(Generic[T]): + def __init__(self, value: T, name: str) -> None: + self.name = name + self.value = value + + def set(self, new: T) -> None: + self.value = new + + def get(self, x: U): + return self.value, x + + +def func(x: T, y: U): + if x: + return y + else: + return 100 diff --git a/utbot-python/samples/samples/type_inference/list_of_datetime.py b/utbot-python/samples/samples/type_inference/list_of_datetime.py new file mode 100644 index 0000000000..b72f31d95c --- /dev/null +++ b/utbot-python/samples/samples/type_inference/list_of_datetime.py @@ -0,0 +1,11 @@ +import datetime + + +def get_data_labels(dates): + if not dates: + dates.append(datetime.time(hour=23, minute=59)) + return None + if all(x.hour == 0 and x.minute == 0 for x in dates): + return [x.strftime('%Y-%m-%d') for x in dates] + else: + return [x.strftime('%H:%M') for x in dates] diff --git a/utbot-python/samples/samples/type_inference/subtypes.py b/utbot-python/samples/samples/type_inference/subtypes.py new file mode 100644 index 0000000000..24eb1c18e6 --- /dev/null +++ b/utbot-python/samples/samples/type_inference/subtypes.py @@ -0,0 +1,52 @@ +import collections +from typing import * + + +class P(Protocol): + def f(self, x: int) -> dict: + ... + + +class S: + def f(self, x: Union[int, str]) -> collections.Counter: + return collections.Counter([x]) + + +class S1: + def f(self, x: Union[int, str]) -> object: + return collections.Counter([x]) + + +def func_for_p(x: P) -> None: + return None + + +# func_for_p(S()) + + +class R(Protocol): + def f(self) -> 'R': + ... + + +class RImpl: + def f(self) -> 'RImpl': + return self + + +def func_for_r(x: R) -> None: + return None + + +# func_for_r(RImpl()) + + +a: List[int] = [] + + +T = TypeVar('T') + + +def func_abs(x: SupportsAbs[T]): + return abs(x) + diff --git a/utbot-python/samples/samples/type_inference/type_inference.py b/utbot-python/samples/samples/type_inference/type_inference.py new file mode 100644 index 0000000000..5cc524df1e --- /dev/null +++ b/utbot-python/samples/samples/type_inference/type_inference.py @@ -0,0 +1,16 @@ +def type_inference(number, string, string_sep, list_of_number, dict_str_to_list): + new_string = '_' + string + '_' * number + new_string = new_string.capitalize() + string_sep + new_string[::-1] + + if len(list_of_number) < len(new_string): + list_of_number += [0] * (len(new_string) - len(list_of_number)) + + dict_str_to_list[string] = [] + for key in dict_str_to_list.keys(): + list_of_number.append(key) + + return list_of_number + + +if __name__ == '__main__': + print(type_inference(5, 'fjsl', '|', [1, 2, 3], {'fjls': [1, 2]})) \ No newline at end of file diff --git a/utbot-python/samples/samples/type_inference/type_inference_2.py b/utbot-python/samples/samples/type_inference/type_inference_2.py new file mode 100644 index 0000000000..5d55939213 --- /dev/null +++ b/utbot-python/samples/samples/type_inference/type_inference_2.py @@ -0,0 +1,9 @@ +def g(x): + + def f(y): + if y in [0, 100, 200, 500]: + return y // 100 + + if f(x) > 10: + return x ** 2 + diff --git a/utbot-python/samples/test_configuration.json b/utbot-python/samples/test_configuration.json new file mode 100644 index 0000000000..6df697b5d8 --- /dev/null +++ b/utbot-python/samples/test_configuration.json @@ -0,0 +1,667 @@ +{ + "parts": [ + { + "path": "samples/algorithms", + "files": [ + { + "name": "bfs", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 92 + } + ] + }, + { + "name": "longest_subsequence", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 91 + } + ] + }, + { + "name": "quick_sort", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 85 + } + ] + } + ] + }, + { + "path": "samples/classes", + "files": [ + { + "name": "constructors", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 80 + } + ] + }, + { + "name": "dataclass", + "groups": [ + { + "classes": ["C"], + "methods": ["C.inc"], + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "dicts", + "groups": [ + { + "classes": ["Dictionary"], + "methods": ["Dictionary.translate"], + "timeout": 60, + "coverage": 100 + } + ] + }, + { + "name": "easy_class", + "groups": [ + { + "classes": ["B"], + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "equals", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "inner_class", + "groups": [ + { + "classes": ["Outer"], + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "rename_self", + "groups": [ + { + "classes": ["A"], + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "setstate_test", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/collection", + "files": [ + { + "name": "dicts", + "groups": [ + { + "classes": ["-"], + "methods": ["f", "g"], + "timeout": 120, + "coverage": 100 + } + ] + }, + { + "name": "lists", + "groups": [ + { + "classes": ["-"], + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "recursive", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 100 + } + ] + }, + { + "name": "sets", + "groups": [ + { + "classes": ["-"], + "methods": ["f", "g"], + "timeout": 120, + "coverage": 100 + } + ] + }, + { + "name": "tuples", + "groups": [ + { + "classes": ["-"], + "methods": null, + "timeout": 10, + "coverage": 75 + } + ] + }, + { + "name": "using_collections", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 88 + } + ] + } + ] + }, + { + "path": "samples/controlflow", + "files": [ + { + "name": "arithmetic", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 100, + "coverage": 46 + } + ] + }, + { + "name": "conditions", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 180, + "coverage": 83 + } + ] + }, + { + "name": "inner_conditions", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 75 + } + ] + }, + { + "name": "multi_conditions", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 15, + "coverage": 93 + } + ] + } + ] + }, + { + "path": "samples/easy_samples", + "files": [ + { + "name": "deep_equals", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 90, + "coverage": 100 + } + ] + }, + { + "name": "deep_equals_2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + }, + { + "name": "fully_annotated", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 300, + "coverage": 100 + } + ] + }, + { + "name": "dummy_with_eq", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + }, + { + "name": "dummy_without_eq", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + }, + { + "name": "long_function_coverage", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/exceptions", + "files": [ + { + "name": "exception_examples", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 180, + "coverage": 97 + } + ] + } + ] + }, + { + "path": "samples/imports/builtins_module", + "files": [ + { + "name": "crypto", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 180, + "coverage": 86 + } + ] + } + ] + }, + { + "path": "samples/imports/pack_1/inner_pack_1", + "files": [ + { + "name": "inner_mod_1", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 180, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/imports/pack_1/inner_pack_2", + "files": [ + { + "name": "inner_mod_2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/imports/pack_1/inner_pack_2", + "files": [ + { + "name": "inner_mod_2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/primitives", + "files": [ + { + "name": "bytes_example", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 100 + } + ] + }, + { + "name": "numbers", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 80, + "coverage": 100 + } + ] + }, + { + "name": "primitive_types", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 60, + "coverage": 92 + } + ] + }, + { + "name": "regex", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 50, + "coverage": 100 + } + ] + }, + { + "name": "str_example", + "groups": [ + { + "classes": null, + "methods": [ + "concat", + "concat_pair", + "string_constants", + "contains", + "const_contains", + "to_str", + "starts_with", + "join_str" + ], + "timeout": 160, + "coverage": 100 + }, + { + "classes": null, + "methods": ["separated_str"], + "timeout": 100, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/recursion", + "files": [ + { + "name": "recursion", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 150, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/structures", + "files": [ + { + "name": "boruvka", + "groups": [ + { + "classes": ["Graph"], + "methods": null, + "timeout": 150, + "coverage": 64 + } + ] + }, + { + "name": "deque", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 150, + "coverage": 100 + } + ] + }, + { + "name": "graph_matrix", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 120, + "coverage": 96 + } + ] + }, + { + "name": "matrix", + "groups": [ + { + "classes": ["Matrix"], + "methods": ["Matrix.__repr__", "Matrix.__eq__", "Matrix.__add__", "Matrix.__mul__", "Matrix.is_diagonal"], + "timeout": 120, + "coverage": 97 + }, + { + "classes": ["Matrix"], + "methods": ["Matrix.__matmul__"], + "timeout": 80, + "coverage": 88 + } + ] + }, + { + "name": "multi_level_feedback_queue", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 180, + "coverage": 62 + } + ] + } + ] + }, + { + "path": "samples/type_inference", + "files": [ + { + "name": "annotations", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 140, + "coverage": 100 + } + ] + }, + { + "name": "annotations2", + "groups": [ + { + "classes": ["A"], + "methods": null, + "timeout": 40, + "coverage": 100 + } + ] + }, + { + "name": "annotations2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 140, + "coverage": 100 + } + ] + }, + { + "name": "generics", + "groups": [ + { + "classes": ["LoggedVar"], + "methods": null, + "timeout": 30, + "coverage": 80 + } + ] + }, + { + "name": "generics", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 80 + } + ] + }, + { + "name": "list_of_datetime", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 50 + } + ] + }, + { + "name": "subtypes", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 90, + "coverage": 100 + } + ] + }, + { + "name": "type_inference", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 40, + "coverage": 100 + } + ] + }, + { + "name": "type_inference_2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 40, + "coverage": 62 + } + ] + } + ] + } + ] +} diff --git a/utbot-python/samples/testing_utils/collect_executions.py b/utbot-python/samples/testing_utils/collect_executions.py new file mode 100644 index 0000000000..81404aa023 --- /dev/null +++ b/utbot-python/samples/testing_utils/collect_executions.py @@ -0,0 +1,34 @@ +import json +import pathlib +import sys +import typing + + +def get_all_files(folder: pathlib.Path) -> typing.List[pathlib.Path]: + if folder.is_dir: + return folder.glob("*.executions") + return [] + + +def get_excecutions_number(file: pathlib.Path) -> int: + with open(file, 'r', encoding='utf-8') as fin: + return int(json.loads(fin.readline().strip())['executions']) + + +def save_data(data: typing.Dict[pathlib.Path, int]) -> None: + with open('data_executions.json', 'w', encoding='utf-8') as fout: + print(json.dumps(data, indent=1), file=fout) + + +def main(folder: pathlib.Path) -> None: + files = get_all_files(folder) + data = { + str(file): get_excecutions_number(file) + for file in files + } + save_data(data) + + +if __name__ == '__main__': + main(pathlib.Path(sys.argv[1])) + diff --git a/utbot-python/samples/testing_utils/collect_timeouts.py b/utbot-python/samples/testing_utils/collect_timeouts.py new file mode 100644 index 0000000000..bbcb8241a2 --- /dev/null +++ b/utbot-python/samples/testing_utils/collect_timeouts.py @@ -0,0 +1,41 @@ +import json +import pathlib +import sys +import typing + +def read_test_config(path: pathlib.Path, coverage_dir: pathlib.Path) -> typing.Dict[str, int]: + with open(path, 'r', encoding='utf-8') as fin: + data = json.loads(fin.read()) + + res = {} + for part in data['parts']: + for file in part['files']: + for group in file['groups']: + file_suffix = f"{part['path'].replace('/', '_')}_{file['name']}" + executions_output_file = pathlib.Path(coverage_dir, f"coverage_{file_suffix}.json.executions") + timeout = group['timeout'] + res[executions_output_file] = timeout + return res + + +def read_executions(path: pathlib.Path): + with open(path, 'r', encoding='utf-8') as fin: + data = json.loads(fin.read()) + return data + + +def main(config_path: pathlib.Path, executions_path: pathlib.Path, coverage_dir: pathlib.Path): + timeouts = read_test_config(config_path, coverage_dir) + executions = read_executions(executions_path) + + for f, timeout in timeouts.items(): + executions[str(f)] /= timeout + + with open('speeds.json', 'w') as fout: + print(json.dumps(executions, indent=1), file=fout) + + +if __name__ == '__main__': + main(*sys.argv[1:]) + + diff --git a/utbot-python/samples/testing_utils/compare.py b/utbot-python/samples/testing_utils/compare.py new file mode 100644 index 0000000000..cb45d0cc49 --- /dev/null +++ b/utbot-python/samples/testing_utils/compare.py @@ -0,0 +1,25 @@ +import json +import matplotlib.pyplot as plt +import pathlib + + +def read_stats(path: pathlib.Path): + with open(path, 'r') as fin: + return json.loads(fin.read()) + + +data1 = read_stats('speeds_basic.json') +data2 = read_stats('speeds_forks.json') +data3 = read_stats('speeds_forks2.json') +data = [list(d.values()) for d in [data1, data2, data3]] + +fig = plt.figure() +ax = fig.add_subplot(1, 1, 1) +ax.boxplot( + data, + labels=['basic', 'forks', 'forks2'], + vert=True, + patch_artist=True + ) +ax.grid(True) +plt.show() diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt new file mode 100644 index 0000000000..5123a67e2f --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt @@ -0,0 +1,63 @@ +package org.utbot.python + +import mu.KotlinLogging +import org.utbot.framework.minimization.minimizeExecutions +import org.utbot.framework.plugin.api.UtClusterInfo +import org.utbot.framework.plugin.api.UtError +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.python.engine.GlobalPythonEngine +import org.utbot.python.framework.api.python.PythonUtExecution + +private val logger = KotlinLogging.logger {} +private const val MAX_EMPTY_COVERAGE_TESTS = 5 + +class PythonTestCaseGenerator( + private val configuration: PythonTestGenerationConfig, + private val mypyConfig: MypyConfig, +) { + private val withMinimization = configuration.withMinimization + + fun generate(method: PythonMethod, until: Long): List { + logger.info { "Start test generation for ${method.name}" } + val engine = GlobalPythonEngine( + method = method, + configuration = configuration, + mypyConfig, + until, + ) + try { + engine.run() + } catch (_: OutOfMemoryError) { + logger.debug { "Out of memory error. Stop test generation process" } + } + + logger.info { "Collect all test executions for ${method.name}" } + return listOf( + buildTestSet(method, engine.executionStorage.fuzzingExecutions, engine.executionStorage.fuzzingErrors, UtClusterInfo("FUZZER")), + ) + } + + private fun buildTestSet( + method: PythonMethod, + executions: List, + errors: List, + clusterInfo: UtClusterInfo, + ): PythonTestSet { + val (emptyCoverageExecutions, coverageExecutions) = executions.partition { it.coverage == null } + val (successfulExecutions, failedExecutions) = coverageExecutions.partition { it.result is UtExecutionSuccess } + val minimized = + if (withMinimization) + minimizeExecutions(successfulExecutions) + + minimizeExecutions(failedExecutions) + + emptyCoverageExecutions.take(MAX_EMPTY_COVERAGE_TESTS) + else + coverageExecutions + emptyCoverageExecutions.take(MAX_EMPTY_COVERAGE_TESTS) + return PythonTestSet( + method, + minimized, + errors, + executionsNumber = executions.size, + clustersInfo = listOf(Pair(clusterInfo, minimized.indices)) + ) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationConfig.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationConfig.kt new file mode 100644 index 0000000000..69d60bd43d --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationConfig.kt @@ -0,0 +1,52 @@ +package org.utbot.python + +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.python.coverage.CoverageOutputFormat +import org.utbot.python.coverage.PythonCoverageMode +import org.utpython.types.mypy.MypyBuildDirectory +import org.utpython.types.mypy.MypyInfoBuild +import org.utpython.types.mypy.MypyReportLine +import java.nio.file.Path + +data class TestFileInformation( + val testedFilePath: String, + val testedFileContent: String, + val moduleName: String, +) + +class PythonTestGenerationConfig( + val pythonPath: String, + val testFileInformation: TestFileInformation, + val sysPathDirectories: Set, + val testedMethods: List, + val timeout: Long, + val timeoutForRun: Long, + val testFramework: TestFramework, + val testSourceRootPath: Path, + val withMinimization: Boolean, + val isCanceled: () -> Boolean, + val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour, + val coverageMeasureMode: PythonCoverageMode = PythonCoverageMode.Instructions, + val sendCoverageContinuously: Boolean = true, + val coverageOutputFormat: CoverageOutputFormat = CoverageOutputFormat.Lines, + val prohibitedExceptions: List = defaultProhibitedExceptions, + val doNotGenerateStateAssertions: Boolean = false, +) { + companion object { + val defaultProhibitedExceptions: List = listOf( + "builtins.AttributeError", + "builtins.TypeError", + "builtins.NotImplementedError", + ) + + val skipClasses: List = emptyList() + val skipTopLevelFunctions: List = emptyList() + } +} + +data class MypyConfig( + val mypyStorage: MypyInfoBuild, + val mypyReportLine: List, + val mypyBuildDirectory: MypyBuildDirectory, +) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt new file mode 100644 index 0000000000..98b8ff2ca1 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt @@ -0,0 +1,341 @@ +package org.utbot.python + +import mu.KotlinLogging +import org.parsers.python.PythonParser +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.python.code.PythonCode +import org.utbot.python.coverage.CoverageFormat +import org.utbot.python.coverage.CoverageInfo +import org.utbot.python.coverage.CoverageOutputFormat +import org.utbot.python.coverage.PyInstruction +import org.utbot.python.coverage.filterMissedLines +import org.utbot.python.coverage.getInstructionsList +import org.utbot.python.coverage.getLinesList +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonMethodId +import org.utbot.python.framework.api.python.PythonModel +import org.utbot.python.framework.api.python.PythonUtExecution +import org.utbot.python.framework.api.python.RawPythonAnnotation +import org.utbot.python.framework.api.python.pythonBuiltinsModuleName +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.api.python.util.pythonNoneClassId +import org.utbot.python.framework.codegen.model.PythonCodeGenerator +import org.utbot.python.framework.codegen.model.PythonImport +import org.utbot.python.framework.codegen.model.PythonSysPathImport +import org.utbot.python.framework.codegen.model.PythonSystemImport +import org.utbot.python.framework.codegen.model.PythonUserImport +import org.utpython.types.PythonConcreteCompositeTypeDescription +import org.utpython.types.PythonFunctionDefinition +import org.utpython.types.general.CompositeType +import org.utpython.types.getPythonAttributes +import org.utpython.types.mypy.MypyBuildDirectory +import org.utpython.types.mypy.MypyInfoBuild +import org.utpython.types.mypy.readMypyAnnotationStorageAndInitialErrors +import org.utpython.types.pythonDescription +import org.utpython.types.pythonName +import org.utbot.python.utils.TemporaryFileManager +import org.utbot.python.utils.convertToTime +import org.utbot.python.utils.separateTimeout +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.pathString + +private val logger = KotlinLogging.logger {} + +// TODO: add asserts that one or less of containing classes and only one file +abstract class PythonTestGenerationProcessor { + abstract val configuration: PythonTestGenerationConfig + + fun sourceCodeAnalyze(): MypyConfig { + return sourceCodeAnalyze( + configuration.sysPathDirectories, + configuration.pythonPath, + configuration.testFileInformation, + ) + } + + fun testGenerate(mypyConfig: MypyConfig): List { + val testCaseGenerator = PythonTestCaseGenerator( + configuration = configuration, + mypyConfig = mypyConfig, + ) + + val oneFunctionTimeout = separateTimeout(configuration.timeout, configuration.testedMethods.size) + val countOfFunctions = configuration.testedMethods.size + val startTime = System.currentTimeMillis() + + val tests = configuration.testedMethods.mapIndexedNotNull { index, methodHeader -> + if (configuration.isCanceled()) { + return emptyList() + } + val usedTime = System.currentTimeMillis() - startTime + val expectedTime = index * oneFunctionTimeout + val localOneFunctionTimeout = if (usedTime < expectedTime) { + separateTimeout(configuration.timeout - usedTime, countOfFunctions - index) + } else { + oneFunctionTimeout + } + val localUntil = System.currentTimeMillis() + localOneFunctionTimeout + logger.info { "Local timeout ${configuration.timeout / configuration.testedMethods.size}ms. Until ${localUntil.convertToTime()}" } + try { + val method = findMethodByHeader( + mypyConfig.mypyStorage, + methodHeader, + configuration.testFileInformation.moduleName, + configuration.testFileInformation.testedFileContent + ) + testCaseGenerator.generate(method, localUntil) + } catch (e: SelectedMethodIsNotAFunctionDefinition) { + logger.warn { "Skipping method ${e.methodName}: did not find its function definition" } + null + } + }.flatten() + val notEmptyTests = tests.filter { it.executions.isNotEmpty() } + + val emptyTests = tests + .groupBy { it.method } + .filter { it.value.all { testSet -> testSet.executions.isEmpty() } } + .map { it.key.name } + + if (emptyTests.isNotEmpty()) { + notGeneratedTestsAction(emptyTests) + } + + return notEmptyTests + } + + fun testCodeGenerate(testSets: List): String { + if (testSets.isEmpty()) return "" + val containingClassName = getContainingClassName(testSets) + val classId = PythonClassId(configuration.testFileInformation.moduleName, containingClassName) + + val methodIds = testSets.associate { testSet -> + testSet.method to PythonMethodId( + classId, + testSet.method.renderMethodName(), + RawPythonAnnotation(pythonAnyClassId.name), + testSet.method.arguments.map { argument -> + argument.annotation?.let { annotation -> + RawPythonAnnotation(annotation) + } ?: pythonAnyClassId + }, + ) + } + + val paramNames = testSets.associate { testSet -> + var params = testSet.method.arguments.map { it.name } + if (testSet.method.hasThisArgument) { + params = params.drop(1) + } + methodIds[testSet.method] as ExecutableId to params + }.toMutableMap() + + val collectedImports = collectImports(testSets) + + val context = UtContext(this::class.java.classLoader) + withUtContext(context) { + val codegen = PythonCodeGenerator( + classId, + paramNames = paramNames, + testFramework = configuration.testFramework, + testClassPackageName = "", + hangingTestsTimeout = HangingTestsTimeout(configuration.timeoutForRun), + runtimeExceptionTestsBehaviour = configuration.runtimeExceptionTestsBehaviour, + ) + codegen.context.existingVariableNames = codegen.context.existingVariableNames.addAll(collectedImports.flatMap { listOfNotNull(it.moduleName, it.rootModuleName, it.importName) }) + val testCode = codegen.pythonGenerateAsStringWithTestReport( + testSets.map { testSet -> + CgMethodTestSet( + executableId = methodIds[testSet.method] as ExecutableId, + executions = testSet.executions, + clustersInfo = testSet.clustersInfo, + ) + }, + collectedImports, + ).generatedCode + return testCode + } + } + + abstract fun saveTests(testsCode: String) + + abstract fun notGeneratedTestsAction(testedFunctions: List) + + abstract fun processCoverageInfo(testSets: List) + + private fun getContainingClassName(testSets: List): String { + val containingClasses = testSets.map { it.method.containingPythonClass?.pythonName()?.replace(".", "") ?: "TopLevelFunctions" } + return containingClasses.toSet().first() + } + + private fun collectImports(notEmptyTests: List): Set { + val importParamModules = notEmptyTests.flatMap { testSet -> + testSet.executions.flatMap { execution -> + val params = (execution.stateAfter.parameters + execution.stateBefore.parameters).toMutableList() + val self = mutableListOf(execution.stateBefore.thisInstance, execution.stateAfter.thisInstance) + if (execution is PythonUtExecution) { + params.addAll(execution.stateInit.parameters) + self.add(execution.stateInit.thisInstance) + } + (params + self) + .filterNotNull() + .flatMap { utModel -> + (utModel as PythonModel).let { + it.allContainingClassIds + .filterNot { classId -> classId == pythonNoneClassId } + .map { classId -> PythonUserImport(importName_ = classId.moduleName) } + } + } + } + }.toSet() + val importResultModules = notEmptyTests.flatMap { testSet -> + testSet.executions.mapNotNull { execution -> + if (execution.result is UtExecutionSuccess) { + (execution.result as UtExecutionSuccess).let { result -> + (result.model as PythonModel).let { + it.allContainingClassIds + .filterNot { classId -> classId == pythonNoneClassId } + .map { classId -> PythonUserImport(importName_ = classId.moduleName) } + } + } + } else null + }.flatten() + }.toSet() + val rootModule = configuration.testFileInformation.moduleName.split(".").first() + val testRootModule = PythonUserImport(importName_ = rootModule) + val sysImport = PythonSystemImport("sys") + val osImport = PythonSystemImport("os") + val sysPathImports = relativizePaths( + configuration.testSourceRootPath, + configuration.sysPathDirectories + ).map { PythonSysPathImport(it) } + + val testFrameworkModule = + configuration.testFramework.testSuperClass?.let { PythonUserImport(importName_ = (it as PythonClassId).rootModuleName) } + + return (importParamModules + importResultModules + testRootModule + sysPathImports + listOf( + testFrameworkModule, osImport, sysImport + )) + .filterNotNull() + .filterNot { it.rootModuleName == pythonBuiltinsModuleName } + .toSet() + } + + private fun findMethodByHeader( + mypyStorage: MypyInfoBuild, + method: PythonMethodHeader, + curModule: String, + sourceFileContent: String + ): PythonMethod { + var containingClass: CompositeType? = null + val containingClassName = method.containingPythonClassId?.simpleName + val definition = if (containingClassName == null) { + mypyStorage.definitions[curModule]?.get(method.name)?.getUtBotDefinition() + } else { + containingClass = + mypyStorage.definitions[curModule]?.get(containingClassName)?.getUtBotType() as? CompositeType + ?: throw SelectedMethodIsNotAFunctionDefinition(method.name) + val descr = containingClass.pythonDescription() + if (descr !is PythonConcreteCompositeTypeDescription) + throw SelectedMethodIsNotAFunctionDefinition(method.name) + mypyStorage.definitions[curModule]?.get(containingClassName)?.type?.asUtBotType?.getPythonAttributes()?.first { + it.meta.name == method.name + } + } ?: throw SelectedMethodIsNotAFunctionDefinition(method.name) + val parsedFile = PythonParser(sourceFileContent).Module() + val funcDef = PythonCode.findFunctionDefinition(parsedFile, method) + val decorators = funcDef.decorators.map { PyDecorator.decoratorByName(it.name.toString()) } + + if (definition is PythonFunctionDefinition) { + return PythonBaseMethod( + name = method.name, + moduleFilename = method.moduleFilename, + containingPythonClass = containingClass, + codeAsString = funcDef.body.source, + definition = definition, + ast = funcDef.body + ) + } else if (decorators == listOf(PyDecorator.StaticMethod)) { + return PythonDecoratedMethod( + name = method.name, + moduleFilename = method.moduleFilename, + containingPythonClass = containingClass, + codeAsString = funcDef.body.source, + definition = definition, + ast = funcDef.body, + decorator = decorators.first() + ) + } else { + throw SelectedMethodIsNotAFunctionDefinition(method.name) + } + + } + + private fun relativizePaths(rootPath: Path?, paths: Set): Set = + if (rootPath != null) { + paths.map { path -> + rootPath.relativize(Path(path)).pathString + }.toSet() + } else { + paths + } + + private fun getCoverageInfo(testSets: List): CoverageInfo { + val covered = mutableSetOf() + val missed = mutableSetOf() + testSets.forEach { testSet -> + testSet.executions.forEach inner@{ execution -> + val coverage = execution.coverage ?: return@inner + covered.addAll(coverage.coveredInstructions.filterIsInstance()) + missed.addAll(coverage.missedInstructions.filterIsInstance()) + } + } + missed -= covered + val info = when (this.configuration.coverageOutputFormat) { + CoverageOutputFormat.Lines -> { + val coveredLines = getLinesList(covered) + val filteredMissed = filterMissedLines(coveredLines, missed) + val missedLines = getLinesList(filteredMissed) + CoverageInfo(coveredLines, missedLines) + } + CoverageOutputFormat.Instructions -> CoverageInfo( + getInstructionsList(covered), + getInstructionsList(missed) + ) + } + return CoverageInfo(info.covered.toSet().toList(), info.notCovered.toSet().toList()) + } + + protected fun getStringCoverageInfo(testSets: List): String { + val coverageInfo = getCoverageInfo(testSets) + val covered = coverageInfo.covered.map { it.toJson() } + val notCovered = coverageInfo.notCovered.map { it.toJson() } + return "{\"covered\": [${covered.joinToString(", ")}], \"notCovered\": [${notCovered.joinToString(", ")}]}" + } + + companion object { + fun sourceCodeAnalyze( + sysPathDirectories: Set, + pythonPath: String, + testFileInformation: TestFileInformation, + ): MypyConfig { + val mypyBuildRoot = TemporaryFileManager.assignTemporaryFile(tag = "mypyBuildRoot") + val buildDirectory = MypyBuildDirectory(mypyBuildRoot, sysPathDirectories) + val (mypyInfoBuild, mypyReportLines) = readMypyAnnotationStorageAndInitialErrors( + pythonPath, + testFileInformation.testedFilePath, + testFileInformation.moduleName, + buildDirectory + ) + return MypyConfig(mypyInfoBuild, mypyReportLines, buildDirectory) + } + + } +} + +data class SelectedMethodIsNotAFunctionDefinition(val methodName: String): Exception() \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt b/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt new file mode 100644 index 0000000000..0de57911d7 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt @@ -0,0 +1,308 @@ +package org.utbot.python + +import org.parsers.python.ast.Block +import org.utbot.framework.plugin.api.UtClusterInfo +import org.utbot.framework.plugin.api.UtError +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTreeModel +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utpython.types.PythonCallableTypeDescription +import org.utpython.types.PythonDefinition +import org.utpython.types.PythonDefinitionDescription +import org.utpython.types.PythonFuncItemDescription +import org.utpython.types.PythonFunctionDefinition +import org.utpython.types.general.CompositeType +import org.utpython.types.general.FunctionType +import org.utpython.types.pythonAnyType +import org.utpython.types.pythonDescription +import org.utpython.types.pythonName +import org.utpython.types.pythonTypeRepresentation +import org.utpython.types.utils.isNamed +import org.utpython.types.utils.isRequired + +data class PythonArgument( + val name: String, + val annotation: String?, + val isNamed: Boolean = false, +) + +class PythonMethodHeader( + val name: String, + val moduleFilename: String, + val containingPythonClassId: PythonClassId?, +) { + val fullname = containingPythonClassId?.let { "${it.typeName}.$name" } ?: name +} + + +interface PythonMethod { + val name: String + val moduleFilename: String + val containingPythonClass: CompositeType? + val codeAsString: String + val ast: Block + + fun methodSignature(): String + val hasThisArgument: Boolean + val arguments: List + val argumentsWithoutSelf: List + val thisObjectName: String? + val argumentsNames: List + val argumentsNamesWithoutSelf: List + + val methodType: FunctionType + val methodMeta: PythonDefinitionDescription + + fun makeCopyWithNewType(newFunctionType: FunctionType): PythonMethod + fun createShortForm(): Pair? + fun changeDefinition(signature: FunctionType) + + fun renderMethodName(): String +} + +class PythonBaseMethod( + override val name: String, + override val moduleFilename: String, + override val containingPythonClass: CompositeType?, + override val codeAsString: String, + private var definition: PythonFunctionDefinition, + override val ast: Block +) : PythonMethod { + override fun methodSignature(): String = "$name(" + arguments.joinToString(", ") { + "${it.name}: ${it.annotation ?: pythonAnyClassId.name}" + } + ")" + + /* + Check that the first argument is `self` of `cls`. + TODO: We should support `@property` decorator + */ + override val hasThisArgument: Boolean + get() = containingPythonClass != null && definition.meta.args.any { it.isSelf } + + override val arguments: List + get() { + val meta = definition.meta + val description = definition.type.pythonDescription() as PythonCallableTypeDescription + return (definition.type.arguments).mapIndexed { index, type -> + PythonArgument( + meta.args[index].name, + type.pythonTypeRepresentation(), // TODO: improve pythonTypeRepresentation + isNamed(description.argumentKinds[index]) + ) + } + } + + override val argumentsWithoutSelf: List + get() = if (hasThisArgument) arguments.drop(1) else arguments + + override val thisObjectName: String? + get() = if (hasThisArgument) arguments[0].name else null + + override val argumentsNames: List + get() = arguments.map { it.name } + + override val argumentsNamesWithoutSelf: List + get() = argumentsNames.drop(if (hasThisArgument) 1 else 0) + + override val methodType: FunctionType = definition.type + + override val methodMeta: PythonDefinitionDescription = definition.meta + + override fun makeCopyWithNewType(newFunctionType: FunctionType): PythonMethod { + val newDefinition = PythonFunctionDefinition(definition.meta, newFunctionType) + return PythonBaseMethod(name, moduleFilename, containingPythonClass, codeAsString, newDefinition, ast) + } + + override fun createShortForm(): Pair? { + val meta = methodType.pythonDescription() as PythonCallableTypeDescription + val argKinds = meta.argumentKinds + if (argKinds.any { !isRequired(it) }) { + val originalDef = definition + val shortType = meta.removeNotRequiredArgs(originalDef.type) + val shortMeta = PythonFuncItemDescription( + originalDef.meta.name, + originalDef.meta.args.filterIndexed { index, _ -> isRequired(argKinds[index]) } + ) + val additionalVars = originalDef.meta.args + .filterIndexed { index, _ -> !isRequired(argKinds[index]) } + .mapIndexed { index, arg -> + "${arg.name}: ${argumentsWithoutSelf[index].annotation ?: pythonAnyType.pythonTypeRepresentation()}" + } + .joinToString(separator = "\n", prefix = "\n") + val shortDef = PythonFunctionDefinition(shortMeta, shortType) + val shortMethod = PythonBaseMethod( + name, + moduleFilename, + containingPythonClass, + codeAsString, + shortDef, + ast + ) + return Pair(shortMethod, additionalVars) + } + return null + } + + fun changeDefinition(newDefinition: PythonDefinition) { + require(newDefinition is PythonFunctionDefinition) + definition = newDefinition + } + + override fun changeDefinition(signature: FunctionType) { + val newDefinition = PythonFunctionDefinition( + definition.meta, + signature + ) + changeDefinition(newDefinition) + } + + override fun renderMethodName(): String { + return name + } +} + +class PythonDecoratedMethod( + override val name: String, + override val moduleFilename: String, + override val containingPythonClass: CompositeType?, + override val codeAsString: String, + private var definition: PythonDefinition, + override val ast: Block, + val decorator: PyDecorator, +) : PythonMethod { + override val methodType: FunctionType = definition.type as FunctionType + override val methodMeta: PythonDefinitionDescription = definition.meta + val typeMeta: PythonCallableTypeDescription = definition.type.pythonDescription() as PythonCallableTypeDescription + + fun changeDefinition(newDefinition: PythonDefinition) { + require(checkDefinition(newDefinition)) { error("Cannot test non-function object") } + definition = newDefinition + } + + override fun changeDefinition(signature: FunctionType) { + val newDefinition = PythonDefinition( + methodMeta, + signature + ) + checkDefinition(newDefinition) + changeDefinition(newDefinition) + } + + override fun renderMethodName(): String { + return decorator.generateCallableName(this) + } + + override fun makeCopyWithNewType(newFunctionType: FunctionType): PythonMethod { + val newDefinition = PythonDefinition(methodMeta, newFunctionType) + return PythonDecoratedMethod( + name, moduleFilename, containingPythonClass, codeAsString, newDefinition, ast, decorator + ) + } + + override fun createShortForm(): Pair? = null + + init { + assert(checkDefinition(definition)) { error("Cannot test non-function object") } + } + override fun methodSignature(): String = "${decorator.generateCallableName(this)}(" + arguments.joinToString(", ") { + "${it.name}: ${it.annotation ?: pythonAnyClassId.name}" + } + ")" + + /* + Check that the first argument is `self` of `cls`. + */ + override val hasThisArgument: Boolean + get() = containingPythonClass != null && decorator.hasSelfArgument() + + override val arguments: List + get() { + return (methodType.arguments).mapIndexed { index, type -> + PythonArgument( + typeMeta.argumentNames[index] ?: "arg$index", + type.pythonTypeRepresentation(), // TODO: improve pythonTypeRepresentation + isNamed(typeMeta.argumentKinds[index]) + ) + } + } + + override val argumentsWithoutSelf: List + get() = if (hasThisArgument) arguments.drop(1) else arguments + + override val thisObjectName: String? + get() = if (hasThisArgument) arguments[0].name else null + + override val argumentsNames: List + get() = arguments.map { it.name } + + override val argumentsNamesWithoutSelf: List + get() = argumentsNames.drop(if (hasThisArgument) 1 else 0) + + companion object { + fun checkDefinition(definition: PythonDefinition): Boolean { + val type = definition.type + val meta = definition.type.pythonDescription() + return type is FunctionType && meta is PythonCallableTypeDescription + } + } +} + +data class PythonTestSet( + val method: PythonMethod, + val executions: List, + val errors: List, + val executionsNumber: Int = 0, + val clustersInfo: List> = listOf(null to executions.indices) +) + +data class FunctionArguments( + val thisObject: PythonTreeModel?, + val thisObjectName: String?, + val arguments: List, + val names: List, +) { + val allArguments: List = (listOf(thisObject) + arguments).filterNotNull() +} + +sealed interface PyDecorator { + fun generateCallableName(method: PythonMethod, baseName: String? = null): String + fun hasSelfArgument(): Boolean + val type: PythonClassId + + object StaticMethod : PyDecorator { + override fun generateCallableName(method: PythonMethod, baseName: String?) = + "${method.containingPythonClass!!.pythonName()}.${method.name}" + + override fun hasSelfArgument() = false + + override val type: PythonClassId = PythonClassId("staticmethod") + } + + object ClassMethod : PyDecorator { + override fun generateCallableName(method: PythonMethod, baseName: String?) = + "${method.containingPythonClass!!.pythonName()}.${method.name}" + + override fun hasSelfArgument() = true + + override val type: PythonClassId = PythonClassId("classmethod") + } + + class UnknownDecorator( + override val type: PythonClassId, + ) : PyDecorator { + override fun generateCallableName(method: PythonMethod, baseName: String?) = + baseName ?: method.name + + override fun hasSelfArgument() = true + } + + companion object { + fun decoratorByName(decoratorName: String): PyDecorator { + return when (decoratorName) { + "classmethod" -> ClassMethod + "staticmethod" -> StaticMethod + else -> UnknownDecorator(PythonClassId(decoratorName)) + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt new file mode 100644 index 0000000000..7c9479dc49 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt @@ -0,0 +1,40 @@ +package org.utbot.python.code + +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.python.PythonMethod +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.codegen.PythonCgLanguageAssistant +import org.utpython.types.general.UtType + + +object PythonCodeGenerator { + + fun generateMypyCheckCode( + method: PythonMethod, + methodAnnotations: Map, + directoriesForSysPath: Set, + moduleToImport: String, + namesInModule: Collection, + additionalVars: String + ): String { + val context = UtContext(this::class.java.classLoader) + withUtContext(context) { + val codegen = org.utbot.python.framework.codegen.model.PythonCodeGenerator( + PythonClassId("TopLevelFunction"), + paramNames = emptyMap>().toMutableMap(), + testFramework = PythonCgLanguageAssistant.getLanguageTestFrameworkManager().testFrameworks[0], + testClassPackageName = "", + ) + return codegen.generateMypyCheckCode( + method, + methodAnnotations, + directoriesForSysPath, + moduleToImport, + namesInModule, + additionalVars + ) + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt new file mode 100644 index 0000000000..446f38059f --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt @@ -0,0 +1,48 @@ +package org.utbot.python.code + +import org.parsers.python.ast.Block +import org.parsers.python.ast.ClassDefinition +import org.parsers.python.ast.FunctionDefinition +import org.parsers.python.ast.Module +import org.utbot.python.PythonMethodHeader +import org.utbot.python.newtyping.ast.ParsedFunctionDefinition +import org.utbot.python.newtyping.ast.parseClassDefinition +import org.utbot.python.newtyping.ast.parseFunctionDefinition + +object PythonCode { + + fun getTopLevelFunctions(parsedFile: Module): List { + return parsedFile.children().filterIsInstance() + } + + fun getTopLevelClasses(parsedFile: Module): List { + return parsedFile.children().filterIsInstance() + } + + fun getInnerClasses(classDef: ClassDefinition): List { + return classDef.children().filterIsInstance().flatMap {it.children() }.filterIsInstance() + } + + fun getClassMethods(class_: Block): List { + return class_.children().filterIsInstance() + } + + fun findFunctionDefinition(parsedFile: Module, method: PythonMethodHeader): ParsedFunctionDefinition { + return if (method.containingPythonClassId == null) { + getTopLevelFunctions(parsedFile).mapNotNull { + parseFunctionDefinition(it) + }.firstOrNull { + it.name.toString() == method.name + } ?: throw Exception("Couldn't find top-level function ${method.name}") + } else { + getTopLevelClasses(parsedFile) + .flatMap { listOf(it) + getInnerClasses(it) } + .mapNotNull { parseClassDefinition(it) } + .flatMap { getClassMethods(it.body) } + .mapNotNull { parseFunctionDefinition(it) } + .firstOrNull { + it.name.toString() == method.name + } ?: throw Exception("Couldn't find method ${method.name}") + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/coverage/CoverageApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/coverage/CoverageApi.kt new file mode 100644 index 0000000000..34403ad42a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/coverage/CoverageApi.kt @@ -0,0 +1,82 @@ +package org.utbot.python.coverage + +import org.utbot.framework.plugin.api.Coverage +import org.utbot.framework.plugin.api.Instruction + +enum class PythonCoverageMode { + Lines { + override fun toString() = "lines" + }, + + Instructions { + override fun toString() = "instructions" + }; + + companion object { + fun parse(name: String): PythonCoverageMode { + return PythonCoverageMode.values().first { + it.name.lowercase() == name.lowercase() + } + } + } +} + +data class PyInstruction( + val pyLineNumber: Int, + val offset: Long, + val fromMainFrame: Boolean, +) : Instruction( + "", + "", + pyLineNumber, + (pyLineNumber.toLong() to offset).toCoverageId() * 2 + fromMainFrame.toLong()) { + override fun toString(): String = listOf(lineNumber, offset, fromMainFrame).joinToString(":") + + constructor(lineNumber: Int) : this(lineNumber, lineNumber.toLong(), true) + constructor(lineNumber: Int, id: Long) : this(lineNumber, id.floorDiv(2).toPair().second, id % 2 == 1L) +} + +fun Boolean.toLong() = if (this) 1L else 0L + +fun String.toPyInstruction(): PyInstruction? { + val data = this.split(":") + when (data.size) { + 3 -> { + val line = data[0].toInt() + val offset = data[1].toLong() + val fromMainFrame = data[2].toInt() != 0 + return PyInstruction(line, offset, fromMainFrame) + } + 2 -> { + val line = data[0].toInt() + val offset = data[1].toLong() + return PyInstruction(line, offset, true) + } + 1 -> { + val line = data[0].toInt() + return PyInstruction(line) + } + else -> return null + } +} + +fun buildCoverage(coveredStatements: List, missedStatements: List): Coverage { + return Coverage( + coveredInstructions = coveredStatements, + instructionsCount = (coveredStatements.size + missedStatements.size).toLong(), + missedInstructions = missedStatements + ) +} + +enum class CoverageOutputFormat { + Lines, + Instructions; + + companion object { + fun parse(name: String): CoverageOutputFormat { + return CoverageOutputFormat.values().first { + it.name.lowercase() == name.lowercase() + } + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/coverage/CoverageIdGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/coverage/CoverageIdGenerator.kt new file mode 100644 index 0000000000..15aafacb54 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/coverage/CoverageIdGenerator.kt @@ -0,0 +1,13 @@ +package org.utbot.python.coverage + +import java.util.concurrent.atomic.AtomicLong + +object CoverageIdGenerator { + private const val LOWER_BOUND: Long = 1500_000_000 + + private val lastId: AtomicLong = AtomicLong(LOWER_BOUND) + + fun createId(): String { + return lastId.incrementAndGet().toString(radix = 16) + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/coverage/CoverageOutput.kt b/utbot-python/src/main/kotlin/org/utbot/python/coverage/CoverageOutput.kt new file mode 100644 index 0000000000..0bd4055da4 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/coverage/CoverageOutput.kt @@ -0,0 +1,41 @@ +package org.utbot.python.coverage + +sealed interface CoverageFormat { + fun toJson(): String +} +data class LineCoverage(val start: Int, val end: Int) : CoverageFormat { + override fun toJson(): String = "{\"start\": ${start}, \"end\": ${end}}" +} + +data class InstructionCoverage( + val line: Int, + val offset: Long, + val fromMainFrame: Boolean +) : CoverageFormat { + override fun toJson(): String = "{\"line\": ${line}, \"offset\": ${offset}, \"fromMainFrame\": ${fromMainFrame}}" +} + +data class CoverageInfo( + val covered: List, + val notCovered: List, +) + +fun getLinesList(instructions: Collection): List = + instructions + .map { it.lineNumber } + .sorted() + .fold(emptyList()) { acc, lineNumber -> + if (acc.isEmpty()) + return@fold listOf(LineCoverage(lineNumber, lineNumber)) + val elem = acc.last() + if (elem.end + 1 == lineNumber || elem.end == lineNumber ) + acc.dropLast(1) + listOf(LineCoverage(elem.start, lineNumber)) + else + acc + listOf(LineCoverage(lineNumber, lineNumber)) + } + +fun filterMissedLines(covered: Collection, missed: Collection): List = + missed.filterNot { missedInstruction -> covered.any { it.start <= missedInstruction.lineNumber && missedInstruction.lineNumber <= it.end } } + +fun getInstructionsList(instructions: Collection): List = + instructions.map { InstructionCoverage(it.lineNumber, it.offset, it.fromMainFrame) }.toSet().toList() diff --git a/utbot-python/src/main/kotlin/org/utbot/python/coverage/Utils.kt b/utbot-python/src/main/kotlin/org/utbot/python/coverage/Utils.kt new file mode 100644 index 0000000000..37e04a747d --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/coverage/Utils.kt @@ -0,0 +1,22 @@ +package org.utbot.python.coverage + +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min +import kotlin.math.sqrt + +fun Long.toPair(): Pair { + val n = ceil(sqrt(this + 2.0)).toLong() - 1 + val k = this - (n * n - 1) + return if (k <= n + 1) { + n + 1 to k + } else { + k to n + 1 + } +} + +fun Pair.toCoverageId(): Long { + val n = max(this.first, this.second) - 1 + val k = min(this.first, this.second) + return (n * n - 1) + k +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/engine/ExecutionFeedback.kt b/utbot-python/src/main/kotlin/org/utbot/python/engine/ExecutionFeedback.kt new file mode 100644 index 0000000000..79e43604c8 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/engine/ExecutionFeedback.kt @@ -0,0 +1,12 @@ +package org.utbot.python.engine + +import org.utbot.framework.plugin.api.UtError +import org.utbot.python.framework.api.python.PythonUtExecution + +sealed interface ExecutionFeedback +class ValidExecution(val utFuzzedExecution: PythonUtExecution): ExecutionFeedback +class InvalidExecution(val utError: UtError): ExecutionFeedback +class TypeErrorFeedback(val message: String) : ExecutionFeedback +class ArgumentsTypeErrorFeedback(val message: String) : ExecutionFeedback +class CachedExecutionFeedback(val cachedFeedback: ExecutionFeedback) : ExecutionFeedback +object FakeNodeFeedback : ExecutionFeedback \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/engine/ExecutionResultHandler.kt b/utbot-python/src/main/kotlin/org/utbot/python/engine/ExecutionResultHandler.kt new file mode 100644 index 0000000000..4445ce68a6 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/engine/ExecutionResultHandler.kt @@ -0,0 +1,126 @@ +package org.utbot.python.engine + +import mu.KotlinLogging +import org.utbot.framework.plugin.api.Coverage +import org.utbot.framework.plugin.api.DocStatement +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.TimeoutException +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.UtImplicitlyThrownException +import org.utbot.framework.plugin.api.UtTimeoutException +import org.utbot.python.PythonMethod +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.coverage.PyInstruction +import org.utbot.python.coverage.buildCoverage +import org.utbot.python.engine.utils.transformModelList +import org.utbot.python.evaluation.PythonEvaluationSuccess +import org.utbot.python.evaluation.serialization.toPythonTree +import org.utbot.python.framework.api.python.PythonTreeModel +import org.utbot.python.framework.api.python.PythonUtExecution +import org.utpython.types.general.UtType +import org.utpython.types.pythonTypeRepresentation +import org.utbot.python.utils.camelToSnakeCase +import org.utbot.summary.fuzzer.names.TestSuggestedInfo + +private val logger = KotlinLogging.logger { } + +object ExecutionResultHandler { + fun handleTimeoutResult( + method: PythonMethod, + arguments: List, + coveredInstructions: List, + summary: List? = null, + ): ExecutionFeedback { + val hasThisObject = method.hasThisArgument + val (beforeThisObject, beforeModelList) = if (hasThisObject) { + arguments.first() to arguments.drop(1) + } else { + null to arguments + } + val executionResult = UtTimeoutException(TimeoutException("Execution is too long")) + val testMethodName = suggestExecutionName(method, executionResult) + val coverage = Coverage(coveredInstructions) + val utFuzzedExecution = PythonUtExecution( + stateInit = EnvironmentModels(beforeThisObject, beforeModelList, emptyMap(), executableToCall = null), + stateBefore = EnvironmentModels(beforeThisObject, beforeModelList, emptyMap(), executableToCall = null), + stateAfter = EnvironmentModels(beforeThisObject, beforeModelList, emptyMap(), executableToCall = null), + diffIds = emptyList(), + result = executionResult, + coverage = coverage, + testMethodName = testMethodName.testName?.camelToSnakeCase(), + displayName = testMethodName.displayName, + summary = summary, + arguments = method.argumentsWithoutSelf + ) + return ValidExecution(utFuzzedExecution) + } + + fun handleSuccessResult( + method: PythonMethod, + configuration: PythonTestGenerationConfig, + types: List, + evaluationResult: PythonEvaluationSuccess, + summary: List? = null, + ): ExecutionFeedback { + val hasThisObject = method.hasThisArgument + + val resultModel = evaluationResult.stateAfter.getById(evaluationResult.resultId).toPythonTree(evaluationResult.stateAfter) + + if (evaluationResult.isException && (resultModel.type.name in configuration.prohibitedExceptions)) { // wrong type (sometimes mypy fails) + val errorMessage = "Evaluation with prohibited exception. Substituted types: ${ + types.joinToString { it.pythonTypeRepresentation() } + }. Exception type: ${resultModel.type.name}" + + logger.debug { errorMessage } + return TypeErrorFeedback(errorMessage) + } + val executionResult = + if (evaluationResult.isException) { + UtImplicitlyThrownException(Throwable(resultModel.type.toString()), false) + } + else { + UtExecutionSuccess(PythonTreeModel(resultModel)) + } + val testMethodName = suggestExecutionName(method, executionResult) + + val (thisObject, initModelList) = transformModelList(hasThisObject, evaluationResult.stateInit, evaluationResult.modelListIds) + val (beforeThisObject, beforeModelList) = transformModelList(hasThisObject, evaluationResult.stateBefore, evaluationResult.modelListIds) + val (afterThisObject, afterModelList) = transformModelList(hasThisObject, evaluationResult.stateAfter, evaluationResult.modelListIds) + + val utFuzzedExecution = PythonUtExecution( + stateInit = EnvironmentModels(thisObject, initModelList, emptyMap(), executableToCall = null), + stateBefore = EnvironmentModels(beforeThisObject, beforeModelList, emptyMap(), executableToCall = null), + stateAfter = EnvironmentModels(afterThisObject, afterModelList, emptyMap(), executableToCall = null), + diffIds = evaluationResult.diffIds, + result = executionResult, + coverage = buildCoverage(evaluationResult.coveredStatements, evaluationResult.missedStatements), + testMethodName = testMethodName.testName?.camelToSnakeCase(), + displayName = testMethodName.displayName, + summary = summary, + arguments = method.argumentsWithoutSelf, + ) + return ValidExecution(utFuzzedExecution) + } + + private fun suggestExecutionName( + method: PythonMethod, + executionResult: UtExecutionResult + ): TestSuggestedInfo { + val testSuffix = when (executionResult) { + is UtExecutionSuccess -> { + // can be improved + method.name + } + is UtExecutionFailure -> "${method.name}_with_exception" + else -> method.name + } + val testName = "test_$testSuffix" + return TestSuggestedInfo( + testName, + testName, + ) + } + +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/engine/ExecutionStorage.kt b/utbot-python/src/main/kotlin/org/utbot/python/engine/ExecutionStorage.kt new file mode 100644 index 0000000000..4ace0716db --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/engine/ExecutionStorage.kt @@ -0,0 +1,25 @@ +package org.utbot.python.engine + +import org.utbot.framework.plugin.api.UtError +import org.utbot.python.framework.api.python.PythonUtExecution + +class ExecutionStorage { + val fuzzingExecutions: MutableList = mutableListOf() + val fuzzingErrors: MutableList = mutableListOf() + + fun saveFuzzingExecution(feedback: ExecutionFeedback) { + when (feedback) { + is ValidExecution -> { + synchronized(fuzzingExecutions) { + fuzzingExecutions += feedback.utFuzzedExecution + } + } + is InvalidExecution -> { + synchronized(fuzzingErrors) { + fuzzingErrors += feedback.utError + } + } + else -> {} + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/engine/GlobalPythonEngine.kt b/utbot-python/src/main/kotlin/org/utbot/python/engine/GlobalPythonEngine.kt new file mode 100644 index 0000000000..89a98ebf8b --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/engine/GlobalPythonEngine.kt @@ -0,0 +1,87 @@ +package org.utbot.python.engine + +import mu.KotlinLogging +import org.utbot.python.MypyConfig +import org.utbot.python.PythonMethod +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.engine.fuzzing.FuzzingEngine +import org.utpython.types.PythonTypeHintsStorage +import org.utbot.python.newtyping.ast.visitor.Visitor +import org.utbot.python.newtyping.ast.visitor.constants.ConstantCollector +import org.utbot.python.newtyping.ast.visitor.hints.HintCollector +import org.utpython.types.mypy.GlobalNamesStorage +import org.utpython.types.mypy.MypyInfoBuild +import kotlin.concurrent.thread + +private val logger = KotlinLogging.logger {} + +class GlobalPythonEngine( + val method: PythonMethod, + val configuration: PythonTestGenerationConfig, + private val mypyConfig: MypyConfig, + val until: Long, +) { + val executionStorage = ExecutionStorage() + private val typeStorage = PythonTypeHintsStorage.get(mypyConfig.mypyStorage) + private val constantCollector = ConstantCollector(typeStorage) + private val hintCollector = constructHintCollector( + mypyConfig.mypyStorage, + typeStorage, + constantCollector, + method, + configuration.testFileInformation.moduleName + ) + + private fun runFuzzing() { + FuzzingEngine( + method, + configuration, + typeStorage, + hintCollector, + constantCollector, + mypyConfig.mypyStorage, + mypyConfig.mypyReportLine, + until, + executionStorage, + ).start() + } + + fun run() { + val fuzzing = thread( + start = true, + isDaemon = false, + name = "Fuzzer" + ) { + logger.info { " >>>>>>> Start fuzzer >>>>>>> " } + runFuzzing() + logger.info { " <<<<<<< Finish fuzzer <<<<<<< " } + } + fuzzing.join() + } + + private fun constructHintCollector( + mypyStorage: MypyInfoBuild, + typeStorage: PythonTypeHintsStorage, + constantCollector: ConstantCollector, + method: PythonMethod, + moduleName: String, + ): HintCollector { + + // initialize definitions first + mypyStorage.definitions[moduleName]!!.values.map { def -> + def.getUtBotDefinition() + } + + val mypyExpressionTypes = mypyStorage.exprTypes[moduleName]?.let { moduleTypes -> + moduleTypes.associate { + Pair(it.startOffset.toInt(), it.endOffset.toInt() + 1) to it.type.asUtBotType + } + } ?: emptyMap() + + val namesStorage = GlobalNamesStorage(mypyStorage) + val hintCollector = HintCollector(method, typeStorage, mypyExpressionTypes, namesStorage, moduleName) + val visitor = Visitor(listOf(hintCollector, constantCollector)) + visitor.visit(method.ast) + return hintCollector + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/engine/fuzzing/FuzzingEngine.kt b/utbot-python/src/main/kotlin/org/utbot/python/engine/fuzzing/FuzzingEngine.kt new file mode 100644 index 0000000000..3d0bc9fdbe --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/engine/fuzzing/FuzzingEngine.kt @@ -0,0 +1,372 @@ +package org.utbot.python.engine.fuzzing + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import org.utbot.framework.plugin.api.DocRegularStmt +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.framework.plugin.api.UtError +import org.utbot.fuzzing.Control +import org.utbot.fuzzing.NoSeedValueException +import org.utbot.fuzzing.fuzz +import org.utbot.fuzzing.utils.Trie +import org.utbot.python.FunctionArguments +import org.utbot.python.PythonMethod +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.coverage.CoverageIdGenerator +import org.utbot.python.coverage.PyInstruction +import org.utbot.python.engine.CachedExecutionFeedback +import org.utbot.python.engine.ExecutionFeedback +import org.utbot.python.engine.ExecutionResultHandler +import org.utbot.python.engine.ExecutionStorage +import org.utbot.python.engine.FakeNodeFeedback +import org.utbot.python.engine.InvalidExecution +import org.utbot.python.engine.ValidExecution +import org.utbot.python.engine.fuzzing.typeinference.createMethodAnnotationModifications +import org.utbot.python.evaluation.EvaluationCache +import org.utbot.python.evaluation.PythonEvaluationError +import org.utbot.python.evaluation.PythonEvaluationSuccess +import org.utbot.python.evaluation.PythonEvaluationTimeout +import org.utbot.python.evaluation.PythonWorkerManager +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.PythonTreeModel +import org.utbot.python.framework.api.python.PythonTreeWrapper +import org.utbot.python.framework.api.python.util.pythonStrClassId +import org.utbot.python.fuzzing.PythonExecutionResult +import org.utbot.python.fuzzing.PythonFeedback +import org.utbot.python.fuzzing.PythonFuzzedConcreteValue +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonFuzzing +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utpython.types.PythonTypeHintsStorage +import org.utbot.python.newtyping.ast.visitor.constants.ConstantCollector +import org.utbot.python.newtyping.ast.visitor.hints.HintCollector +import org.utpython.types.general.FunctionType +import org.utpython.types.general.UtType +import org.utbot.python.newtyping.inference.InvalidTypeFeedback +import org.utbot.python.newtyping.inference.SuccessFeedback +import org.utbot.python.newtyping.inference.baseline.BaselineAlgorithm +import org.utbot.python.newtyping.inference.baseline.MethodAndVars +import org.utpython.types.mypy.MypyInfoBuild +import org.utpython.types.mypy.MypyReportLine +import org.utpython.types.mypy.getErrorNumber +import org.utpython.types.pythonModules +import org.utpython.types.pythonTypeName +import org.utpython.types.pythonTypeRepresentation +import org.utpython.types.utils.getOffsetLine +import org.utbot.python.utils.ExecutionWithTimeoutMode +import org.utbot.python.utils.TestGenerationLimitManager +import org.utbot.python.utils.TimeoutMode +import org.utbot.python.utils.convertToTime +import java.net.ServerSocket +import kotlin.random.Random + +private val logger = KotlinLogging.logger {} +private const val RANDOM_TYPE_FREQUENCY = 4 +private const val MINIMAL_TIMEOUT_FOR_SUBSTITUTION = 4_000 // ms + +class FuzzingEngine( + val method: PythonMethod, + val configuration: PythonTestGenerationConfig, + private val typeStorage: PythonTypeHintsStorage, + private val hintCollector: HintCollector, + constantCollector: ConstantCollector, + private val mypyStorage: MypyInfoBuild, + private val mypyReport: List, + val until: Long, + private val executionStorage: ExecutionStorage, +) { + private val cache = EvaluationCache() + + private val constants: List = constantCollector.result + .mapNotNull { (type, value) -> + if (type.pythonTypeName() == pythonStrClassId.name && value is String) { + // Filter doctests + if (value.contains(">>>")) return@mapNotNull null + } + logger.debug { "Collected constant: ${type.pythonTypeRepresentation()}: $value" } + PythonFuzzedConcreteValue(type, value) + } + + fun start() { + logger.info { "Fuzzing until: ${until.convertToTime()}" } + val modifications = createMethodAnnotationModifications(method, typeStorage) + val now = System.currentTimeMillis() + val filterModifications = modifications + .take(minOf(modifications.size, maxOf(((until - now) / MINIMAL_TIMEOUT_FOR_SUBSTITUTION).toInt(), 1))) + .map { (modifiedMethod, additionalVars) -> + logger.info { "Substitution: ${modifiedMethod.methodSignature()}" } + MethodAndVars(modifiedMethod, additionalVars) + } + generateTests(method, filterModifications, until) + } + + private fun generateTests( + method: PythonMethod, + methodModifications: List, + until: Long, + ) { + val timeoutLimitManager = TestGenerationLimitManager( + TimeoutMode, + until, + ) + val namesInModule = mypyStorage.names + .getOrDefault(configuration.testFileInformation.moduleName, emptyList()) + .map { it.name } + .filter { + it.length < 4 || !it.startsWith("__") || !it.endsWith("__") + } + + val sourceFileContent = configuration.testFileInformation.testedFileContent + val algo = BaselineAlgorithm( + typeStorage, + hintCollector.result, + configuration.pythonPath, + MethodAndVars(method, ""), + methodModifications, + configuration.sysPathDirectories, + configuration.testFileInformation.moduleName, + namesInModule, + getErrorNumber( + mypyReport, + configuration.testFileInformation.testedFilePath, + getOffsetLine(sourceFileContent, method.ast.beginOffset), + getOffsetLine(sourceFileContent, method.ast.endOffset) + ), + mypyStorage.buildRoot.configFile, + randomTypeFrequency = RANDOM_TYPE_FREQUENCY, + dMypyTimeout = configuration.timeoutForRun, + ) + + val fuzzerCancellation = { configuration.isCanceled() || timeoutLimitManager.isCancelled() } + runBlocking { + runFuzzing( + method, + algo, + fuzzerCancellation, + until + ).collect { + executionStorage.saveFuzzingExecution(it) + } + } + } + + private fun runFuzzing( + method: PythonMethod, + typeInferenceAlgorithm: BaselineAlgorithm, + isCancelled: () -> Boolean, + until: Long + ): Flow = flow { + ServerSocket(0).use { serverSocket -> + logger.debug { "Server port: ${serverSocket.localPort}" } + val manager = try { + PythonWorkerManager( + method, + serverSocket, + configuration, + until, + ) + } catch (_: TimeoutException) { + return@use + } + logger.debug { "Executor manager was created successfully" } + + val initialType = (typeInferenceAlgorithm.expandState() ?: method.methodType) as FunctionType + + val pmd = PythonMethodDescription( + method.name, + constants, + typeStorage, + Trie(PyInstruction::id), + Random(0), + TestGenerationLimitManager(ExecutionWithTimeoutMode, until, isRootManager = true), + initialType, + ) + + try { + val parameters = method.methodType.arguments + if (parameters.isEmpty()) { + val result = fuzzingResultHandler(pmd, emptyList(), parameters, manager) + result?.let { + emit(it.executionFeedback) + } + } else { + try { + PythonFuzzing(typeStorage, typeInferenceAlgorithm, isCancelled) { description, arguments -> + if (isCancelled()) { + logger.debug { "Fuzzing process was interrupted" } + manager.disconnect() + return@PythonFuzzing PythonFeedback(control = Control.STOP) + } + if (System.currentTimeMillis() >= until) { + logger.debug { "Fuzzing process was interrupted by timeout" } + manager.disconnect() + return@PythonFuzzing PythonFeedback(control = Control.STOP) + } + + if (arguments.any { PythonTree.containsFakeNode(it.tree) }) { + logger.debug { "FakeNode in Python model" } + description.limitManager.addFakeNodeExecutions() + emit(FakeNodeFeedback) + return@PythonFuzzing PythonFeedback(control = Control.CONTINUE) + } else { + description.limitManager.restartFakeNode() + } + + val pair = Pair(description, arguments.map { PythonTreeWrapper(it.tree) }) + val mem = cache.get(pair) + if (mem != null) { + logger.debug { "Repeat in fuzzing ${arguments.map { it.tree }}" } + description.limitManager.addSuccessExecution() + emit(CachedExecutionFeedback(mem.executionFeedback)) + return@PythonFuzzing mem.fuzzingPlatformFeedback.fromCache() + } + val result = fuzzingResultHandler(description, arguments, parameters, manager) + if (result == null) { // timeout + manager.disconnect() + return@PythonFuzzing PythonFeedback(control = Control.STOP) + } + + cache.add(pair, result) + emit(result.executionFeedback) + return@PythonFuzzing result.fuzzingPlatformFeedback + }.fuzz(pmd) + } catch (_: NoSeedValueException) { + logger.debug { "Cannot fuzz values for types: ${parameters.map { it.pythonTypeRepresentation() }}" } + } + } + } finally { + manager.shutdown() + } + } + }.flowOn(Dispatchers.IO) + + private fun handleTimeoutResult( + arguments: List, + coveredInstructions: List, + ): ExecutionFeedback { + val summary = arguments + .zip(method.arguments) + .mapNotNull { it.first.summary?.replace("%var%", it.second.name) } + + return ExecutionResultHandler.handleTimeoutResult( + method, + arguments.map { PythonTreeModel(it.tree) }, + coveredInstructions, + summary.map { DocRegularStmt(it) } + ) + } + + private fun handleSuccessResult( + arguments: List, + types: List, + evaluationResult: PythonEvaluationSuccess, + ): ExecutionFeedback { + val summary = arguments + .zip(method.arguments) + .mapNotNull { it.first.summary?.replace("%var%", it.second.name) } + + return ExecutionResultHandler.handleSuccessResult( + method, + configuration, + types, + evaluationResult, + summary.map { DocRegularStmt(it) }, + ) + } + + private fun fuzzingResultHandler( + description: PythonMethodDescription, + arguments: List, + parameters: List, + manager: PythonWorkerManager, + ): PythonExecutionResult? { + val additionalModules = parameters.flatMap { it.pythonModules() } + + val argumentValues = arguments.map { PythonTreeModel(it.tree, it.tree.type) } + val moduleToImport = configuration.testFileInformation.moduleName + val argumentModules = argumentValues + .flatMap { it.allContainingClassIds } + .map { it.moduleName } + .filterNot { it.startsWith(moduleToImport) } + val localAdditionalModules = (additionalModules + argumentModules + moduleToImport).toSet() + + val (thisObject, modelList) = if (method.hasThisArgument) + Pair(argumentValues[0], argumentValues.drop(1)) + else + Pair(null, argumentValues) + val functionArguments = FunctionArguments( + thisObject, + method.thisObjectName, + modelList, + method.argumentsNamesWithoutSelf + ) + try { + val coverageId = CoverageIdGenerator.createId() + return when (val evaluationResult = + manager.runWithCoverage(functionArguments, localAdditionalModules, coverageId)) { + is PythonEvaluationError -> { + val stackTraceMessage = evaluationResult.stackTrace.joinToString("\n") + val utError = UtError( + "Error evaluation: ${evaluationResult.status}, ${evaluationResult.message}\n${stackTraceMessage}", + Throwable(stackTraceMessage) + ) + description.limitManager.addInvalidExecution() + logger.warn(stackTraceMessage) + PythonExecutionResult(InvalidExecution(utError), PythonFeedback(control = Control.PASS)) + } + + is PythonEvaluationTimeout -> { + val coveredInstructions = + manager.coverageReceiver.coverageStorage.getOrDefault(coverageId, mutableListOf()) + val utTimeoutException = handleTimeoutResult(arguments, coveredInstructions) + val trieNode: Trie.Node = + if (coveredInstructions.isEmpty()) + Trie.emptyNode() + else + description.tracer.add(coveredInstructions) + description.limitManager.addInvalidExecution() + PythonExecutionResult( + utTimeoutException, + PythonFeedback(control = Control.PASS, result = trieNode, SuccessFeedback) + ) + } + + is PythonEvaluationSuccess -> { + val coveredInstructions = evaluationResult.coveredStatements + + val result = handleSuccessResult( + arguments, + parameters, + evaluationResult, + ) + val typeInferenceFeedback = if (result is ValidExecution) SuccessFeedback else InvalidTypeFeedback + when (result) { + is ValidExecution -> { + val trieNode: Trie.Node = description.tracer.add(coveredInstructions) + description.limitManager.addSuccessExecution() + PythonExecutionResult( + result, + PythonFeedback(Control.CONTINUE, trieNode, typeInferenceFeedback) + ) + } + is InvalidExecution -> { + description.limitManager.addInvalidExecution() + PythonExecutionResult(result, PythonFeedback(control = Control.CONTINUE, typeInferenceFeedback = typeInferenceFeedback)) + } + else -> { + description.limitManager.addInvalidExecution() + PythonExecutionResult(result, PythonFeedback(control = Control.PASS, typeInferenceFeedback = typeInferenceFeedback)) + } + } + } + } + } catch (_: TimeoutException) { + logger.debug { "Fuzzing process was interrupted by timeout" } + return null + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/engine/fuzzing/typeinference/FunctionAnnotationUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/engine/fuzzing/typeinference/FunctionAnnotationUtils.kt new file mode 100644 index 0000000000..68f1861db4 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/engine/fuzzing/typeinference/FunctionAnnotationUtils.kt @@ -0,0 +1,91 @@ +package org.utbot.python.engine.fuzzing.typeinference + +import org.utbot.python.PythonMethod +import org.utpython.types.PythonSubtypeChecker +import org.utpython.types.PythonTypeHintsStorage +import org.utpython.types.PythonTypeVarDescription +import org.utpython.types.general.DefaultSubstitutionProvider +import org.utpython.types.general.FunctionType +import org.utpython.types.general.TypeParameter +import org.utpython.types.general.UtType +import org.utpython.types.general.getBoundedParameters +import org.utpython.types.general.hasBoundedParameters +import org.utpython.types.getPythonAttributeByName +import org.utpython.types.pythonDescription +import org.utpython.types.pythonTypeName +import org.utbot.python.utils.PriorityCartesianProduct + +private const val MAX_SUBSTITUTIONS = 10 +private val BAD_TYPES = setOf( + "builtins.function", + "builtins.super", + "builtins.type", + "builtins.slice", + "builtins.range", + "builtins.memoryview", + "builtins.object", +) + +fun getCandidates(param: TypeParameter, typeStorage: PythonTypeHintsStorage): List { + val meta = param.pythonDescription() as PythonTypeVarDescription + return when (meta.parameterKind) { + PythonTypeVarDescription.ParameterKind.WithConcreteValues -> { + param.constraints.map { it.boundary } + } + + PythonTypeVarDescription.ParameterKind.WithUpperBound -> { + typeStorage.simpleTypes.filter { + if (it.hasBoundedParameters()) + return@filter false + val bound = param.constraints.first().boundary + PythonSubtypeChecker.checkIfRightIsSubtypeOfLeft(bound, it, typeStorage) + } + } + } +} + +fun generateTypesAfterSubstitution(type: UtType, typeStorage: PythonTypeHintsStorage): List { + val params = type.getBoundedParameters() + return PriorityCartesianProduct(params.map { getCandidates(it, typeStorage) }).getSequence() + .filter { types -> types.all { it.pythonTypeName() !in BAD_TYPES } } + .map { subst -> + DefaultSubstitutionProvider.substitute(type, (params zip subst).associate { it }) + }.take(MAX_SUBSTITUTIONS).toList() +} + +fun substituteTypeParameters( + method: PythonMethod, + typeStorage: PythonTypeHintsStorage, +): List { + val newClasses = method.containingPythonClass?.let { + generateTypesAfterSubstitution(it, typeStorage) + } ?: listOf(null) + return newClasses.flatMap { newClass -> + val funcType = newClass?.getPythonAttributeByName(typeStorage, method.name)?.type as? FunctionType + ?: method.methodType + val newFuncTypes = generateTypesAfterSubstitution(funcType, typeStorage) + newFuncTypes.map { newFuncType -> + method.makeCopyWithNewType(newFuncType as FunctionType) + } + }.take(MAX_SUBSTITUTIONS) +} + + +data class ModifiedAnnotation( + val method: PythonMethod, + val additionalVars: String +) + +fun createMethodAnnotationModifications( + method: PythonMethod, + typeStorage: PythonTypeHintsStorage, +): List { + return substituteTypeParameters(method, typeStorage).flatMap { newMethod -> + listOfNotNull( + newMethod.createShortForm(), + (newMethod to "") + ) + }.map { + ModifiedAnnotation(it.first, it.second) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/engine/utils/ModelsTransformation.kt b/utbot-python/src/main/kotlin/org/utbot/python/engine/utils/ModelsTransformation.kt new file mode 100644 index 0000000000..69df63af45 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/engine/utils/ModelsTransformation.kt @@ -0,0 +1,30 @@ +package org.utbot.python.engine.utils + +import org.utbot.framework.plugin.api.UtModel +import org.utbot.python.evaluation.serialization.MemoryDump +import org.utbot.python.evaluation.serialization.toPythonTree +import org.utbot.python.framework.api.python.PythonTreeModel + +fun transformModelList( + hasThisObject: Boolean, + state: MemoryDump, + modelListIds: List +): Pair> { + val (stateThisId, resultModelListIds) = + if (hasThisObject) { + Pair(modelListIds.first(), modelListIds.drop(1)) + } else { + Pair(null, modelListIds) + } + val stateThisObject = stateThisId?.let { + PythonTreeModel( + state.getById(it).toPythonTree(state) + ) + } + val modelList = resultModelListIds.map { + PythonTreeModel( + state.getById(it).toPythonTree(state) + ) + } + return Pair(stateThisObject, modelList) +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/CodeEvaluationApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/CodeEvaluationApi.kt new file mode 100644 index 0000000000..c8a638bbf1 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/CodeEvaluationApi.kt @@ -0,0 +1,56 @@ +package org.utbot.python.evaluation + +import org.utbot.python.FunctionArguments +import org.utbot.python.PythonMethod +import org.utbot.python.coverage.PyInstruction +import org.utbot.python.evaluation.serialization.MemoryDump + +interface PythonCodeExecutor { + val method: PythonMethod + val moduleToImport: String + val pythonPath: String + val syspathDirectories: Set + val executionTimeout: Long + + fun run( + fuzzedValues: FunctionArguments, + additionalModulesToImport: Set + ): PythonEvaluationResult + + fun runWithCoverage( + fuzzedValues: FunctionArguments, + additionalModulesToImport: Set, + coverageId: String, + ): PythonEvaluationResult + + fun runWithCoverage( + pickledArguments: String, + coverageId: String, + ): PythonEvaluationResult + + fun stop() +} + +sealed class PythonEvaluationResult + +data class PythonEvaluationError( + val status: Int, + val message: String, + val stackTrace: List +) : PythonEvaluationResult() + +data class PythonEvaluationTimeout( + val message: String = "Timeout" +) : PythonEvaluationResult() + +data class PythonEvaluationSuccess( + val isException: Boolean, + val coveredStatements: List, + val missedStatements: List, + val stateInit: MemoryDump, + val stateBefore: MemoryDump, + val stateAfter: MemoryDump, + val diffIds: List, + val modelListIds: List, + val resultId: String, +) : PythonEvaluationResult() \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/EvaluationCache.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/EvaluationCache.kt new file mode 100644 index 0000000000..8890757b06 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/EvaluationCache.kt @@ -0,0 +1,26 @@ +package org.utbot.python.evaluation + +import mu.KotlinLogging +import org.utbot.python.framework.api.python.PythonTreeWrapper +import org.utbot.python.fuzzing.PythonExecutionResult +import org.utbot.python.fuzzing.PythonMethodDescription + +private const val MAX_CACHE_SIZE = 200 + +class EvaluationCache { + private val cache = mutableMapOf>, PythonExecutionResult>() + + fun add(key: Pair>, result: PythonExecutionResult) { + cache[key] = result + if (cache.size > MAX_CACHE_SIZE) { + val elemToDelete = cache.keys.maxBy { (_, args) -> + args.fold(0) { acc, arg -> arg.commonDiversity(acc) } + } + cache.remove(elemToDelete) + } + } + + fun get(key: Pair>): PythonExecutionResult? { + return cache[key] + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCodeSocketExecutor.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCodeSocketExecutor.kt new file mode 100644 index 0000000000..52f5e81beb --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCodeSocketExecutor.kt @@ -0,0 +1,205 @@ +package org.utbot.python.evaluation + +import org.utbot.python.FunctionArguments +import org.utbot.python.PythonMethod +import org.utbot.python.coverage.CoverageIdGenerator +import org.utbot.python.coverage.toPyInstruction +import org.utbot.python.evaluation.serialization.ExecutionRequest +import org.utbot.python.evaluation.serialization.ExecutionRequestSerializer +import org.utbot.python.evaluation.serialization.ExecutionResultDeserializer +import org.utbot.python.evaluation.serialization.FailExecution +import org.utbot.python.evaluation.serialization.MemoryMode +import org.utbot.python.evaluation.serialization.PythonExecutionResult +import org.utbot.python.evaluation.serialization.SuccessExecution +import org.utbot.python.evaluation.serialization.serializeObjects +import org.utpython.types.PythonCallableTypeDescription +import org.utpython.types.pythonDescription +import org.utpython.types.pythonTypeName +import org.utpython.types.utils.isNamed +import java.net.SocketException + +class PythonCodeSocketExecutor( + override val method: PythonMethod, + override val moduleToImport: String, + override val pythonPath: String, + override val syspathDirectories: Set, + override val executionTimeout: Long, +) : PythonCodeExecutor { + private lateinit var pythonWorker: PythonWorker + + constructor( + method: PythonMethod, + moduleToImport: String, + pythonPath: String, + syspathDirectories: Set, + executionTimeout: Long, + pythonWorker: PythonWorker + ) : this( + method, + moduleToImport, + pythonPath, + syspathDirectories, + executionTimeout + ) { + this.pythonWorker = pythonWorker + } + + override fun run( + fuzzedValues: FunctionArguments, + additionalModulesToImport: Set + ): PythonEvaluationResult { + val coverageId = CoverageIdGenerator.createId() + return runWithCoverage(fuzzedValues, additionalModulesToImport, coverageId) + } + + override fun runWithCoverage( + fuzzedValues: FunctionArguments, + additionalModulesToImport: Set, + coverageId: String + ): PythonEvaluationResult { + val (arguments, memory) = serializeObjects(fuzzedValues.allArguments.map { it.tree }) + + val meta = method.methodType.pythonDescription() as PythonCallableTypeDescription + val argKinds = meta.argumentKinds + val namedArgs = meta.argumentNames + .filterIndexed { index, _ -> !isNamed(argKinds[index]) } + + val (positionalArguments, namedArguments) = arguments + .zip(meta.argumentNames) + .partition { (_, name) -> + namedArgs.contains(name) + } + + val containingClass = method.containingPythonClass + val functionTextName = + if (containingClass == null) + method.name + else { + val fullname = "${containingClass.pythonTypeName()}.${method.name}" + fullname.drop(moduleToImport.length).removePrefix(".") + } + + val request = ExecutionRequest( + functionTextName, + moduleToImport, + additionalModulesToImport.toList(), + syspathDirectories.toList(), + positionalArguments.map { it.first }, + namedArguments.associate { it.second!! to it.first }, // here can be only-kwargs arguments + memory, + MemoryMode.REDUCE, + method.moduleFilename, + coverageId, + ) + val message = ExecutionRequestSerializer.serializeRequest(request) ?: error("Cannot serialize request to python executor") + try { + pythonWorker.sendData(message) + } catch (_: SocketException) { + return parseExecutionResult(FailExecution("Send data error")) + } + + val (status, response) = UtExecutorThread.run(pythonWorker, executionTimeout) + return when (status) { + UtExecutorThread.Status.TIMEOUT -> { + + PythonEvaluationTimeout() + } + + UtExecutorThread.Status.OK -> { + val executionResult = response?.let { + ExecutionResultDeserializer.parseExecutionResult(it) + ?: error("Cannot parse execution result: $it") + } ?: FailExecution("Execution result error") + + parseExecutionResult(executionResult) + } + } + } + + override fun runWithCoverage(pickledArguments: String, coverageId: String): PythonEvaluationResult { + val containingClass = method.containingPythonClass + val functionTextName = + if (containingClass == null) + method.name + else { + val fullname = "${containingClass.pythonTypeName()}.${method.name}" + fullname.drop(moduleToImport.length).removePrefix(".") + } + + val request = ExecutionRequest( + functionTextName, + moduleToImport, + emptyList(), + syspathDirectories.toList(), + emptyList(), + emptyMap(), + pickledArguments, + MemoryMode.PICKLE, + method.moduleFilename, + coverageId, + ) + val message = ExecutionRequestSerializer.serializeRequest(request) ?: error("Cannot serialize request to python executor") + try { + pythonWorker.sendData(message) + } catch (_: SocketException) { + return parseExecutionResult(FailExecution("Send data error")) + } + + val (status, response) = UtExecutorThread.run(pythonWorker, executionTimeout) + + return when (status) { + UtExecutorThread.Status.TIMEOUT -> { + PythonEvaluationTimeout() + } + + UtExecutorThread.Status.OK -> { + val executionResult = response?.let { + ExecutionResultDeserializer.parseExecutionResult(it) + ?: error("Cannot parse execution result: $it") + } ?: FailExecution("Execution result error") + + parseExecutionResult(executionResult) + } + } + } + + private fun parseExecutionResult(executionResult: PythonExecutionResult): PythonEvaluationResult { + val parsingException = PythonEvaluationError( + -1, + "Incorrect format of output", + emptyList() + ) + return when (executionResult) { + is SuccessExecution -> { + val stateBefore = ExecutionResultDeserializer.parseMemoryDump(executionResult.stateBefore) ?: return parsingException + val stateAfter = ExecutionResultDeserializer.parseMemoryDump(executionResult.stateAfter) ?: return parsingException + val stateInit = ExecutionResultDeserializer.parseMemoryDump(executionResult.stateInit) ?: stateBefore + val diffIds = executionResult.diffIds.map {it.toLong()} + val statements = executionResult.statements.mapNotNull { it.toPyInstruction() } + val missedStatements = executionResult.missedStatements.mapNotNull { it.toPyInstruction() } + PythonEvaluationSuccess( + executionResult.isException, + statements, + missedStatements, + stateInit, + stateBefore, + stateAfter, + diffIds, + executionResult.argsIds + executionResult.kwargsIds.values, + executionResult.resultId, + ) + } + is FailExecution -> { + PythonEvaluationError( + -2, + "Fail Execution", + executionResult.exception.split(System.lineSeparator()), + ) + } + } + } + + override fun stop() { + pythonWorker.stopServer() + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCoverageReceiver.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCoverageReceiver.kt new file mode 100644 index 0000000000..e28cc2654b --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCoverageReceiver.kt @@ -0,0 +1,61 @@ +package org.utbot.python.evaluation + +import mu.KotlinLogging +import org.utbot.python.coverage.PyInstruction +import org.utbot.python.coverage.toPyInstruction +import java.io.IOException +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.SocketException +import java.net.SocketTimeoutException +import kotlin.math.max + +class PythonCoverageReceiver( + val until: Long, +) : Thread() { + val coverageStorage = mutableMapOf>() + private val socket = DatagramSocket() + private val logger = KotlinLogging.logger {} + + init { + updateSoTimeout() + } + + private fun updateSoTimeout() { + socket.soTimeout = max((until - System.currentTimeMillis()).toInt(), 0) + } + + fun address(): Pair { + return "localhost" to socket.localPort.toString() + } + + fun kill() { + socket.close() + this.interrupt() + } + + override fun run() { + try { + while (true) { + updateSoTimeout() + val buf = ByteArray(256) + val request = DatagramPacket(buf, buf.size) + socket.receive(request) + val requestData = request.data.decodeToString().take(request.length).split(":", limit=2) + if (requestData.size == 2) { + val (id, line) = requestData + val instruction = line.toPyInstruction() + if (instruction != null) { + coverageStorage.getOrPut(id) { mutableListOf() }.add(instruction) + } + } + } + } catch (ex: SocketException) { + logger.debug { ex.message } + } catch (ex: IOException) { + logger.debug { "IO error: " + ex.message } + } catch (ex: SocketTimeoutException) { + logger.debug { "IO error: " + ex.message } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonWorker.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonWorker.kt new file mode 100644 index 0000000000..a38d3f2acc --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonWorker.kt @@ -0,0 +1,52 @@ +package org.utbot.python.evaluation + +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.net.Socket + +class PythonWorker( + private val clientSocket: Socket +) { + private val outStream = BufferedWriter(OutputStreamWriter(clientSocket.getOutputStream(), "UTF8")) + private val inStream = BufferedReader(InputStreamReader(clientSocket.getInputStream(), "UTF8")) + + fun stop() { + outStream.write("STOP") + outStream.flush() + } + + fun sendData(msg: String) { + outStream.write("DATA") + + val size = msg.encodeToByteArray().size + outStream.write(size.toString().padStart(16)) + outStream.flush() + + outStream.write(msg) + outStream.flush() + } + + fun receiveMessage(): String? { + val lengthLine = inStream.readLine() ?: return null + val length = lengthLine.toInt() + val buffer = CharArray(length) + val bytesRead = inStream.read(buffer) + if (bytesRead < length) // TODO: maybe we should add more time for reading? + return null + return String(buffer) + } + + private fun stopConnection() { + inStream.close() + outStream.close() + clientSocket.close() + } + + fun stopServer() { + stop() + stopConnection() + } + +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonWorkerManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonWorkerManager.kt new file mode 100644 index 0000000000..17e292dcb1 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonWorkerManager.kt @@ -0,0 +1,200 @@ +package org.utbot.python.evaluation + +import mu.KotlinLogging +import org.apache.logging.log4j.LogManager +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.python.FunctionArguments +import org.utbot.python.PythonMethod +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.coverage.PythonCoverageMode +import org.utbot.python.utils.TemporaryFileManager +import org.utbot.python.utils.getResult +import org.utbot.python.utils.startProcess +import java.lang.Long.max +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketException +import java.net.SocketTimeoutException +import kotlin.math.min + +private val logger = KotlinLogging.logger {} + +class PythonWorkerManager( + private val serverSocket: ServerSocket, + val pythonPath: String, + val until: Long, + private val coverageMeasureMode: PythonCoverageMode = PythonCoverageMode.Instructions, + private val sendCoverageContinuously: Boolean = true, + private val doNotGenerateStateAssertions: Boolean = false, + val pythonCodeExecutorConstructor: (PythonWorker) -> PythonCodeExecutor, +) { + constructor( + method: PythonMethod, + serverSocket: ServerSocket, + configuration: PythonTestGenerationConfig, + until: Long, + ) : this( + serverSocket, + configuration.pythonPath, + until, + configuration.coverageMeasureMode, + configuration.sendCoverageContinuously, + configuration.doNotGenerateStateAssertions, + { + PythonCodeSocketExecutor( + method, + configuration.testFileInformation.moduleName, + configuration.pythonPath, + configuration.sysPathDirectories, + min(configuration.timeoutForRun, until - System.currentTimeMillis()), + it, + ) + } + ) + + var timeout: Long = 0 + lateinit var process: Process + private lateinit var workerSocket: Socket + private lateinit var codeExecutor: PythonCodeExecutor + + val coverageReceiver = PythonCoverageReceiver(until) + + init { + coverageReceiver.start() + connect() + } + + private fun connect() { + val processStartTime = System.currentTimeMillis() + if (this::process.isInitialized && process.isAlive) { + process.destroy() + } + val logLevel = LogManager.getRootLogger().level.name() + if (serverSocket.isClosed) { + serverSocket.accept() + } + process = startProcess( + listOf( + pythonPath, + "-m", "utbot_executor", + "localhost", + serverSocket.localPort.toString(), + coverageReceiver.address().first, + coverageReceiver.address().second, + "--logfile", logfile.absolutePath, + "--loglevel", logLevel, // "DEBUG", "INFO", "WARNING", "ERROR" + "--coverage_type", coverageMeasureMode.toString(), // "lines", "instructions" + sendCoverageContinuously.toSendCoverageContinuouslyString(), // "--send_coverage", "--no-send_coverage" + doNotGenerateStateAssertions.toDoNotGenerateStateAssertionsString(), // "--generate_state_assertions", // "--no-generate_state_assertions" + ) + ) + timeout = max(until - processStartTime, 0) + if (this::workerSocket.isInitialized && !workerSocket.isClosed) { + workerSocket.close() + } + workerSocket = try { + serverSocket.soTimeout = timeout.toInt() + serverSocket.accept() + } catch (e: SocketTimeoutException) { + val result = getResult(process, max(until - processStartTime, 0)) + logger.debug { "utbot_executor exit value: ${result.exitValue}. stderr: ${result.stderr}, stdout: ${result.stdout}." } + process.destroy() + throw TimeoutException("Worker not connected") + } catch (e: SocketException) { + logger.debug { e.message } + throw SocketException("Worker not connected: $e") + } + logger.debug { "Worker connected successfully" } + +// workerSocket.soTimeout = timeoutForRun // TODO: maybe +eps for serialization/deserialization? + val pythonWorker = PythonWorker(workerSocket) + codeExecutor = pythonCodeExecutorConstructor(pythonWorker) + } + + fun disconnect() { + workerSocket.close() + process.destroyForcibly() + } + + private fun reconnect() { + disconnect() + connect() + } + + fun shutdown() { + disconnect() + coverageReceiver.kill() + } + + fun runWithCoverage( + fuzzedValues: FunctionArguments, + additionalModulesToImport: Set, + coverageId: String + ): PythonEvaluationResult { + val evaluationResult = try { + codeExecutor.runWithCoverage(fuzzedValues, additionalModulesToImport, coverageId) + } catch (_: SocketTimeoutException) { + logger.debug { "Socket timeout" } + reconnect() + PythonEvaluationTimeout() + } + if (evaluationResult is PythonEvaluationError || evaluationResult is PythonEvaluationTimeout) { + reconnect() + } + return evaluationResult + } + + fun runWithCoverage( + pickledArguments: String, + coverageId: String + ): PythonEvaluationResult { + val evaluationResult = try { + codeExecutor.runWithCoverage(pickledArguments, coverageId) + } catch (_: SocketTimeoutException) { + logger.debug { "Socket timeout" } + reconnect() + PythonEvaluationTimeout() + } + if (evaluationResult is PythonEvaluationError || evaluationResult is PythonEvaluationTimeout) { + reconnect() + } + return evaluationResult + } + + fun run( + fuzzedValues: FunctionArguments, + additionalModulesToImport: Set + ): PythonEvaluationResult { + val evaluationResult = try { + codeExecutor.run(fuzzedValues, additionalModulesToImport) + } catch (_: SocketTimeoutException) { + logger.debug { "Socket timeout" } + reconnect() + PythonEvaluationTimeout() + } + if (evaluationResult is PythonEvaluationError) { + reconnect() + } + return evaluationResult + } + + companion object { + val logfile = TemporaryFileManager.createTemporaryFile("", "utbot_executor.log", "log", true) + + fun Boolean.toSendCoverageContinuouslyString(): String { + return if (this) { + "--send_coverage" + } else { + "--no-send_coverage" + } + } + + fun Boolean.toDoNotGenerateStateAssertionsString(): String { + return if (this) { + "--no-generate_state_assertions" + } else { + "--generate_state_assertions" + } + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/UtExecutorThread.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/UtExecutorThread.kt new file mode 100644 index 0000000000..877f47cfed --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/UtExecutorThread.kt @@ -0,0 +1,42 @@ +package org.utbot.python.evaluation + +import java.net.SocketException +import java.util.concurrent.Callable +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +class UtExecutorThread { + enum class Status { + TIMEOUT, + OK, + } + + companion object { + fun run(worker: PythonWorker, executionTimeout: Long): Pair { + val executor = Executors.newSingleThreadExecutor() + val future = executor.submit(Task(worker)) + + val result = try { + Status.OK to future.get(executionTimeout, TimeUnit.MILLISECONDS) + } catch (ex: TimeoutException) { + future.cancel(true) + Status.TIMEOUT to null + } + executor.shutdown() + return result + } + } +} + +class Task( + private val worker: PythonWorker +) : Callable { + override fun call(): String? { + return try { + worker.receiveMessage() + } catch (ex: SocketException) { + null + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/serialization/ExecutionRequestSerializer.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/serialization/ExecutionRequestSerializer.kt new file mode 100644 index 0000000000..a8db047a6a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/serialization/ExecutionRequestSerializer.kt @@ -0,0 +1,34 @@ +package org.utbot.python.evaluation.serialization + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +object ExecutionRequestSerializer { + private val moshi = Moshi.Builder() + .addLast(KotlinJsonAdapterFactory()) + .build() + + private val jsonAdapter = moshi.adapter(ExecutionRequest::class.java) + + fun serializeRequest(request: ExecutionRequest): String? { + return jsonAdapter.toJson(request) + } +} + +enum class MemoryMode { + PICKLE, + REDUCE +} + +data class ExecutionRequest( + val functionName: String, + val functionModule: String, + val imports: List, + val syspaths: List, + val argumentsIds: List, + val kwargumentsIds: Map, + val serializedMemory: String, + val memoryMode: MemoryMode, + val filepath: String, + val coverageId: String, +) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/serialization/ExecutionResultDeserializer.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/serialization/ExecutionResultDeserializer.kt new file mode 100644 index 0000000000..0815248c2e --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/serialization/ExecutionResultDeserializer.kt @@ -0,0 +1,65 @@ +package org.utbot.python.evaluation.serialization + +import com.squareup.moshi.JsonEncodingException +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +object ExecutionResultDeserializer { + private val moshi = Moshi.Builder() + .add( + PolymorphicJsonAdapterFactory.of(PythonExecutionResult::class.java, "status") + .withSubtype(SuccessExecution::class.java, "success") + .withSubtype(FailExecution::class.java, "fail") + ) + .add( + PolymorphicJsonAdapterFactory.of(MemoryObject::class.java, "strategy") + .withSubtype(ReprMemoryObject::class.java, "repr") + .withSubtype(ListMemoryObject::class.java, "list") + .withSubtype(DictMemoryObject::class.java, "dict") + .withSubtype(ReduceMemoryObject::class.java, "reduce") + .withSubtype(IteratorMemoryObject::class.java, "iterator") + .withSubtype(NdarrayMemoryObject::class.java, "ndarray") + ) + .addLast(KotlinJsonAdapterFactory()) + .build() + + private val jsonAdapter = moshi.adapter(PythonExecutionResult::class.java) + private val jsonAdapterMemoryDump = moshi.adapter(MemoryDump::class.java) + + fun parseExecutionResult(content: String): PythonExecutionResult? { + try { + return jsonAdapter.fromJson(content) ?: error("Parsing error with: $content") + } catch (_: JsonEncodingException) { + println(content) + } + return null + } + + fun parseMemoryDump(content: String): MemoryDump? { + return if (content.isNotEmpty()) { + jsonAdapterMemoryDump.fromJson(content) + } else { + null + } + } +} + +sealed class PythonExecutionResult + +data class SuccessExecution( + val isException: Boolean, + val statements: List, + val missedStatements: List, + val stateInit: String, + val stateBefore: String, + val stateAfter: String, + val diffIds: List, + val argsIds: List, + val kwargsIds: Map, + val resultId: String, +): PythonExecutionResult() + +data class FailExecution( + val exception: String, +): PythonExecutionResult() diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/serialization/PythonObjectParser.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/serialization/PythonObjectParser.kt new file mode 100644 index 0000000000..98a5d3adbe --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/serialization/PythonObjectParser.kt @@ -0,0 +1,351 @@ +package org.utbot.python.evaluation.serialization + +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTree + +object PythonObjectParser { + private val moshi = Moshi.Builder() + .add( + PolymorphicJsonAdapterFactory.of(MemoryObject::class.java, "strategy") + .withSubtype(ReprMemoryObject::class.java, "repr") + .withSubtype(ListMemoryObject::class.java, "list") + .withSubtype(DictMemoryObject::class.java, "dict") + .withSubtype(ReduceMemoryObject::class.java, "reduce") + .withSubtype(IteratorMemoryObject::class.java, "iterator") + .withSubtype(NdarrayMemoryObject::class.java, "ndarray") + ) + .addLast(KotlinJsonAdapterFactory()) + .build() + + private val jsonAdapter = moshi.adapter(MemoryDump::class.java) + + fun parseDumpedObjects(jsonWithDump: String): MemoryDump { + return jsonAdapter.fromJson(jsonWithDump) ?: error("Couldn't parse json dump") + } + + fun serializeMemory(memory: MemoryDump): String { + return jsonAdapter.toJson(memory) ?: error("Couldn't serialize dump to json") + } +} + +class MemoryDump( + private val objects: MutableMap +) { + fun contains(id: String): Boolean { + return objects.containsKey(id) + } + + fun getById(id: String): MemoryObject { + return objects[id]!! + } + + fun addObject(value: MemoryObject) { + objects[value.id] = value + } +} + +data class TypeInfo( + val module: String, + val kind: String, +) { + val qualname: String = if (module.isEmpty()) kind else "$module.$kind" +} + +sealed class MemoryObject( + val id: String, + val typeinfo: TypeInfo, + val comparable: Boolean, +) { + val qualname: String = typeinfo.qualname +} + +class ReprMemoryObject( + id: String, + typeinfo: TypeInfo, + comparable: Boolean, + val value: String, +) : MemoryObject(id, typeinfo, comparable) + +class ListMemoryObject( + id: String, + typeinfo: TypeInfo, + comparable: Boolean, + val items: List, +) : MemoryObject(id, typeinfo, comparable) + +class DictMemoryObject( + id: String, + typeinfo: TypeInfo, + comparable: Boolean, + val items: Map, +) : MemoryObject(id, typeinfo, comparable) + +class IteratorMemoryObject( + id: String, + typeinfo: TypeInfo, + comparable: Boolean, + val items: List, + val exception: TypeInfo, +) : MemoryObject(id, typeinfo, comparable) + +class ReduceMemoryObject( + id: String, + typeinfo: TypeInfo, + comparable: Boolean, + val constructor: TypeInfo, + val args: String, + val state: String, + val listitems: String, + val dictitems: String +) : MemoryObject(id, typeinfo, comparable) + +class NdarrayMemoryObject( + id: String, + typeinfo: TypeInfo, + comparable: Boolean, + val items: List, + val dimensions: List +) : MemoryObject(id, typeinfo, comparable) + +fun PythonTree.PythonTreeNode.toMemoryObject(memoryDump: MemoryDump, reload: Boolean = false): String { + val id = this.id.toString() + if (memoryDump.contains(id) && !reload) return id + + val typeinfo = TypeInfo(this.type.moduleName, this.type.typeName) + val obj = when (this) { + is PythonTree.PrimitiveNode -> { + ReprMemoryObject( + id, + typeinfo, + this.comparable, + this.repr.replace("\n", "\\n").replace("\r", "\\r") + ) + } + + is PythonTree.NDArrayNode -> { // TODO: Optimize for ndarray + val items = this.items.entries + .map { it.value.toMemoryObject(memoryDump) } + val dimensions = this.dimensions + NdarrayMemoryObject(id, typeinfo, this.comparable, items, dimensions) // TODO: Ndarray to memory object + } + + is PythonTree.ListNode -> { + val draft = ListMemoryObject(id, typeinfo, this.comparable, emptyList()) + memoryDump.addObject(draft) + + val items = this.items.entries + .map { it.value.toMemoryObject(memoryDump) } + ListMemoryObject(id, typeinfo, this.comparable, items) + } + + is PythonTree.TupleNode -> { + val items = this.items.entries + .map { it.value.toMemoryObject(memoryDump) } + ListMemoryObject(id, typeinfo, this.comparable, items) + } + + is PythonTree.SetNode -> { + val items = this.items.map { it.toMemoryObject(memoryDump) } + ListMemoryObject(id, typeinfo, this.comparable, items) + } + + is PythonTree.DictNode -> { + val draft = DictMemoryObject(id, typeinfo, this.comparable, emptyMap()) + memoryDump.addObject(draft) + + val items = this.items.entries + .associate { + it.key.toMemoryObject(memoryDump) to it.value.toMemoryObject(memoryDump) + } + DictMemoryObject(id, typeinfo, this.comparable, items) + } + + is PythonTree.IteratorNode -> { + val items = this.items.entries + .map { it.value.toMemoryObject(memoryDump) } + IteratorMemoryObject(id, typeinfo, this.comparable, items, TypeInfo(this.exception.moduleName, this.exception.typeName)) + } + + is PythonTree.ReduceNode -> { + val argsIds = PythonTree.ListNode(this.args.withIndex().associate { it.index to it.value }.toMutableMap()) + val draft = ReduceMemoryObject( + id, + typeinfo, + this.comparable, + TypeInfo( + this.constructor.moduleName, + this.constructor.typeName, + ), + argsIds.toMemoryObject(memoryDump), + "", + "", + "", + ) + memoryDump.addObject(draft) + + val stateObjId = if (this.customState) { + this.state["state"]!! + } else { + PythonTree.DictNode(this.state.entries.associate { + PythonTree.fromString(it.key) to it.value + }.toMutableMap()) + } + val listItemsIds = + PythonTree.ListNode(this.listitems.withIndex().associate { it.index to it.value }.toMutableMap()) + val dictItemsIds = PythonTree.DictNode(this.dictitems.toMutableMap()) + ReduceMemoryObject( + id, + typeinfo, + this.comparable, + TypeInfo( + this.constructor.moduleName, + this.constructor.typeName, + ), + argsIds.toMemoryObject(memoryDump), + stateObjId.toMemoryObject(memoryDump), + listItemsIds.toMemoryObject(memoryDump), + dictItemsIds.toMemoryObject(memoryDump), + ) + } + + else -> { + error("Invalid PythonTree.PythonTreeNode $this") + } + } + memoryDump.addObject(obj) + return obj.id +} + +fun MemoryObject.toPythonTree( + memoryDump: MemoryDump, + visited: MutableMap = mutableMapOf() +): PythonTree.PythonTreeNode { + val obj = visited.getOrPut(this.id) { + val id = this.id.toLong() + val obj = when (this) { + is ReprMemoryObject -> { + PythonTree.PrimitiveNode( + id, + PythonClassId(this.typeinfo.module, this.typeinfo.kind), + value + ) + } + + is DictMemoryObject -> { + val draft = PythonTree.DictNode( + id, + mutableMapOf() + ) + visited[this.id] = draft + items.entries.map { + draft.items[memoryDump.getById(it.key).toPythonTree(memoryDump, visited)] = + memoryDump.getById(it.value).toPythonTree(memoryDump, visited) + } + draft + } + + is ListMemoryObject -> { + val draft = when (this.qualname) { + "builtins.tuple" -> PythonTree.TupleNode(id, mutableMapOf()) + "builtins.set" -> PythonTree.SetNode(id, mutableSetOf()) + else -> PythonTree.ListNode(id, mutableMapOf()) + } + visited[this.id] = draft + + items.mapIndexed { index, valueId -> + val value = memoryDump.getById(valueId).toPythonTree(memoryDump, visited) + when (draft) { + is PythonTree.TupleNode -> draft.items[index] = value + is PythonTree.SetNode -> draft.items.add(value) + is PythonTree.ListNode -> draft.items[index] = value + else -> {} + } + } + draft + } + + is IteratorMemoryObject -> { + val draft = PythonTree.IteratorNode(id, mutableMapOf(), PythonClassId(exception.module, exception.kind)) + + visited[this.id] = draft + + items.mapIndexed { index, valueId -> + val value = memoryDump.getById(valueId).toPythonTree(memoryDump, visited) + draft.items[index] = value + } + draft + } + + is ReduceMemoryObject -> { + val stateObjsDraft = memoryDump.getById(state) + val customState = stateObjsDraft !is DictMemoryObject + + val argumentsDump = memoryDump.getById(args) + if (argumentsDump is ReprMemoryObject && argumentsDump.value == "None") { // This is global variable + return@getOrPut PythonTree.PrimitiveNode( + PythonClassId(this.constructor.module, this.constructor.kind), + this.constructor.qualname + ) + } + val arguments = argumentsDump as ListMemoryObject + val listitemsObjs = memoryDump.getById(listitems) as ListMemoryObject + val dictitemsObjs = memoryDump.getById(dictitems) as DictMemoryObject + val prevObj = PythonTree.ReduceNode( + id, + PythonClassId(this.typeinfo.module, this.typeinfo.kind), + PythonClassId(this.constructor.module, this.constructor.kind), + arguments.items.map { memoryDump.getById(it).toPythonTree(memoryDump, visited) }, + ) + prevObj.customState = customState + visited[this.id] = prevObj + + prevObj.state = if (prevObj.customState) { + val stateObjs = memoryDump.getById(state) + mutableMapOf("state" to stateObjs.toPythonTree(memoryDump, visited)) + } else { + val stateObjs = memoryDump.getById(state) as DictMemoryObject + stateObjs.items.entries.associate { + (memoryDump.getById(it.key).toPythonTree(memoryDump, visited) as PythonTree.PrimitiveNode) + .repr.drop(1).dropLast(1) to + memoryDump.getById(it.value).toPythonTree(memoryDump, visited) + }.toMutableMap() + } + prevObj.listitems = listitemsObjs.items.map { memoryDump.getById(it).toPythonTree(memoryDump, visited) } + prevObj.dictitems = dictitemsObjs.items.entries.associate { + memoryDump.getById(it.key).toPythonTree(memoryDump, visited) to + memoryDump.getById(it.value).toPythonTree(memoryDump, visited) + } + prevObj + } + + is NdarrayMemoryObject -> { + val draft = when (this.qualname) { + "numpy.ndarray" -> PythonTree.NDArrayNode(id, mutableMapOf(), this.dimensions) + else -> PythonTree.ListNode(id, mutableMapOf()) + } + visited[this.id] = draft + + items.mapIndexed { index, valueId -> + val value = memoryDump.getById(valueId).toPythonTree(memoryDump, visited) + when (draft) { + is PythonTree.NDArrayNode -> draft.items[index] = value + else -> {} + } + } + draft + } + } + obj.comparable = this.comparable + return obj + } + return obj +} + +fun serializeObjects(objs: List): Pair, String> { + val memoryDump = MemoryDump(emptyMap().toMutableMap()) + val ids = objs.map { it.toMemoryObject(memoryDump) } + return Pair(ids, PythonObjectParser.serializeMemory(memoryDump)) +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonApi.kt new file mode 100644 index 0000000000..e2c6ca43ba --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonApi.kt @@ -0,0 +1,136 @@ +package org.utbot.python.framework.api.python + +import org.utbot.framework.plugin.api.* +import org.utbot.python.PythonArgument +import org.utbot.python.framework.api.python.util.moduleOfType + +/** + * PythonClassId represents Python type. + * NormalizedPythonAnnotation represents annotation after normalization. + * + * Example of PythonClassId, but not NormalizedPythonAnnotation: + * builtins.list (normalized annotation is typing.List[typing.Any]) + */ + +const val pythonBuiltinsModuleName = "builtins" + +class PythonClassId( + val moduleName: String, + val typeName: String, +) : ClassId("$moduleName.$typeName") { + constructor(fullName: String) : this( + moduleOfType(fullName) ?: pythonBuiltinsModuleName, + fullName.removePrefix(moduleOfType(fullName) ?: pythonBuiltinsModuleName).removePrefix(".") + ) + override fun toString(): String = canonicalName + val rootModuleName: String = moduleName.split(".").first() + override val simpleName: String = typeName + override val canonicalName = name + override val packageName = moduleName + val prettyName: String = if (rootModuleName == pythonBuiltinsModuleName) + name.split(".", limit=2).last() + else + name +} + +open class RawPythonAnnotation( + annotation: String +): ClassId(annotation) + +class NormalizedPythonAnnotation( + annotation: String +) : RawPythonAnnotation(annotation) + +class PythonMethodId( + override val classId: PythonClassId, // may be a fake class for top-level functions + override val name: String, + override val returnType: RawPythonAnnotation, + override val parameters: List, +) : MethodId(classId, name, returnType, parameters) { + val moduleName: String = classId.moduleName + val rootModuleName: String = this.toString().split(".")[0] + override fun toString(): String = if (moduleName.isNotEmpty()) "$moduleName.$name" else name +} + +sealed class PythonModel(classId: PythonClassId): UtModel(classId) { + open val allContainingClassIds: Set = setOf(classId) +} + +class PythonTreeModel( + val tree: PythonTree.PythonTreeNode, + classId: PythonClassId, +): PythonModel(classId) { + constructor(tree: PythonTree.PythonTreeNode) : this(tree, tree.type) + + override val allContainingClassIds: Set + get() { return findAllContainingClassIds(setOf(this.tree)) } + + private fun findAllContainingClassIds(visited: Set): Set { + val children = tree.children.map { PythonTreeModel(it, it.type) } + val newVisited = (visited + setOf(this.tree)).toMutableSet() + val childrenClassIds = children.filterNot { newVisited.contains(it.tree) }.flatMap { + newVisited.add(it.tree) + it.findAllContainingClassIds(newVisited) + } + return super.allContainingClassIds + childrenClassIds + } + + override fun equals(other: Any?): Boolean { + if (other !is PythonTreeModel) { + return false + } + return tree == other.tree + } + + override fun hashCode(): Int { + return tree.hashCode() + } +} + +class PythonUtExecution( + val stateInit: EnvironmentModels, + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + val diffIds: List, + result: UtExecutionResult, + val arguments: List, + coverage: Coverage? = null, + summary: List? = null, + testMethodName: String? = null, + displayName: String? = null, +) : UtExecution(stateBefore, stateAfter, result, coverage, summary, testMethodName, displayName) { + init { + stateInit.parameters.zip(stateBefore.parameters).map { (init, before) -> + if (init is PythonTreeModel && before is PythonTreeModel) { + init.tree.comparable = before.tree.comparable + } + } + val init = stateInit.thisInstance + val before = stateBefore.thisInstance + if (init is PythonTreeModel && before is PythonTreeModel) { + init.tree.comparable = before.tree.comparable + } + } + override fun copy( + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + result: UtExecutionResult, + coverage: Coverage?, + summary: List?, + testMethodName: String?, + displayName: String? + ): UtExecution { + return PythonUtExecution( + stateInit = stateInit, + stateBefore = stateBefore, + stateAfter = stateAfter, + diffIds = diffIds, + result = result, + coverage = coverage, + summary = summary, + testMethodName = testMethodName, + displayName = displayName, + arguments = arguments + ) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonTree.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonTree.kt new file mode 100644 index 0000000000..20e378f07b --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonTree.kt @@ -0,0 +1,443 @@ +package org.utbot.python.framework.api.python + +import org.utbot.python.framework.api.python.util.pythonBoolClassId +import org.utbot.python.framework.api.python.util.pythonDictClassId +import org.utbot.python.framework.api.python.util.pythonFloatClassId +import org.utbot.python.framework.api.python.util.pythonIntClassId +import org.utbot.python.framework.api.python.util.pythonIteratorClassId +import org.utbot.python.framework.api.python.util.pythonNdarrayClassId +import org.utbot.python.framework.api.python.util.pythonListClassId +import org.utbot.python.framework.api.python.util.pythonNoneClassId +import org.utbot.python.framework.api.python.util.pythonObjectClassId +import org.utbot.python.framework.api.python.util.pythonSetClassId +import org.utbot.python.framework.api.python.util.pythonStopIterationClassId +import org.utbot.python.framework.api.python.util.pythonStrClassId +import org.utbot.python.framework.api.python.util.pythonTupleClassId +import org.utbot.python.framework.api.python.util.toPythonRepr +import org.utpython.types.general.UtType +import org.utpython.types.pythonTypeName +import java.math.BigDecimal +import java.math.BigInteger +import java.util.* +import java.util.concurrent.atomic.AtomicLong + + +object PythonTree { + + const val MAX_ITERATOR_SIZE = 1000 + + fun isRecursiveObject(tree: PythonTreeNode): Boolean { + return isRecursiveObjectDFS(tree, mutableSetOf()) + } + + private fun isRecursiveObjectDFS(tree: PythonTreeNode, visited: Set): Boolean { + if (tree is PrimitiveNode) { + return false + } + if (visited.contains(tree)) + return true + return tree.children.any { isRecursiveObjectDFS(it, visited + tree) } + } + + fun containsFakeNode(tree: PythonTreeNode): Boolean { + return containsFakeNodeDFS(tree, mutableSetOf()) + } + + private fun containsFakeNodeDFS(tree: PythonTreeNode, visited: MutableSet): Boolean { + if (visited.contains(tree)) + return false + if (tree is FakeNode) + return true + visited.add(tree) + return tree.children.any { containsFakeNodeDFS(it, visited) } + } + + open class PythonTreeNode( + val id: Long, + val type: PythonClassId, + var comparable: Boolean = true, + ) { + constructor(type: PythonClassId, comparable: Boolean = true) : this(PythonIdGenerator.createId(), type, comparable) + + fun PythonTreeNode.wrap(): PythonTreeWrapper = PythonTreeWrapper(this) + + open val children: List = emptyList() + + fun isRecursive(): Boolean { + return isRecursiveObject(this) + } + + override fun toString(): String { + return type.name + children.toString() + } + + open fun typeEquals(other: Any?): Boolean { + return if (other is PythonTreeNode) + type == other.type && comparable && other.comparable + else + false + } + + override fun equals(other: Any?): Boolean { + if (other !is PythonTreeNode) { + return false + } + return this.wrap() == other.wrap() +// return this.id == other.id + } + + override fun hashCode(): Int { + return id.hashCode() + } + + open fun softEquals(other: PythonTreeNode): Boolean { // must be called only from PythonTreeWrapper! + return type == other.type && children == other.children + } + + open fun softHashCode(): Int { // must be called only from PythonTreeWrapper! + return type.hashCode() * 31 + children.hashCode() + } + + open fun diversity(): Int = // must be called only from PythonTreeWrapper! + 1 + children.fold(0) { acc, child -> acc + child.diversity() } + } + + object FakeNode: PythonTreeNode(0L, PythonClassId("")) + + class PrimitiveNode( + id: Long, + type: PythonClassId, + val repr: String, + ) : PythonTreeNode(id, type) { + constructor(type: PythonClassId, repr: String) : this(PythonIdGenerator.getOrCreateIdForValue(repr), type, repr) + + override fun toString(): String { + return repr + } + + override fun equals(other: Any?): Boolean { + if (other !is PrimitiveNode) + return false + return repr == other.repr + } + + override fun hashCode(): Int { + return repr.hashCode() + } + + override fun softEquals(other: PythonTreeNode): Boolean { + if (other !is PrimitiveNode) + return false + return repr == other.repr + } + + override fun softHashCode(): Int { + return repr.hashCode() + } + + override fun diversity(): Int = 2 + } + + class ListNode( + id: Long, + val items: MutableMap + ) : PythonTreeNode(id, pythonListClassId) { + constructor(items: MutableMap) : this(PythonIdGenerator.createId(), items) + + override val children: List + get() = items.values.toList() + + override fun typeEquals(other: Any?): Boolean { + return if (other is ListNode) + children.zip(other.children).all { + it.first.typeEquals(it.second) + } + else false + } + } + class NDArrayNode( + id: Long, + val items: MutableMap, + val dimensions: List + ) : PythonTreeNode(id, pythonNdarrayClassId) { + constructor(items: MutableMap, dimensions: List) : this(PythonIdGenerator.createId(), items, dimensions) + + override val children: List + get() = items.values.toList() + + override fun typeEquals(other: Any?): Boolean { + return if (other is NDArrayNode) + dimensions == other.dimensions && + children.zip(other.children).all { + it.first.typeEquals(it.second) + } + else false + } + + + override fun toString(): String { + return "ndarray${items.values}, shape: $dimensions" + } + } + + class DictNode( + id: Long, + val items: MutableMap + ) : PythonTreeNode(id, pythonDictClassId) { + constructor(items: MutableMap) : this(PythonIdGenerator.createId(), items) + + override val children: List + get() = items.values + items.keys + + override fun typeEquals(other: Any?): Boolean { + return if (other is DictNode) { + items.keys.size == other.items.keys.size && items.keys.all { + items[it]?.typeEquals(other.items[it]) ?: false + } + + } else false + } + + } + + class SetNode( + id: Long, + val items: MutableSet + ) : PythonTreeNode(id, pythonSetClassId) { + constructor(items: MutableSet) : this(PythonIdGenerator.createId(), items) + + override val children: List + get() = items.toList() + + override fun typeEquals(other: Any?): Boolean { + return if (other is SetNode) { + items.size == other.items.size && ( + items.isEmpty() || items.all { + items.first().typeEquals(it) + } && other.items.all { + items.first().typeEquals(it) + }) + } else { + false + } + } + } + + class TupleNode( + id: Long, + val items: MutableMap + ) : PythonTreeNode(id, pythonTupleClassId) { + constructor(items: MutableMap) : this(PythonIdGenerator.createId(), items) + + override val children: List + get() = items.values.toList() + + override fun typeEquals(other: Any?): Boolean { + return if (other is TupleNode) { + items.size == other.items.size && children.zip(other.children).all { + it.first.typeEquals(it.second) + } + } else { + false + } + } + } + + class IteratorNode( + id: Long, + val items: MutableMap, + val exception: PythonClassId = pythonStopIterationClassId, + ) : PythonTreeNode(id, pythonIteratorClassId) { + + constructor(items: MutableMap, stopException: PythonClassId = pythonStopIterationClassId) : this(PythonIdGenerator.createId(), items, stopException) + + override val children: List + get() = items.values.toList() + + override fun typeEquals(other: Any?): Boolean { + return if (other is ListNode) + children.zip(other.children).all { + it.first.typeEquals(it.second) + } + else false + } + } + + class ReduceNode( + id: Long, + type: PythonClassId, + val constructor: PythonClassId, + val args: List, + var state: MutableMap, + var listitems: List, + var dictitems: Map, + var customState: Boolean = false, // if this field is true, state must have structure {'state': } + ) : PythonTreeNode(id, type) { + constructor( + type: PythonClassId, + constructor: PythonClassId, + args: List, + ) : this(PythonIdGenerator.createId(), type, constructor, args, emptyMap().toMutableMap(), emptyList(), emptyMap()) + + constructor( + id: Long, + type: PythonClassId, + constructor: PythonClassId, + args: List, + ) : this(id, type, constructor, args, emptyMap().toMutableMap(), emptyList(), emptyMap()) + + override val children: List + get() = args + state.values + listitems + dictitems.values + dictitems.keys + PythonTreeNode(constructor) + + override fun typeEquals(other: Any?): Boolean { + return if (other is ReduceNode) { + type == other.type && state.all { (key, value) -> + other.state.containsKey(key) && value.typeEquals(other.state[key]) + } && listitems.withIndex().all { (index, item) -> + other.listitems.size > index && item.typeEquals(other.listitems[index]) + } && dictitems.all { (key, value) -> + other.dictitems.containsKey(key) && value.typeEquals(other.dictitems[key]) + } + } else false + } + + override fun softEquals(other: PythonTreeNode): Boolean { + if (other !is ReduceNode) + return false + return type == other.type && constructor == other.constructor && args == other.args && + state == other.state && listitems == other.listitems && dictitems == other.dictitems + } + + override fun softHashCode(): Int { + var result = constructor.hashCode() + result = 31 * result + args.hashCode() + result = 31 * result + state.hashCode() + result = 31 * result + listitems.hashCode() + result = 31 * result + dictitems.hashCode() + return result + } + } + + fun allElementsHaveSameStructure(elements: Collection): Boolean { + return if (elements.isEmpty()) { + true + } else { + val firstElement = elements.first() + elements.drop(1).all { + it.typeEquals(firstElement) + } + } + } + + fun fromObject(): PrimitiveNode { + return PrimitiveNode( + pythonObjectClassId, + "object()" + ) + } + + fun fromNone(): PrimitiveNode { + return PrimitiveNode( + pythonNoneClassId, + "None" + ) + } + + fun fromInt(value: BigInteger): PrimitiveNode { + return PrimitiveNode( + pythonIntClassId, + value.toString(10) + ) + } + + fun fromString(value: String): PrimitiveNode { + return PrimitiveNode( + pythonStrClassId, + value.toPythonRepr() + ) + } + + fun fromBool(value: Boolean): PrimitiveNode { + return PrimitiveNode( + pythonBoolClassId, + if (value) "True" else "False" + ) + } + + fun fromFloat(value: Double): PrimitiveNode { + val stringValue = + when (value) { + Double.POSITIVE_INFINITY -> "float('inf')" + Double.NEGATIVE_INFINITY -> "-float('inf')" + else -> value.toString() + } + return PrimitiveNode( + pythonFloatClassId, + stringValue + ) + } + + fun fromParsedConstant(value: Pair): PythonTreeNode? { + return when (value.first.pythonTypeName()) { + "builtins.int" -> fromInt(value.second as? BigInteger ?: return null) + "builtins.float" -> fromFloat((value.second as? BigDecimal)?.toDouble() ?: return null) + "typing.Tuple", "builtins.tuple" -> { + val elemsUntyped = (value.second as? List<*>) ?: return null + val elems = elemsUntyped.map { + val pair = it as? Pair<*,*> ?: return null + Pair(pair.first as? UtType ?: return null, pair.second ?: return null) + } + TupleNode( + elems.mapIndexed { index, pair -> + Pair(index, fromParsedConstant(pair) ?: return null) + }.associate { it }.toMutableMap() + ) + } + else -> null + } + } +} + +object PythonIdGenerator { + private const val lower_bound: Long = 1500_000_000 + + private val lastId: AtomicLong = AtomicLong(lower_bound) + private val cache: IdentityHashMap = IdentityHashMap() + + fun getOrCreateIdForValue(value: Any): Long { + return cache.getOrPut(value) { createId() } + } + + fun createId(): Long { + return lastId.incrementAndGet() + } + +} + +class PythonTreeWrapper(val tree: PythonTree.PythonTreeNode) { + override fun equals(other: Any?): Boolean { + if (other !is PythonTreeWrapper) + return false + if (PythonTree.isRecursiveObject(tree) || PythonTree.isRecursiveObject(other.tree)) + return tree.id == other.tree.id + return tree.softEquals(other.tree) + } + + override fun hashCode(): Int { + if (PythonTree.isRecursiveObject(tree)) + return tree.hashCode() + return tree.softHashCode() + } + + private val INF = 1000_000_000 + + private fun diversity(): Int { + if (PythonTree.isRecursiveObject(tree)) + return INF + return tree.diversity() + } + + fun commonDiversity(other: Int): Int { + return listOf(diversity() + other, INF).min() + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonIdUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonIdUtils.kt new file mode 100644 index 0000000000..3ca45d7a96 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonIdUtils.kt @@ -0,0 +1,26 @@ +package org.utbot.python.framework.api.python.util + +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonClassId + +// none annotation can be used in code only since Python 3.10 +val pythonNoneClassId = PythonClassId("types.NoneType") +val pythonObjectClassId = PythonClassId("builtins.object") +val pythonAnyClassId = NormalizedPythonAnnotation("typing.Any") +val pythonIntClassId = PythonClassId("builtins.int") +val pythonFloatClassId = PythonClassId("builtins.float") +val pythonComplexClassId = PythonClassId("builtins.complex") +val pythonStrClassId = PythonClassId("builtins.str") +val pythonBoolClassId = PythonClassId("builtins.bool") +val pythonRangeClassId = PythonClassId("builtins.range") +val pythonListClassId = PythonClassId("builtins.list") +val pythonTupleClassId = PythonClassId("builtins.tuple") +val pythonDictClassId = PythonClassId("builtins.dict") +val pythonSetClassId = PythonClassId("builtins.set") +val pythonBytearrayClassId = PythonClassId("builtins.bytearray") +val pythonBytesClassId = PythonClassId("builtins.bytes") +val pythonExceptionClassId = PythonClassId("builtins.Exception") +val pythonIteratorClassId = PythonClassId("typing.Iterator") +val pythonRePatternClassId = PythonClassId("re.Pattern") +val pythonStopIterationClassId = PythonClassId("builtins.StopIteration") +val pythonNdarrayClassId = PythonClassId("numpy.ndarray") \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonTreeComparator.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonTreeComparator.kt new file mode 100644 index 0000000000..e6a03ee171 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonTreeComparator.kt @@ -0,0 +1,100 @@ +package org.utbot.python.framework.api.python.util + +import org.utbot.python.framework.api.python.PythonTree + +enum class VisitStatus { + OPENED, CLOSED +} + +/* + * Compare python tree by structure. Returns false if: + * - objects have different types + * - incomparable and have different ids + * - have the same type but structures aren't equal recursively + */ +fun comparePythonTree( + left: PythonTree.PythonTreeNode, + right: PythonTree.PythonTreeNode, + visitedLeft: MutableMap = emptyMap().toMutableMap(), + visitedRight: MutableMap = emptyMap().toMutableMap(), + equals: MutableMap, Boolean> = emptyMap, Boolean>().toMutableMap(), + ): Boolean { + if (visitedLeft[left.id] != visitedRight[right.id]) { + visitedLeft[left.id] = VisitStatus.CLOSED + visitedRight[right.id] = VisitStatus.CLOSED + equals[left.id to right.id] = false + return false + } + if (visitedLeft[left.id] == VisitStatus.CLOSED) { + return equals[left.id to right.id] ?: false + } + if (visitedLeft[left.id] == VisitStatus.OPENED) { + return true + } + + visitedLeft[left.id] = VisitStatus.OPENED + visitedRight[right.id] = VisitStatus.OPENED + + val areEquals = if (left.comparable && right.comparable && left.type == right.type) { + when (left) { + is PythonTree.PrimitiveNode -> { + left == right + } + + is PythonTree.ListNode -> { + if (right !is PythonTree.ListNode) false + else if (left.items.keys != right.items.keys) false + else left.items.keys.all { comparePythonTree(left.items[it]!!, right.items[it]!!, visitedLeft, visitedRight, equals) } + } + + is PythonTree.DictNode -> { + if (right !is PythonTree.DictNode) false + else if (left.items.keys != right.items.keys) false + else left.items.keys.all { comparePythonTree(left.items[it]!!, right.items[it]!!, visitedLeft, visitedRight, equals) } + } + + is PythonTree.TupleNode -> { + if (right !is PythonTree.TupleNode) false + else if (left.items.keys != right.items.keys) false + else left.items.keys.all { comparePythonTree(left.items[it]!!, right.items[it]!!, visitedLeft, visitedRight, equals) } + } + + is PythonTree.SetNode -> { + if (right !is PythonTree.SetNode) false + else if (left.items.size != right.items.size) false + else left.items.sortedBy { it.id } + .zip(right.items.sortedBy { it.id }) + .all { comparePythonTree(it.first, it.second, visitedLeft, visitedRight, equals) } + } + + is PythonTree.ReduceNode -> { + if (right !is PythonTree.ReduceNode) false + else { + val type = left.type == right.type + val state = left.state.size == right.state.size && left.state.keys.all { + comparePythonTree( + left.state[it]!!, + right.state[it]!!, + visitedLeft, + visitedRight, + equals + ) + } + val listitems = left.listitems.size == right.listitems.size && left.listitems.zip(right.listitems) + .all { comparePythonTree(it.first, it.second, visitedLeft, visitedRight, equals) } + val dictitems = left.dictitems.keys == right.dictitems.keys && left.dictitems.keys + .all { comparePythonTree(left.dictitems[it]!!, right.dictitems[it]!!, visitedLeft, visitedRight, equals) } + + type && state && listitems && dictitems + } + } + + else -> false + } + } else left.id == right.id + + visitedLeft[left.id] = VisitStatus.CLOSED + visitedRight[right.id] = VisitStatus.CLOSED + equals[left.id to right.id] = areEquals + return areEquals +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonUtils.kt new file mode 100644 index 0000000000..4f5247ffcf --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonUtils.kt @@ -0,0 +1,27 @@ +package org.utbot.python.framework.api.python.util + + +fun moduleOfType(typeName: String): String? { + val lastIndex = typeName.lastIndexOf('.') + return if (lastIndex == -1) null else typeName.substring(0, lastIndex) +} + +fun String.toSnakeCase(): String { + val splitSymbols = "_" + return this.mapIndexed { index: Int, c: Char -> + if (c.isLowerCase() || c.isDigit() || splitSymbols.contains(c)) c + else if (c.isUpperCase()) { + (if (index > 0) "_" else "") + c.lowercase() + } else c + } + .joinToString("") + .replace(".", "_") +} + +fun String.toPythonRepr(): String { + val repr = this + .replace(Regex("((? = setOf( + "True", "False", "None", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", + "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", + "not", "or", "pass", "raise", "return", "try", "while", "with", "yield", "list", "int", "str", "float", "bool", + "bytes", "frozenset", "dict", "set", "tuple", "abs", "aiter", "all", "any", "anext", "ascii", "bool", + "breakpoint", "bytearray", "callable", "chr", "classmethod", "compile", "complex", "delattr", "dir", "divmod", + "enumerate", "eval", "exec", "filter", "format", "getattr", "globals", "hasattr", "hash", "help", "hex", "id", + "input", "isinstance", "issubclass", "iter", "len", "list", "locals", "map", "max", "memoryview", "min", + "next", "object", "oct", "open", "ord", "pow", "print", "property", "range", "repr", "reversed", "round", + "set", "setattr", "slice", "sorted", "staticmethod", "sum", "super", "type", "vars", "zip", "self" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + val simpleName = testClassCustomName ?: "Test${classUnderTest.simpleName}" + return Pair(simpleName, simpleName) + } + + override fun getNameGeneratorBy(context: CgContext) = PythonCgNameGenerator(context) + override fun getCallableAccessManagerBy(context: CgContext) = PythonCgCallableAccessManagerImpl(context) + override fun getStatementConstructorBy(context: CgContext) = PythonCgStatementConstructorImpl(context) + override fun getVariableConstructorBy(context: CgContext) = PythonCgVariableConstructor(context) + override fun getMethodConstructorBy(context: CgContext) = PythonCgMethodConstructor(context) + override fun getLanguageTestFrameworkManager() = PythonTestFrameworkManager() + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = + CgPythonRenderer(context, printer) + + var memoryObjects: MutableMap = emptyMap().toMutableMap() + var memoryObjectsModels: MutableMap = emptyMap().toMutableMap() + + fun clear() { + memoryObjects.clear() + memoryObjectsModels.clear() + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonTestFrameworkManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonTestFrameworkManager.kt new file mode 100644 index 0000000000..312b94cc6a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonTestFrameworkManager.kt @@ -0,0 +1,22 @@ +package org.utbot.python.framework.codegen + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.services.language.LanguageTestFrameworkManager +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.framework.codegen.model.constructor.tree.PytestManager +import org.utbot.python.framework.codegen.model.constructor.tree.UnittestManager + +class PythonTestFrameworkManager : LanguageTestFrameworkManager() { + + override fun managerByFramework(context: CgContext) = when (context.testFramework) { + is Unittest -> UnittestManager(context) + is Pytest -> PytestManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework = Unittest + + override val testFrameworks = listOf(Unittest, Pytest) + +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt new file mode 100644 index 0000000000..799fd2852d --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt @@ -0,0 +1,137 @@ +package org.utbot.python.framework.codegen.model + +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.SimpleTestClassModel +import org.utbot.framework.codegen.generator.CodeGenerator +import org.utbot.framework.codegen.generator.CodeGeneratorParams +import org.utbot.framework.codegen.generator.CodeGeneratorResult +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.codegen.renderer.CgPrinterImpl +import org.utbot.framework.codegen.renderer.CgRendererContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.python.PythonMethod +import org.utbot.python.framework.codegen.PythonCgLanguageAssistant +import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgTestClassConstructor +import org.utbot.python.framework.codegen.model.constructor.visitor.CgPythonRenderer +import org.utpython.types.general.UtType +import org.utpython.types.pythonAnyType +import org.utpython.types.pythonModules +import org.utpython.types.pythonTypeRepresentation + +class PythonCodeGenerator( + classUnderTest: ClassId, + paramNames: MutableMap> = mutableMapOf(), + testFramework: TestFramework = TestFramework.defaultItem, + mockFramework: MockFramework = MockFramework.defaultItem, + staticsMocking: StaticsMocking = StaticsMocking.defaultItem, + forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem, + generateWarningsForStaticMocking: Boolean = true, + parameterizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem, + runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, + hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), + enableTestsTimeout: Boolean = true, + testClassPackageName: String = classUnderTest.packageName +) : CodeGenerator( + CodeGeneratorParams( + classUnderTest = classUnderTest, + projectType = ProjectType.Python, + paramNames = paramNames, + generateUtilClassFile = true, + testFramework = testFramework, + mockFramework = mockFramework, + staticsMocking = staticsMocking, + forceStaticMocking = forceStaticMocking, + generateWarningsForStaticMocking = generateWarningsForStaticMocking, + parameterizedTestSource = parameterizedTestSource, + runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, + hangingTestsTimeout = hangingTestsTimeout, + enableTestsTimeout = enableTestsTimeout, + testClassPackageName = testClassPackageName, + cgLanguageAssistant = PythonCgLanguageAssistant, + ) +) { + fun pythonGenerateAsStringWithTestReport( + cgTestSets: List, + importModules: Set, + testClassCustomName: String? = null, + ): CodeGeneratorResult = withCustomContext(testClassCustomName) { + context.withTestClassFileScope { + (context.cgLanguageAssistant as PythonCgLanguageAssistant).clear() + val testClassModel = SimpleTestClassModel(classUnderTest, cgTestSets) + context.collectedImports.addAll(importModules) + + val astConstructor = PythonCgTestClassConstructor(context) + val renderer = CgAbstractRenderer.makeRenderer(context) + + val testClassFile = astConstructor.construct(testClassModel) + testClassFile.accept(renderer) + + CodeGeneratorResult(renderer.toString(), astConstructor.testsGenerationReport) + } + } + + fun generateMypyCheckCode( + method: PythonMethod, + methodAnnotations: Map, + directoriesForSysPath: Set, + moduleToImport: String, + namesInModule: Collection, + additionalVars: String + ): String { + val cgRendererContext = CgRendererContext.fromCgContext(context) + val printer = CgPrinterImpl() + val renderer = CgPythonRenderer(cgRendererContext, printer) + + val importOs = PythonSystemImport("os") + val importSys = PythonSystemImport("sys") + val importTyping = PythonSystemImport("typing") + val importSysPaths = directoriesForSysPath.map { PythonSysPathImport(it) } + val importsFromModule = namesInModule.map { name -> + PythonUserImport(name, moduleToImport) + } + + val additionalModules = methodAnnotations.values.flatMap { it.pythonModules() }.map { PythonUserImport(it) } + val imports = + (listOf(importOs, importSys, importTyping) + importSysPaths + (importsFromModule + additionalModules)).toSet() + .toList() + + imports.forEach { renderer.renderPythonImport(it) } + + val paramNames = method.argumentsNames + val parameters: List = paramNames.map { argument -> + if (methodAnnotations[argument]?.meta.toString() == "numpy.ndarray") { + val re = """, .*[^]]""".toRegex() + val type = re.find(methodAnnotations[argument]?.pythonTypeRepresentation().toString())?.value + val newAnnotation: String = + type?.let { + methodAnnotations[argument]?.pythonTypeRepresentation().toString() + .replace(it, ", ${pythonAnyType.pythonTypeRepresentation()}") + } ?: pythonAnyType.pythonTypeRepresentation() + "${argument}: $newAnnotation" + } else + "${argument}: ${methodAnnotations[argument]?.pythonTypeRepresentation() ?: pythonAnyType.pythonTypeRepresentation()}" + } + + val functionPrefix = "__mypy_check" + val functionName = + "def ${functionPrefix}_${method.name}(${parameters.joinToString(", ")}):" // TODO: in future can be "async def" + + val mypyCheckCode = listOf( + renderer.toString(), + "", + additionalVars, + "", + functionName, + ) + method.codeAsString.split("\n") + return mypyCheckCode.joinToString("\n") + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt new file mode 100644 index 0000000000..a9e7dd8f10 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt @@ -0,0 +1,91 @@ +package org.utbot.python.framework.codegen.model + +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.util.methodId +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.api.python.util.pythonNoneClassId + +object Pytest : TestFramework(displayName = "pytest", id = "pytest") { + override val mainPackage: String = "pytest" + override val assertionsClass: ClassId = pythonNoneClassId + override val arraysAssertionsClass: ClassId = assertionsClass + override val kotlinFailureAssertionsClass: ClassId = assertionsClass + + override val testAnnotationId: ClassId = BuiltinClassId( + canonicalName = "pytest", + simpleName = "Tests" + ) + + override val beforeMethodId: ClassId = pythonAnyClassId + + override val afterMethodId: ClassId = pythonAnyClassId + + override val parameterizedTestAnnotationId: ClassId = pythonAnyClassId + override val methodSourceAnnotationId: ClassId = pythonAnyClassId + override val nestedClassesShouldBeStatic: Boolean = false + override val argListClassId: ClassId = pythonAnyClassId + + val skipDecoratorClassId = PythonClassId("pytest", "mark.skip") + + @OptIn(ExperimentalStdlibApi::class) + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List = buildList { + add(executionInvoke) + addAll(additionalArguments) + add(mainPackage) + } +} + +object Unittest : TestFramework(displayName = "Unittest", id = "Unittest") { + init { + isInstalled = true + } + + override val testSuperClass: ClassId = PythonClassId("unittest.TestCase") + override val mainPackage: String = "unittest" + override val assertionsClass: ClassId = PythonClassId("self") + override val arraysAssertionsClass: ClassId = assertionsClass + override val kotlinFailureAssertionsClass: ClassId = assertionsClass + override val testAnnotationId: ClassId = BuiltinClassId( + canonicalName = "Unittest", + simpleName = "Tests" + ) + + override val beforeMethodId: ClassId = pythonAnyClassId + + override val afterMethodId: ClassId = pythonAnyClassId + + override val parameterizedTestAnnotationId: ClassId = pythonAnyClassId + override val methodSourceAnnotationId: ClassId = pythonAnyClassId + override val nestedClassesShouldBeStatic: Boolean = false + override val argListClassId: ClassId = pythonAnyClassId + + val skipDecoratorClassId = PythonClassId("unittest.skip") + + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List { + throw UnsupportedOperationException() + } + + override val assertEquals by lazy { assertionId("assertEqual", objectClassId, objectClassId) } + + override fun assertionId(name: String, vararg params: ClassId): MethodId = + methodId(assertionsClass, name, voidClassId, *params) +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt new file mode 100644 index 0000000000..c3dfabb64e --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt @@ -0,0 +1,92 @@ +package org.utbot.python.framework.codegen.model + +import org.utbot.framework.codegen.domain.Import + +sealed class PythonImport(order: Int) : Import(order) { + var importName: String = "" + var moduleName: String? = null + var alias: String? = null + + constructor(order: Int, importName: String, moduleName: String? = null, alias: String? = null) : this(order) { + this.importName = importName + this.moduleName = moduleName + this.alias = alias + } + + override val qualifiedName: String + get() = if (moduleName != null) "${moduleName}.${importName}" else importName + + val rootModuleName: String + get() = qualifiedName.split(".")[0] + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + var result = importName.hashCode() + result = 31 * result + (moduleName?.hashCode() ?: 0) + return result + } +} + +data class PythonSysPathImport(val sysPath: String) : PythonImport(2) { + override val qualifiedName: String + get() = sysPath + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonSysPathImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + return sysPath.hashCode() + } +} + +data class PythonUserImport(val importName_: String, val moduleName_: String? = null, val alias_: String? = null) : + PythonImport(3, importName_, moduleName_, alias_) { + override val qualifiedName: String + get() = if (moduleName != null) "${moduleName}.${importName}" else importName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonUserImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + var result = importName.hashCode() + result = 31 * result + (moduleName?.hashCode() ?: 0) + return result + } +} + +data class PythonSystemImport(val importName_: String, val moduleName_: String? = null, val alias_: String? = null) : + PythonImport(1, importName_, moduleName_, alias_) { + override val qualifiedName: String + get() = if (moduleName != null) "${moduleName}.${importName}" else importName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonSystemImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + var result = importName.hashCode() + result = 31 * result + (moduleName?.hashCode() ?: 0) + return result + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt new file mode 100644 index 0000000000..ca8987d3d4 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt @@ -0,0 +1,107 @@ +package org.utbot.python.framework.codegen.model.constructor.name + +import org.utbot.python.framework.codegen.model.PythonImport +import org.utbot.framework.codegen.services.language.isLanguageKeyword +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.services.CgNameGenerator +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.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.util.toSnakeCase + +internal fun infiniteInts(): Sequence = + generateSequence(1) { it + 1 } + +class PythonCgNameGenerator(val context: CgContext) + : CgNameGenerator, CgContextOwner by context { + + private fun nextIndexedVarName(base: String): String = + infiniteInts() + .map { "$base$it" } + .first { it !in existingVariableNames } + + private fun nextIndexedMethodName(base: String, skipOne: Boolean = false): String = + infiniteInts() + .map { if (skipOne && it == 1) base else "$base$it" } + .first { it !in existingMethodNames } + + private fun createNameFromKeyword(baseName: String): String = + nextIndexedVarName(baseName) + + private fun createExecutableName(executableId: ExecutableId): String { + return when (executableId) { + is ConstructorId -> executableId.classId.prettifiedName + is MethodId -> executableId.name + } + } + + override fun nameFrom(id: ClassId): String = + when (id) { + is NormalizedPythonAnnotation -> "var" + else -> id.simpleName.toSnakeCase() + } + + override fun variableName(base: String, isMock: Boolean, isStatic: Boolean): String { + val baseName = when { + isMock -> base + "_mock" + isStatic -> base + "_static" + else -> base + } + return when { + baseName in existingVariableNames -> nextIndexedVarName(baseName) + isLanguageKeyword(baseName, context.cgLanguageAssistant) -> createNameFromKeyword(baseName) + else -> baseName + }.also { + existingVariableNames = existingVariableNames.add(it) + } + } + + override fun variableName(type: ClassId, base: String?, isMock: Boolean): String { + val baseName = base?.toSnakeCase() ?: nameFrom(type) + val importedModuleNames = collectedImports.mapNotNull { + if (it is PythonImport) it.rootModuleName else null + } + return when { + baseName in existingVariableNames -> nextIndexedVarName(baseName) + baseName in importedModuleNames -> nextIndexedVarName(baseName) + isLanguageKeyword(baseName, context.cgLanguageAssistant) -> createNameFromKeyword(baseName) + else -> baseName + }.also { + existingVariableNames = existingVariableNames.add(it) + } + } + + override fun testMethodNameFor(executableId: ExecutableId, customName: String?): String { + val executableName = createExecutableName(executableId) + + val name = if (customName != null && customName !in existingMethodNames) { + customName + } else { + val base = customName ?: "test_${executableName.toSnakeCase()}" + nextIndexedMethodName(base) + } + existingMethodNames += name + return name + } + + override fun parameterizedTestMethodName(dataProviderMethodName: String): String { + TODO("Not yet implemented") + } + + override fun dataProviderMethodNameFor(executableId: ExecutableId): String { + TODO("Not yet implemented") + } + + override fun errorMethodNameFor(executableId: ExecutableId): String { + val executableName = createExecutableName(executableId) + val newName = when (val base = "test_${executableName.toSnakeCase()}_errors") { + !in existingMethodNames -> base + else -> nextIndexedMethodName(base) + } + existingMethodNames += newName + return newName + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt new file mode 100644 index 0000000000..7830ad22a8 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt @@ -0,0 +1,77 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgFieldAccess +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgStaticFieldAccess +import org.utbot.framework.codegen.domain.models.CgThisInstance +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.services.access.CgIncompleteMethodCall +import org.utbot.framework.codegen.tree.importIfNeeded +import org.utbot.framework.codegen.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.util.exceptions +import org.utbot.python.framework.api.python.PythonMethodId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.codegen.model.constructor.util.importIfNeeded +import org.utbot.python.framework.codegen.model.tree.CgPythonTree + +class PythonCgCallableAccessManagerImpl(val context: CgContext) : CgCallableAccessManager, + CgContextOwner by context { + + override fun CgExpression?.get(methodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(methodId, this) + + override fun ClassId.get(staticMethodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(staticMethodId, CgThisInstance(pythonAnyClassId)) + + override fun CgExpression.get(fieldId: FieldId): CgExpression { + return CgFieldAccess(this, fieldId) + } + + override fun ClassId.get(fieldId: FieldId): CgStaticFieldAccess { + TODO("Not yet implemented") + } + + override fun ConstructorId.invoke(vararg args: Any?): CgExecutableCall { + val resolvedArgs = args.resolve() + val constructorCall = CgConstructorCall(this, resolvedArgs) + newConstructorCall(this) + return constructorCall + } + + override fun CgIncompleteMethodCall.invoke(vararg args: Any?): CgMethodCall { + val resolvedArgs = emptyList().toMutableList() + args.forEach { arg -> + // if arg is named argument we must use this name + if (arg is CgPythonTree) { + resolvedArgs.add(arg.value) + } else { + resolvedArgs.add(arg as CgExpression) + } + } + val methodCall = CgMethodCall(caller, method, resolvedArgs) + if (method is PythonMethodId) + newMethodCall(method) + return methodCall + } + + private fun newMethodCall(methodId: MethodId) { + importIfNeeded(methodId as PythonMethodId) + } + + private fun newConstructorCall(constructorId: ConstructorId) { + importIfNeeded(constructorId.classId) + for (exception in constructorId.exceptions) { + addExceptionIfNeeded(exception) + } + } + +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt new file mode 100644 index 0000000000..24938294e0 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt @@ -0,0 +1,517 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgDocumentationComment +import org.utbot.framework.codegen.domain.models.CgFieldAccess +import org.utbot.framework.codegen.domain.models.CgGetLength +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.CgMultilineComment +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgReferenceExpression +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTestMethodType +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.domain.models.convertDocToCg +import org.utbot.framework.codegen.tree.CgMethodConstructor +import org.utbot.framework.codegen.tree.buildTestMethod +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.InstrumentedProcessDeathException +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.UtExecutionFailure +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtTimeoutException +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonMethodId +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.PythonTreeModel +import org.utbot.python.framework.api.python.PythonUtExecution +import org.utbot.python.framework.api.python.util.pythonExceptionClassId +import org.utbot.python.framework.api.python.util.pythonIntClassId +import org.utbot.python.framework.api.python.util.pythonNoneClassId +import org.utbot.python.framework.codegen.PythonCgLanguageAssistant +import org.utbot.python.framework.codegen.model.constructor.util.importIfNeeded +import org.utbot.python.framework.codegen.model.tree.CgPythonFunctionCall +import org.utbot.python.framework.codegen.model.tree.CgPythonIndex +import org.utbot.python.framework.codegen.model.tree.CgPythonNamedArgument +import org.utbot.python.framework.codegen.model.tree.CgPythonRange +import org.utbot.python.framework.codegen.model.tree.CgPythonRepr +import org.utbot.python.framework.codegen.model.tree.CgPythonTree +import org.utbot.python.framework.codegen.model.tree.CgPythonZip + +class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(context) { + private val maxDepth: Int = 5 + + private fun CgVariable.deepcopy(): CgVariable { + val classId = PythonClassId("copy.deepcopy") + importIfNeeded(classId) + return newVar(this.type) { CgPythonFunctionCall(classId, "copy.deepcopy", listOf(this)) } + } + + override fun assertEquality(expected: CgValue, actual: CgVariable) { + pythonDeepEquals(expected, actual) + } + + override fun createTestMethod(testSet: CgMethodTestSet, execution: UtExecution): CgTestMethod = + withTestMethodScope(execution) { + val constructorState = (execution as PythonUtExecution).stateInit + val diffIds = execution.diffIds + (context.cgLanguageAssistant as PythonCgLanguageAssistant).clear() + val testMethodName = nameGenerator.testMethodNameFor(testSet.executableUnderTest, execution.testMethodName) + if (execution.testMethodName == null) { + execution.testMethodName = testMethodName + } + // TODO: remove this line when SAT-1273 is completed + execution.displayName = execution.displayName?.let { "${testSet.executableUnderTest.name}: $it" } + pythonTestMethod(testMethodName, execution.displayName) { + val statics = currentExecution!!.stateBefore.statics + rememberInitialStaticFields(statics) + + // TODO: move such methods to another class and leave only 2 public methods: remember initial and final states + val mainBody = { + substituteStaticFields(statics) + setupInstrumentation() + // build this instance + thisInstance = constructorState.thisInstance?.let { + variableConstructor.getOrCreateVariable(it) + } + + val beforeThisInstance = execution.stateBefore.thisInstance + val afterThisInstance = execution.stateAfter.thisInstance + val assertThisObject = emptyList>().toMutableList() + if (beforeThisInstance is PythonTreeModel && afterThisInstance is PythonTreeModel) { + if (diffIds.contains(afterThisInstance.tree.id)) { + thisInstance = thisInstance?.let { + val newValue = + if (it is CgPythonTree) { + if (it.value is CgVariable) { + it.value + } else { + newVar(it.type) {it.value} + } + } else { + newVar(it.type) {it} + } + assertThisObject.add(Pair(newValue, afterThisInstance)) + newValue + } + } + } + if (thisInstance is CgPythonTree) { + context.currentBlock.addAll((thisInstance as CgPythonTree).arguments) + } + + // build arguments + val stateAssertions = emptyMap>().toMutableMap() + for ((index, param) in constructorState.parameters.withIndex()) { + val name = execution.arguments[index].name + var argument = variableConstructor.getOrCreateVariable(param, name) + + val beforeValue = execution.stateBefore.parameters[index] + val afterValue = execution.stateAfter.parameters[index] + if (afterValue is PythonTreeModel && beforeValue is PythonTreeModel) { + if (diffIds.contains(afterValue.tree.id)) { + if (argument !is CgVariable) { + argument = newVar(argument.type, name) {argument} + } + stateAssertions[index] = Pair(argument, afterValue) + } + } + if (execution.arguments[index].isNamed) { + argument = CgPythonNamedArgument(name, argument) + } + + methodArguments += argument + } + methodArguments.forEach { + if (it is CgPythonTree) { + context.currentBlock.addAll(it.arguments) + } + if (it is CgPythonNamedArgument && it.value is CgPythonTree) { + context.currentBlock.addAll(it.value.arguments) + } + } + + recordActualResult() + generateResultAssertions() + + if (currentExecution?.result !is UtExecutionFailure && methodType == CgTestMethodType.SUCCESSFUL) { + generateFieldStateAssertions(stateAssertions, assertThisObject, testSet.executableUnderTest) + } + } + + if (statics.isNotEmpty()) { + +tryBlock { + mainBody() + }.finally { + recoverStaticFields() + } + } else { + mainBody() + } + } + } + + override fun generateResultAssertions() { + if (currentExecutableToCall is MethodId) { + val currentExecution = currentExecution!! + val executionResult = currentExecution.result + if (executionResult is UtExecutionFailure) { + val exceptionId = executionResult.rootCauseException.message?.let {PythonClassId(it)} ?: pythonExceptionClassId + val executionBlock = { + with(currentExecutableToCall) { + when (this) { + is MethodId -> thisInstance[this](*methodArguments.toTypedArray()).intercepted() + else -> {} + } + } + } + when (methodType) { + CgTestMethodType.PASSED_EXCEPTION -> { + testFrameworkManager.expectException(exceptionId) { + executionBlock() + } + return + } + CgTestMethodType.FAILING -> { + val executable = currentExecutableToCall!! as PythonMethodId + val executableName = "${executable.moduleName}.${executable.name}" + val warningLine = + "This test fails because function [$executableName] produces [${exceptionId.prettyName}]" + +CgMultilineComment(warningLine) + emptyLineIfNeeded() + executionBlock() + return + } + else -> {} + } + } + } + super.generateResultAssertions() + } + + private fun generateFieldStateAssertions( + stateAssertions: MutableMap>, + assertThisObject: MutableList>, + executableId: ExecutableId, + ) { + if (stateAssertions.isNotEmpty()) { + emptyLineIfNeeded() + } + stateAssertions.forEach { (index, it) -> + assertEquality( + expected = it.second, + actual = it.first, + expectedVariableName = paramNames[executableId]?.get(index) + "_expected" + ) + } + if (assertThisObject.isNotEmpty()) { + emptyLineIfNeeded() + } + assertThisObject.forEach { + assertEquality( + expected = it.second, + actual = it.first, + expectedVariableName = it.first.name + "_expected" + ) + } + } + + override fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean { + if (exception is TimeoutException || exception is InstrumentedProcessDeathException) return false + return runtimeExceptionTestsBehaviour == RuntimeExceptionTestsBehaviour.PASS + } + + private fun pythonTestMethod( + methodName: String, + displayName: String?, + params: List = emptyList(), + body: () -> Unit, + ): CgTestMethod { + displayName?.let { + testFrameworkManager.addTestDescription(displayName) + } + + val result = currentExecution!!.result + if (result is UtTimeoutException) { + testFrameworkManager.disableTestMethod( + "Disabled due to the fact that the execution is longer then ${hangingTestsTimeout.timeoutMs} ms" + ) + } + + val testMethod = buildTestMethod { + name = methodName + parameters = params + statements = block(body) + // Exceptions and annotations assignment must run after the statements block is build, + // because we collect info about exceptions and required annotations while building the statements + exceptions += collectedExceptions + annotations += collectedMethodAnnotations + methodType = this@PythonCgMethodConstructor.methodType + + val docComment = currentExecution?.summary?.map { convertDocToCg(it) }?.toMutableList() ?: mutableListOf() + documentation = CgDocumentationComment(docComment) + } + testMethods += testMethod + return testMethod + } + + private fun pythonDeepEquals(expected: CgValue, actual: CgVariable) { + require(expected is CgPythonTree) { + "Expected value have to be CgPythonTree but `${expected::class}` found" + } + pythonDeepTreeEquals(expected.tree, expected, actual) + } + + private fun pythonLenAssertConstructor(expected: CgVariable, actual: CgVariable): CgVariable { + val expectedValue = newVar(pythonIntClassId, "expected_length") { + CgGetLength(expected) + } + val actualValue = newVar(pythonIntClassId, "actual_length") { + CgGetLength(actual) + } + emptyLineIfNeeded() + testFrameworkManager.assertEquals(expectedValue, actualValue) + return expectedValue + } + + private fun assertIsInstance(expected: CgValue, actual: CgVariable) { + when (testFrameworkManager) { + is PytestManager -> + (testFrameworkManager as PytestManager).assertIsinstance(listOf(expected.type as PythonClassId), actual) + is UnittestManager -> + (testFrameworkManager as UnittestManager).assertIsinstance(listOf(expected.type as PythonClassId), actual) + else -> testFrameworkManager.assertEquals(expected, actual) + } + } + + private fun pythonAssertElementsByKey( + expectedNode: PythonTree.PythonTreeNode, + expected: CgVariable, + actual: CgVariable, + iterator: CgReferenceExpression, + keyName: String = "index", + ) { + val elements = when (expectedNode) { + is PythonTree.ListNode -> expectedNode.items.values + is PythonTree.TupleNode -> expectedNode.items.values + is PythonTree.DictNode -> expectedNode.items.values + else -> throw UnsupportedOperationException() + } + if (elements.isNotEmpty()) { + val elementsHaveSameStructure = PythonTree.allElementsHaveSameStructure(elements) + val firstChild = + elements.first() // TODO: We can use only structure => we should use another element if the first is empty + + emptyLineIfNeeded() + if (elementsHaveSameStructure) { + val index = newVar(pythonNoneClassId, keyName) { + CgPythonRepr(pythonNoneClassId, "None") + } + forEachLoop { + innerBlock { + condition = index + iterable = iterator + val indexExpected = newVar(firstChild.type, "expected_element") { + CgPythonIndex( + pythonIntClassId, + expected, + index + ) + } + val indexActual = newVar(firstChild.type, "actual_element") { + CgPythonIndex( + pythonIntClassId, + actual, + index + ) + } + pythonDeepTreeEquals(firstChild, indexExpected, indexActual, useExpectedAsValue = true) + statements = currentBlock + } + } + } else { + emptyLineIfNeeded() + assertIsInstance(expected, actual) + } + } + } + + private fun pythonAssertBuiltinsCollection( + expectedNode: PythonTree.PythonTreeNode, + expected: CgValue, + actual: CgVariable, + expectedName: String, + elementName: String = "index", + ) { + val expectedCollection = newVar(expected.type, expectedName) { expected } + + val length = pythonLenAssertConstructor(expectedCollection, actual) + + val iterator = if (expectedNode is PythonTree.DictNode) expected else CgPythonRange(length) + pythonAssertElementsByKey(expectedNode, expectedCollection, actual, iterator, elementName) + } + + private fun pythonAssertIterators( + expectedNode: PythonTree.IteratorNode, + actual: CgVariable, + ) { + val zip = CgPythonZip( + variableConstructor.getOrCreateVariable(PythonTreeModel(expectedNode)), + actual, + ) + val index = newVar(pythonNoneClassId, "pair") { + CgPythonRepr(pythonNoneClassId, "None") + } + forEachLoop { + innerBlock { + condition = index + iterable = zip + testFrameworkManager.assertEquals( + CgPythonIndex( + pythonIntClassId, + index, + CgPythonRepr(pythonIntClassId, "1") + ), + CgPythonIndex( + pythonIntClassId, + index, + CgPythonRepr(pythonIntClassId, "0") + ) + ) + statements = currentBlock + } + } + if (expectedNode.items.size < PythonTree.MAX_ITERATOR_SIZE) { + testFrameworkManager.expectException(expectedNode.exception) { + +CgPythonFunctionCall( + PythonClassId("builtins.next"), + "next", + listOf(actual) + ) + emptyLineIfNeeded() + } + } + } + + private fun pythonDeepTreeEquals( + expectedNode: PythonTree.PythonTreeNode, + expected: CgValue, + actual: CgVariable, + depth: Int = maxDepth, + useExpectedAsValue: Boolean = false + ) { + if (!expectedNode.comparable && expectedNode.isRecursive()) { + emptyLineIfNeeded() + comment("Cannot compare recursive objects") // TODO: add special function for recursive comparison + assertIsInstance(expected, actual) + return + } + if (expectedNode.comparable || depth == 0) { + val expectedValue = if (useExpectedAsValue) { + expected + } else { + variableConstructor.getOrCreateVariable(PythonTreeModel(expectedNode)) + } + if (expectedNode is PythonTree.IteratorNode) { + pythonAssertIterators( + expectedNode, + actual + ) + } else { + testFrameworkManager.assertEquals( + expectedValue, + actual, + ) + } + return + } + when (expectedNode) { + is PythonTree.PrimitiveNode -> { + emptyLineIfNeeded() + assertIsInstance(expected, actual) + } + + is PythonTree.ListNode -> { + pythonAssertBuiltinsCollection( + expectedNode, + expected, + actual, + "expected_list" + ) + } + + is PythonTree.TupleNode -> { + pythonAssertBuiltinsCollection( + expectedNode, + expected, + actual, + "expected_tuple" + ) + } + + is PythonTree.SetNode -> { + emptyLineIfNeeded() + testFrameworkManager.assertEquals( + expected, actual + ) + } + + is PythonTree.DictNode -> { + pythonAssertBuiltinsCollection( + expectedNode, + expected, + actual, + "expected_dict", + "key" + ) + } + + is PythonTree.IteratorNode -> { + pythonAssertIterators( + expectedNode, + actual + ) + } + + is PythonTree.ReduceNode -> { + if (expectedNode.state.isNotEmpty()) { + expectedNode.state.forEach { (field, value) -> + if (value.comparable) { + val fieldActual = newVar(value.type, "actual_$field") { + CgFieldAccess( + actual, FieldId( + value.type, + field + ) + ) + } + val fieldExpected = if (useExpectedAsValue) { + newVar(value.type, "expected_$field") { + CgFieldAccess( + expected, FieldId( + value.type, + field + ) + ) + } + } else { + variableConstructor.getOrCreateVariable(PythonTreeModel(expectedNode)) + } + pythonDeepTreeEquals(value, fieldExpected, fieldActual, depth - 1, useExpectedAsValue = useExpectedAsValue) + } + } + } else { + emptyLineIfNeeded() + assertIsInstance(expected, actual) + } + } + + else -> {} + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt new file mode 100644 index 0000000000..349c207617 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt @@ -0,0 +1,292 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import fj.data.Either +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.AnnotationTarget +import org.utbot.framework.codegen.domain.models.CgAnnotation +import org.utbot.framework.codegen.domain.models.CgAnonymousFunction +import org.utbot.framework.codegen.domain.models.CgComment +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgEmptyLine +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgIfStatement +import org.utbot.framework.codegen.domain.models.CgInnerBlock +import org.utbot.framework.codegen.domain.models.CgIsInstance +import org.utbot.framework.codegen.domain.models.CgLogicalAnd +import org.utbot.framework.codegen.domain.models.CgLogicalOr +import org.utbot.framework.codegen.domain.models.CgMultilineComment +import org.utbot.framework.codegen.domain.models.CgMultipleArgsAnnotation +import org.utbot.framework.codegen.domain.models.CgNamedAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgReturnStatement +import org.utbot.framework.codegen.domain.models.CgSingleArgAnnotation +import org.utbot.framework.codegen.domain.models.CgSingleLineComment +import org.utbot.framework.codegen.domain.models.CgThrowStatement +import org.utbot.framework.codegen.domain.models.CgTryCatch +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.tree.* +import org.utbot.framework.codegen.util.isAccessibleFrom +import org.utbot.framework.codegen.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.codegen.PythonCgLanguageAssistant.getCallableAccessManagerBy +import org.utbot.python.framework.codegen.PythonCgLanguageAssistant.getNameGeneratorBy +import org.utbot.python.framework.codegen.model.constructor.util.importIfNeeded +import org.utbot.python.framework.codegen.model.constructor.util.plus +import java.util.* + +class PythonCgStatementConstructorImpl(context: CgContext) : + CgStatementConstructor, + CgContextOwner by context, + CgCallableAccessManager by getCallableAccessManagerBy(context) { + + private val nameGenerator = getNameGeneratorBy(context) + + override fun newVar( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutable: Boolean, + init: () -> CgExpression + ): CgVariable { + val declarationOrVar: Either = + createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType, + model, + baseName, + isMock, + isMutable, + init + ) + + return declarationOrVar.either( + { declaration -> + currentBlock += declaration + + declaration.variable + }, + { variable -> variable } + ) + } + + override fun createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutableVar: Boolean, + init: () -> CgExpression + ): Either { + val baseExpr = init() + + val name = nameGenerator.variableName(baseType, baseName) + val (type, expr) = guardExpression(baseType, baseExpr) + + val declaration = buildDeclaration { + variableType = type + variableName = name + initializer = expr + } + rememberVariableForModel(declaration.variable, model) + return Either.left(declaration) + } + + override fun CgExpression.`=`(value: Any?) { + currentBlock += buildAssignment { + lValue = this@`=` + rValue = value.resolve() + } + } + + override fun CgExpression.and(other: CgExpression): CgLogicalAnd = + CgLogicalAnd(this, other) + + override fun CgExpression.or(other: CgExpression): CgLogicalOr = + CgLogicalOr(this, other) + + override fun ifStatement( + condition: CgExpression, + trueBranch: () -> Unit, + falseBranch: (() -> Unit)? + ): CgIfStatement { + val trueBranchBlock = block(trueBranch) + val falseBranchBlock = falseBranch?.let { block(it) } + return CgIfStatement(condition, trueBranchBlock, falseBranchBlock).also { + currentBlock += it + } + } + + override fun forLoop(init: CgForLoopBuilder.() -> Unit) { + currentBlock += buildForLoop(init) + } + + override fun whileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun doWhileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildDoWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun forEachLoop(init: CgForEachLoopBuilder.() -> Unit) = withNameScope { + currentBlock += buildCgForEachLoop(init) + } + + override fun getClassOf(classId: ClassId): CgExpression { + TODO("Not yet implemented") + } + + override fun createFieldVariable(fieldId: FieldId): CgVariable { + TODO("Not yet implemented") + } + + override fun createExecutableVariable(executableId: ExecutableId, arguments: List): CgVariable { + TODO("Not yet implemented") + } + + override fun tryBlock(init: () -> Unit): CgTryCatch = tryBlock(init, null) + + override fun tryBlock(init: () -> Unit, resources: List?): CgTryCatch = + buildTryCatch { + statements = block(init) + this.resources = resources + } + + override fun CgTryCatch.catch(exception: ClassId, init: (CgVariable) -> Unit): CgTryCatch { + val newHandler = buildExceptionHandler { + val e = declareVariable(exception, nameGenerator.variableName(exception.simpleName.replaceFirstChar { + it.lowercase( + Locale.getDefault() + ) + })) + this.exception = e + this.statements = block { init(e) } + } + return this.copy(handlers = handlers + newHandler) + } + + override fun CgTryCatch.finally(init: () -> Unit): CgTryCatch { + val finallyBlock = block(init) + return this.copy(finally = finallyBlock) + } + + override fun CgExpression.isInstance(value: CgExpression): CgIsInstance = TODO("Not yet implemented") + + override fun innerBlock(init: () -> Unit): CgInnerBlock = + CgInnerBlock(block(init)).also { + currentBlock += it + } + + override fun comment(text: String): CgComment = + CgSingleLineComment(text).also { + currentBlock += it + } + + override fun comment(): CgComment = + CgSingleLineComment("").also { + currentBlock += it + } + + override fun multilineComment(lines: List): CgComment = + CgMultilineComment(lines).also { + currentBlock += it + } + + override fun lambda(type: ClassId, vararg parameters: CgVariable, body: () -> Unit): CgAnonymousFunction { + return withNameScope { + for (parameter in parameters) { + declareParameter(parameter.type, parameter.name) + } + val paramDeclarations = parameters.map { CgParameterDeclaration(it) } + CgAnonymousFunction(type, paramDeclarations, block(body)) + } + } + + override fun addAnnotation(classId: ClassId, argument: Any?, target: AnnotationTarget): CgAnnotation { + val annotation = CgSingleArgAnnotation(classId, argument.resolve(), target) + addAnnotation(annotation) + return annotation + } + + override fun addAnnotation( + classId: ClassId, + namedArguments: List, + target: AnnotationTarget, + ): CgAnnotation { + val annotation = CgMultipleArgsAnnotation( + classId, + namedArguments.toMutableList(), + target, + ) + addAnnotation(annotation) + return annotation + } + + override fun addAnnotation( + classId: ClassId, + target: AnnotationTarget, + buildArguments: MutableList>.() -> Unit, + ): CgAnnotation { + val arguments = mutableListOf>() + .apply(buildArguments) + .map { (name, value) -> CgNamedAnnotationArgument(name, value) } + val annotation = CgMultipleArgsAnnotation(classId, arguments.toMutableList(), target) + addAnnotation(annotation) + return annotation + } + + private fun addAnnotation(annotation: CgAnnotation) { + when (annotation.target) { + AnnotationTarget.Method -> collectedMethodAnnotations.add(annotation) + AnnotationTarget.Class, + AnnotationTarget.Field -> error("Annotation ${annotation.target} is not supported in Python") + } + + val classId = annotation.classId + if (classId is PythonClassId) { + importIfNeeded(classId) + } + } + + override fun returnStatement(expression: () -> CgExpression) { + currentBlock += CgReturnStatement(expression()) + } + + override fun throwStatement(exception: () -> CgExpression): CgThrowStatement = + CgThrowStatement(exception()).also { currentBlock += it } + + override fun emptyLine() { + currentBlock += CgEmptyLine + } + + override fun emptyLineIfNeeded() { + val lastStatement = currentBlock.lastOrNull() ?: return + if (lastStatement is CgEmptyLine) return + emptyLine() + } + + override fun declareVariable(type: ClassId, name: String): CgVariable = + CgVariable(name, type).also { + rememberVariableForModel(it) + } + + override fun guardExpression(baseType: ClassId, expression: CgExpression): ExpressionWithType { + return ExpressionWithType(baseType, expression) + } + + override fun wrapTypeIfRequired(baseType: ClassId): ClassId = + if (baseType.isAccessibleFrom(testClassPackageName)) baseType else objectClassId +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt new file mode 100644 index 0000000000..665b1c9f16 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt @@ -0,0 +1,19 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.domain.models.SimpleTestClassModel +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.tree.CgSimpleTestClassConstructor +import org.utbot.framework.codegen.tree.buildClassFile + +internal class PythonCgTestClassConstructor(context: CgContext) : CgSimpleTestClassConstructor(context) { + override fun construct(testClassModel: SimpleTestClassModel): CgClassFile { + return buildClassFile { + this.declaredClass = withTestClassScope { + with(currentTestClassContext) { testClassSuperclass = testFramework.testSuperClass } + constructTestClass(testClassModel) + } + imports.addAll(context.collectedImports) + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt new file mode 100644 index 0000000000..7bf8676333 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt @@ -0,0 +1,211 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.tree.CgComponents +import org.utbot.framework.codegen.tree.CgVariableConstructor +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonMethodId +import org.utbot.python.framework.api.python.PythonModel +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.PythonTreeModel +import org.utbot.python.framework.api.python.RawPythonAnnotation +import org.utbot.python.framework.api.python.util.comparePythonTree +import org.utbot.python.framework.api.python.util.pythonDictClassId +import org.utbot.python.framework.api.python.util.pythonListClassId +import org.utbot.python.framework.api.python.util.pythonNoneClassId +import org.utbot.python.framework.codegen.PythonCgLanguageAssistant +import org.utbot.python.framework.codegen.model.tree.CgPythonDict +import org.utbot.python.framework.codegen.model.tree.CgPythonIndex +import org.utbot.python.framework.codegen.model.tree.CgPythonIterator +import org.utbot.python.framework.codegen.model.tree.CgPythonList +import org.utbot.python.framework.codegen.model.tree.CgPythonNdarray +import org.utbot.python.framework.codegen.model.tree.CgPythonRepr +import org.utbot.python.framework.codegen.model.tree.CgPythonSet +import org.utbot.python.framework.codegen.model.tree.CgPythonTree +import org.utbot.python.framework.codegen.model.tree.CgPythonTuple + + +class PythonCgVariableConstructor(cgContext: CgContext) : CgVariableConstructor(cgContext) { + private val nameGenerator = CgComponents.getNameGeneratorBy(context) + override fun getOrCreateVariable(model: UtModel, name: String?): CgValue { + val baseName = name ?: nameGenerator.nameFrom(model.classId) + + return valueByUtModelWrapper.getOrPut(model.wrap()) { + when (model) { + is PythonTreeModel -> { + val (value, arguments) = pythonBuildObject(model.tree, baseName) + CgPythonTree(model.classId, model.tree, value, arguments) + } + is PythonModel -> error("Unexpected PythonModel: ${model::class}") + else -> super.getOrCreateVariable(model, name) + } + } + } + + private fun pythonBuildObject(objectNode: PythonTree.PythonTreeNode, baseName: String? = null): Pair> { + val id = objectNode.id + val assistant = (context.cgLanguageAssistant as PythonCgLanguageAssistant) + return when (objectNode) { + is PythonTree.PrimitiveNode -> { + Pair(CgPythonRepr(objectNode.type, objectNode.repr), emptyList()) + } + + is PythonTree.ListNode -> { + if (PythonTree.isRecursiveObject(objectNode)) { + val obj = PythonTree.ReduceNode( + id, + pythonListClassId, + PythonClassId("builtins.list"), + emptyList(), + mutableMapOf(), + objectNode.items.values.toList(), + emptyMap(), + ) + pythonBuildObject(obj) + } else { + val items = objectNode.items.values.map { pythonBuildObject(it) } + Pair(CgPythonList(items.map { it.first }), items.flatMap { it.second }) + } + } + + is PythonTree.NDArrayNode -> { + val items = objectNode.items.values.map { pythonBuildObject(it) } + val shape = objectNode.dimensions + val type = objectNode.items.values.firstOrNull()?.type + Pair(CgPythonNdarray(items.map { it.first }, shape, type?:PythonClassId("int")), items.flatMap { it.second }) + } + + is PythonTree.TupleNode -> { + val items = objectNode.items.values.map { pythonBuildObject(it) } + Pair(CgPythonTuple(items.map {it.first}), items.flatMap { it.second }) + } + + is PythonTree.SetNode -> { + val items = objectNode.items.map { pythonBuildObject(it) } + Pair(CgPythonSet(items.map {it.first}.toSet()), items.flatMap { it.second }) + } + + is PythonTree.DictNode -> { + if (PythonTree.isRecursiveObject(objectNode)) { + val obj = PythonTree.ReduceNode( + id, + pythonDictClassId, + PythonClassId("builtins.dict"), + emptyList(), + mutableMapOf(), + emptyList(), + objectNode.items, + ) + pythonBuildObject(obj) + } else { + val keys = objectNode.items.keys.map { pythonBuildObject(it) } + val values = objectNode.items.values.map { pythonBuildObject(it) } + Pair( + CgPythonDict( + keys.zip(values).associate { (key, value) -> + key.first to value.first + } + ), + keys.flatMap { it.second } + values.flatMap { it.second } + ) + } + } + + is PythonTree.IteratorNode -> { + val items = objectNode.items.values.map { pythonBuildObject(it) } + Pair(CgPythonIterator(items.map {it.first}), items.flatMap { it.second }) + } + + is PythonTree.ReduceNode -> { + if (assistant.memoryObjects.containsKey(id)) { + val tree = assistant.memoryObjectsModels[id] + val savedObj = assistant.memoryObjects[id] + if (tree != null && savedObj != null && comparePythonTree(tree, objectNode)) { + return Pair(savedObj, emptyList()) + } + } + + val initArgs = objectNode.args.map { + getOrCreateVariable(PythonTreeModel(it, it.type)) + } + val constructor = ConstructorId( + objectNode.constructor, + initArgs.map { it.type } + ) + val constructorCall = CgConstructorCall(constructor, initArgs) + val obj = newVar(objectNode.type, baseName) { + constructorCall + } + + assistant.memoryObjects[id] = obj + assistant.memoryObjectsModels[id] = objectNode + + val state = objectNode.state.map { (key, value) -> + key to getOrCreateVariable(PythonTreeModel(value, value.type)) + }.toMap() + val listitems = objectNode.listitems.map { + getOrCreateVariable(PythonTreeModel(it, it.type)) + } + val dictitems = objectNode.dictitems.map { (key, value) -> + val keyObj = getOrCreateVariable(PythonTreeModel(key, key.type)) + val valueObj = getOrCreateVariable(PythonTreeModel(value, value.type)) + keyObj to valueObj + } + + if (objectNode.customState) { + val setstate = state["state"]!! + val methodCall = CgMethodCall( + obj, + PythonMethodId( + obj.type as PythonClassId, + "__setstate__", + NormalizedPythonAnnotation(pythonNoneClassId.name), + listOf(RawPythonAnnotation(setstate.type.name)) + ), + listOf(setstate) + ) + +methodCall + } else { + state.forEach { (key, value) -> + obj[FieldId(objectNode.type, key)] `=` value + } + } + listitems.forEach { + val methodCall = CgMethodCall( + obj, + PythonMethodId( + obj.type as PythonClassId, + "append", + NormalizedPythonAnnotation(pythonNoneClassId.name), + listOf(RawPythonAnnotation(it.type.name)) + ), + listOf(it) + ) + +methodCall + } + dictitems.forEach { (key, value) -> + val index = CgPythonIndex( + value.type as PythonClassId, + obj, + key + ) + index `=` value + } + + return Pair(obj, context.currentBlock.toList()) + } + + else -> { + throw UnsupportedOperationException() + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt new file mode 100644 index 0000000000..2bd9e38bb2 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt @@ -0,0 +1,183 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.TestClassContext +import org.utbot.framework.codegen.domain.models.AnnotationTarget.Method +import org.utbot.framework.codegen.domain.models.CgEqualTo +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgNamedAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.services.framework.TestFrameworkManager +import org.utbot.framework.plugin.api.ClassId +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.api.python.util.pythonBoolClassId +import org.utbot.python.framework.api.python.util.pythonNoneClassId +import org.utbot.python.framework.api.python.util.pythonStrClassId +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.framework.codegen.model.constructor.util.importIfNeeded +import org.utbot.python.framework.codegen.model.tree.CgPythonAssertEquals +import org.utbot.python.framework.codegen.model.tree.CgPythonFunctionCall +import org.utbot.python.framework.codegen.model.tree.CgPythonRepr +import org.utbot.python.framework.codegen.model.tree.CgPythonTuple +import org.utbot.python.framework.codegen.model.tree.CgPythonWith + +internal class PytestManager(context: CgContext) : TestFrameworkManager(context) { + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Pytest) { "According to settings, Pytest was expected, but got: $testFramework" } + require(exception is PythonClassId) { "Exceptions must be PythonClassId" } + context.importIfNeeded(PythonClassId("pytest.raises")) + importIfNeeded(exception) + val withExpression = CgPythonFunctionCall( + pythonNoneClassId, + "pytest.raises", + listOf(CgLiteral(exception, exception.prettyName)) + ) + +CgPythonWith(withExpression, null, context.block(block)) + } + + override val isExpectedExceptionExecutionBreaking: Boolean = true + + override fun addDataProviderAnnotations(dataProviderMethodName: String) { + error("Parametrized tests are not supported for Python") + } + + override fun createArgList(length: Int): CgVariable { + error("Parametrized tests are not supported for Python") + } + + override fun addParameterizedTestAnnotations(dataProviderMethodName: String?) { + error("Parametrized tests are not supported for Python") + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + TODO("Not yet implemented") + } + + override fun addTestDescription(description: String) = Unit + + override fun disableTestMethod(reason: String) { + require(testFramework is Pytest) { "According to settings, Pytest was expected, but got: $testFramework" } + + context.importIfNeeded(Pytest.skipDecoratorClassId) + val reasonArgument = CgNamedAnnotationArgument( + name = "reason", + value = CgPythonRepr(pythonStrClassId, "'${reason.replace("\"", "'")}'"), + ) + statementConstructor.addAnnotation( + classId = Pytest.skipDecoratorClassId, + namedArguments = listOf(reasonArgument), + target = Method + ) + } + + override val dataProviderMethodsHolder: TestClassContext get() = + error("Parametrized tests are not supported for Python") + + override fun addAnnotationForNestedClasses() { + error("Nested classes annotation does not exist in PyTest") + } + + override fun assertEquals(expected: CgValue, actual: CgValue) { + +CgPythonAssertEquals( + CgEqualTo(actual, expected) + ) + } + + override fun assertSame(expected: CgValue, actual: CgValue) { + error("assertSame does not exist in PyTest") + } + + fun assertIsinstance(types: List, actual: CgVariable) { + +CgPythonAssertEquals( + CgPythonFunctionCall( + pythonBoolClassId, + "isinstance", + listOf( + actual, + if (types.size == 1) + CgLiteral(pythonAnyClassId, types[0].prettyName) + else + CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.prettyName) }) + ), + ), + ) + } +} + +internal class UnittestManager(context: CgContext) : TestFrameworkManager(context) { + override val isExpectedExceptionExecutionBreaking: Boolean = true + + override val dataProviderMethodsHolder: TestClassContext + get() = error("Parametrized tests are not supported in Unittest") + + override fun addAnnotationForNestedClasses() { + error("Nested classes annotation does not exist in Unittest") + } + + override fun assertSame(expected: CgValue, actual: CgValue) { + error("assertSame does not exist in Unittest") + } + + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Unittest) { "According to settings, Unittest was expected, but got: $testFramework" } + require(exception is PythonClassId) { "Exceptions must be PythonClassId" } + importIfNeeded(exception) + val withExpression = CgPythonFunctionCall( + pythonNoneClassId, + "self.assertRaises", + listOf(CgLiteral(exception, exception.prettyName)) + ) + +CgPythonWith(withExpression, null, context.block(block)) + } + + override fun addDataProviderAnnotations(dataProviderMethodName: String) { + error("Parametrized tests are not supported for Python") + } + + override fun createArgList(length: Int): CgVariable { + error("Parametrized tests are not supported for Python") + } + + override fun addParameterizedTestAnnotations(dataProviderMethodName: String?) { + error("Parametrized tests are not supported for Python") + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + TODO("Not yet implemented") + } + + override fun addTestDescription(description: String) = Unit + + override fun disableTestMethod(reason: String) { + require(testFramework is Unittest) { "According to settings, Unittest was expected, but got: $testFramework" } + + val reasonArgument = CgNamedAnnotationArgument( + name = "reason", + value = CgPythonRepr(pythonStrClassId, "'${reason.replace("\"", "'")}'"), + ) + statementConstructor.addAnnotation( + classId = Unittest.skipDecoratorClassId, + namedArguments = listOf(reasonArgument), + target = Method, + ) + } + + fun assertIsinstance(types: List, actual: CgVariable) { + +assertions[assertTrue]( + CgPythonFunctionCall( + pythonBoolClassId, + "isinstance", + listOf( + actual, + if (types.size == 1) + CgLiteral(pythonAnyClassId, types[0].prettyName) + else + CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.prettyName) }) + ), + ), + ) + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt new file mode 100644 index 0000000000..0feb5e99d8 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -0,0 +1,52 @@ +package org.utbot.python.framework.codegen.model.constructor.util + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentSet +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonMethodId +import org.utbot.python.framework.api.python.pythonBuiltinsModuleName +import org.utbot.python.framework.codegen.model.PythonUserImport +import org.utbot.python.framework.codegen.model.tree.CgPythonFunctionCall + +internal fun CgContextOwner.importIfNeeded(method: PythonMethodId) { + collectedImports += PythonUserImport(method.moduleName) +} + +internal fun CgContextOwner.importIfNeeded(pyClass: PythonClassId) { + collectedImports += PythonUserImport(pyClass.moduleName) +} + +internal operator fun PersistentList.plus(element: T): PersistentList = + this.add(element) + +internal operator fun PersistentList.plus(other: PersistentList): PersistentList = + this.addAll(other) + +internal operator fun PersistentSet.plus(element: T): PersistentSet = + this.add(element) + +internal operator fun PersistentSet.plus(other: PersistentSet): PersistentSet = + this.addAll(other) + +internal fun PythonClassId.dropBuiltins(): PythonClassId { + return if (this.rootModuleName == pythonBuiltinsModuleName) { + val moduleParts = this.moduleName.split(".", limit = 2) + if (moduleParts.size > 1) { + PythonClassId(moduleParts[1], this.simpleName) + } else { + PythonClassId(this.name.split(".", limit = 2).last()) + } + } else + this +} + +internal fun String.dropBuiltins(): String { + val builtinsPrefix = "$pythonBuiltinsModuleName." + return if (this.startsWith(builtinsPrefix)) + this.drop(builtinsPrefix.length) + else + this +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt new file mode 100644 index 0000000000..47b0cc1437 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt @@ -0,0 +1,676 @@ +package org.utbot.python.framework.codegen.model.constructor.visitor + +import org.apache.commons.text.StringEscapeUtils +import org.utbot.framework.codegen.domain.RegularImport +import org.utbot.framework.codegen.domain.StaticImport +import org.utbot.framework.codegen.domain.models.CgAbstractMultilineComment +import org.utbot.framework.codegen.domain.models.CgAllocateArray +import org.utbot.framework.codegen.domain.models.CgAllocateInitializedArray +import org.utbot.framework.codegen.domain.models.CgAnonymousFunction +import org.utbot.framework.codegen.domain.models.CgArrayAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgArrayInitializer +import org.utbot.framework.codegen.domain.models.CgClass +import org.utbot.framework.codegen.domain.models.CgClassBody +import org.utbot.framework.codegen.domain.models.CgClassFile +import org.utbot.framework.codegen.domain.models.CgCommentedAnnotation +import org.utbot.framework.codegen.domain.models.CgConstructorCall +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgDocRegularStmt +import org.utbot.framework.codegen.domain.models.CgDocumentationComment +import org.utbot.framework.codegen.domain.models.CgElement +import org.utbot.framework.codegen.domain.models.CgEqualTo +import org.utbot.framework.codegen.domain.models.CgErrorTestMethod +import org.utbot.framework.codegen.domain.models.CgErrorWrapper +import org.utbot.framework.codegen.domain.models.CgExecutableCall +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgForEachLoop +import org.utbot.framework.codegen.domain.models.CgForLoop +import org.utbot.framework.codegen.domain.models.CgFormattedString +import org.utbot.framework.codegen.domain.models.CgFrameworkUtilMethod +import org.utbot.framework.codegen.domain.models.CgGetJavaClass +import org.utbot.framework.codegen.domain.models.CgGetKotlinClass +import org.utbot.framework.codegen.domain.models.CgGetLength +import org.utbot.framework.codegen.domain.models.CgInnerBlock +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgMultilineComment +import org.utbot.framework.codegen.domain.models.CgMultipleArgsAnnotation +import org.utbot.framework.codegen.domain.models.CgNamedAnnotationArgument +import org.utbot.framework.codegen.domain.models.CgNotNullAssertion +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration +import org.utbot.framework.codegen.domain.models.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.domain.models.CgSingleArgAnnotation +import org.utbot.framework.codegen.domain.models.CgSingleLineComment +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgSwitchCase +import org.utbot.framework.codegen.domain.models.CgSwitchCaseLabel +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgThisInstance +import org.utbot.framework.codegen.domain.models.CgTripleSlashMultilineComment +import org.utbot.framework.codegen.domain.models.CgTryCatch +import org.utbot.framework.codegen.domain.models.CgTypeCast +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.renderer.CgAbstractRenderer +import org.utbot.framework.codegen.renderer.CgPrinter +import org.utbot.framework.codegen.renderer.CgPrinterImpl +import org.utbot.framework.codegen.renderer.CgRendererContext +import org.utbot.framework.codegen.tree.VisibilityModifier +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.TypeParameters +import org.utbot.framework.plugin.api.WildcardTypeParameter +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.pythonBuiltinsModuleName +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.codegen.model.PythonImport +import org.utbot.python.framework.codegen.model.PythonSysPathImport +import org.utbot.python.framework.codegen.model.constructor.util.dropBuiltins +import org.utbot.python.framework.codegen.model.tree.CgPythonAssertEquals +import org.utbot.python.framework.codegen.model.tree.CgPythonDict +import org.utbot.python.framework.codegen.model.tree.CgPythonFunctionCall +import org.utbot.python.framework.codegen.model.tree.CgPythonIndex +import org.utbot.python.framework.codegen.model.tree.CgPythonIterator +import org.utbot.python.framework.codegen.model.tree.CgPythonList +import org.utbot.python.framework.codegen.model.tree.CgPythonNdarray +import org.utbot.python.framework.codegen.model.tree.CgPythonNamedArgument +import org.utbot.python.framework.codegen.model.tree.CgPythonRange +import org.utbot.python.framework.codegen.model.tree.CgPythonRepr +import org.utbot.python.framework.codegen.model.tree.CgPythonSet +import org.utbot.python.framework.codegen.model.tree.CgPythonTree +import org.utbot.python.framework.codegen.model.tree.CgPythonTuple +import org.utbot.python.framework.codegen.model.tree.CgPythonWith +import org.utbot.python.framework.codegen.model.tree.CgPythonZip +import org.utbot.python.framework.codegen.utils.toRelativeRawPath + +internal class CgPythonRenderer( + context: CgRendererContext, + printer: CgPrinter = CgPrinterImpl() +) : + CgAbstractRenderer(context, printer), + CgPythonVisitor { + + override val regionStart: String = "# region" + override val regionEnd: String = "# endregion" + + override val statementEnding: String = "" + + override val logicalAnd: String + get() = "and" + + override val logicalOr: String + get() = "or" + + override val langPackage: String = "python" + + override val ClassId.methodsAreAccessibleAsTopLevel: Boolean + get() = false + + override fun visit(element: CgClassFile) { + renderClassFileImports(element) + + println() + println() + + element.declaredClass.accept(this) + } + + override fun visit(element: CgClass) { + print("class ") + print(element.simpleName) + if (element.superclass != null) { + print("(${element.superclass!!.asString()})") + } + println(":") + withIndent { element.body.accept(this) } + println("") + } + + override fun visit(element: CgCommentedAnnotation) { + print("#") + element.annotation.accept(this) + } + + override fun visit(element: CgSingleArgAnnotation) { + print("@${element.classId.asString()}") + print("(") + element.argument.accept(this) + println(")") + } + + override fun visit(element: CgMultipleArgsAnnotation) { + print("@${element.classId.asString()}") + if (element.arguments.isNotEmpty()) { + print("(") + element.arguments.renderSeparated() + print(")") + } + println() + } + + override fun visit(element: CgNamedAnnotationArgument) { + print(element.name) + print("=") + element.value.accept(this) + } + + override fun visit(element: CgSingleLineComment) { + println("# ${element.comment}") + } + + override fun visit(element: CgAbstractMultilineComment) { + visit(element as CgElement) + } + + override fun visit(element: CgTripleSlashMultilineComment) { + element.lines.forEach { line -> + println("# $line") + } + } + + override fun visit(element: CgMultilineComment) { + val lines = element.lines + if (lines.isEmpty()) return + + if (lines.size == 1) { + print("# ${lines.first()}") + return + } + + // print lines saving indentation + print("\"\"\"") + println(lines.first()) + lines.subList(1, lines.lastIndex).forEach { println(it) } + print(lines.last()) + println("\"\"\"") + } + + override fun visit(element: CgDocumentationComment) { + if (element.lines.all { it.isEmpty() }) return + + println("\"\"\"") + for (line in element.lines) line.accept(this) + println("\"\"\"") + } + + override fun visit(element: CgDocRegularStmt) { + if (element.isEmpty()) return + + println(element.stmt) + } + + override fun visit(element: CgErrorWrapper) { + element.expression.accept(this) + } + + override fun visit(element: CgClassBody) { + // render regions for test methods + for ((i, region) in (element.methodRegions + element.nestedClassRegions).withIndex()) { + if (i != 0) println() + + region.accept(this) + } + + if (element.staticDeclarationRegions.isEmpty()) { + return + } + } + + override fun visit(element: CgTryCatch) { + print("try") + // TODO introduce CgBlock + visit(element.statements) + for ((exception, statements) in element.handlers) { + print("except ") + renderExceptionCatchVariable(exception) + // TODO introduce CgBlock + visit(statements, printNextLine = element.finally == null) + } + element.finally?.let { + print("finally") + // TODO introduce CgBlock + visit(element.finally!!, printNextLine = true) + } + } + + override fun visit(element: CgArrayAnnotationArgument) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgAnonymousFunction) { + print("lambda ") + element.parameters.renderSeparated() + print(": ") + + visit(element.body) + } + + override fun visit(element: CgEqualTo) { + element.left.accept(this) + val isCompareTypes = listOf("builtins.bool", "types.NoneType") + if (isCompareTypes.contains(element.right.type.canonicalName)) { + print(" is ") + } else { + print(" == ") + } + element.right.accept(this) + } + + override fun visit(element: CgTypeCast) { + TODO("Not yet implemented") + } + + override fun visit(element: CgNotNullAssertion) { + element.expression.accept(this) + } + + override fun visit(element: CgAllocateArray) { + print("[None] * ${element.size}") + } + + override fun visit(element: CgAllocateInitializedArray) { + print(" [") + element.initializer.accept(this) + print(" ]") + } + + override fun visit(element: CgArrayInitializer) { + val elementType = element.elementType + val elementsInLine = arrayElementsInLine(elementType) + + print("[") + element.values.renderElements(elementsInLine) + print("]") + } + + override fun visit(element: CgSwitchCaseLabel) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgSwitchCase) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgParameterDeclaration) { + print(element.name.escapeNamePossibleKeyword()) + if (element.type.name != "") + print(": ") + print(element.type.name) + } + + override fun visit(element: CgGetLength) { + print("len(") + element.variable.accept(this) + print(")") + } + + override fun visit(element: CgGetJavaClass) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgGetKotlinClass) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgConstructorCall) { + print(element.executableId.classId.name.dropBuiltins()) + renderExecutableCallArguments(element) + } + + override fun renderRegularImport(regularImport: RegularImport) { + val escapedImport = getEscapedImportRendering(regularImport) + println("import $escapedImport") + } + + override fun renderStaticImport(staticImport: StaticImport) { + throw UnsupportedOperationException() + } + + override fun renderClassFileImports(element: CgClassFile) { + element.imports + .filterIsInstance() + .sortedBy { it.order } + .forEach { renderPythonImport(it) } + } + + fun renderPythonImport(pythonImport: PythonImport) { + val importBuilder = StringBuilder() + if (pythonImport is PythonSysPathImport) { + importBuilder.append("sys.path.append(${pythonImport.sysPath.toRelativeRawPath()})") + } else if (pythonImport.moduleName == null) { + importBuilder.append("import ${pythonImport.importName}") + } else { + importBuilder.append("from ${pythonImport.moduleName} import ${pythonImport.importName}") + } + if (pythonImport.alias != null) { + importBuilder.append(" as ${pythonImport.alias}") + } + println(importBuilder.toString()) + } + + override fun renderMethodSignature(element: CgTestMethod) { + print("def ") + print(element.name) + + print("(") + val newLinesNeeded = element.parameters.size > maxParametersAmountInOneLine + val selfParameter = CgThisInstance(pythonAnyClassId) + (listOf(selfParameter) + element.parameters).renderSeparated(newLinesNeeded) + print(")") + } + + override fun renderMethodSignature(element: CgErrorTestMethod) { + print("def ") + print(element.name) + print("(") + val selfParameter = CgThisInstance(pythonAnyClassId) + listOf(selfParameter).renderSeparated() + print(")") + } + + override fun visit(element: CgErrorTestMethod) { + renderMethodDocumentation(element) + renderMethodSignature(element) + visit(element as CgMethod) + withIndent { + println("pass") + } + } + + override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { + val returnType = element.returnType.canonicalName + println("def ${element.name}() -> $returnType: pass") + } + + override fun renderMethodSignature(element: CgFrameworkUtilMethod) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgInnerBlock) { + withIndent { + for (statement in element.statements) { + statement.accept(this) + } + } + } + + override fun renderForLoopVarControl(element: CgForLoop) { + print("for ") + visit(element.condition) + print(" in ") + element.initialization.accept(this@CgPythonRenderer) + println(":") + } + + override fun renderDeclarationLeftPart(element: CgDeclaration) { + visit(element.variable) + } + + override fun toStringConstantImpl(byte: Byte): String { + return "b'$byte'" + } + + override fun toStringConstantImpl(short: Short): String { + return "$short" + } + + override fun toStringConstantImpl(int: Int): String { + return "$int" + } + + override fun toStringConstantImpl(long: Long): String { + return "$long" + } + + override fun toStringConstantImpl(float: Float): String { + return "$float" + } + + override fun renderAccess(caller: CgExpression) { + print(".") + } + + override fun renderTypeParameters(typeParameters: TypeParameters) { + if (typeParameters.parameters.isNotEmpty()) { + print("[") + if (typeParameters is WildcardTypeParameter) { + print("typing.Any") + } else { + print(typeParameters.parameters.joinToString { it.name }) + } + print("]") + } + } + + override fun renderExecutableCallArguments(executableCall: CgExecutableCall) { + print("(") + executableCall.arguments.renderSeparated() + print(")") + } + + override fun renderExceptionCatchVariable(exception: CgVariable) { + print(exception.type.canonicalName) + print(" as ") + print(exception.name.escapeNamePossibleKeyword()) + } + + override fun escapeNamePossibleKeywordImpl(s: String): String = s + override fun renderVisibility(modifier: VisibilityModifier) { + throw UnsupportedOperationException() + } + + override fun renderClassModality(aClass: CgClass) { + throw UnsupportedOperationException() + } + + override fun visit(block: List, printNextLine: Boolean) { + println(":") + + withIndent { + for (statement in block) { + statement.accept(this) + } + } + + if (printNextLine) println() + } + + override fun visit(element: CgThisInstance) { + print("self") + } + + override fun visit(element: CgMethod) { + visit(listOf(element.documentation) + element.statements, printNextLine = false) + } + + override fun visit(element: CgTestMethod) { + for (annotation in element.annotations) { + annotation.accept(this) + } + renderMethodSignature(element) + visit(element as CgMethod) + } + + override fun visit(element: CgMethodCall) { + if (element.caller == null) { + val module = (element.executableId.classId as PythonClassId).moduleName + if (module != pythonBuiltinsModuleName) { + print("$module.") + } + } else { + element.caller!!.accept(this) + print(".") + } + print(element.executableId.name) + + renderTypeParameters(element.typeParameters) + renderExecutableCallArguments(element) + } + + override fun visit(element: CgPythonRepr) { + val content = element.content.dropBuiltins() + if (content.startsWith("\"") && content.endsWith("\"")) { + val realContent = content.slice(1 until content.length - 1) + if (realContent.startsWith("r\\\"") && realContent.endsWith("\\\"")) { // raw string + val innerContent = realContent.slice(5 until realContent.length - 4) + print("r\"${innerContent.replace("\r", "\\r").replace("\n", "\\n")}\"") + } else { + print("\"${realContent.replace("\r", "\\r").replace("\n", "\\n")}\"") + } + } else { + print(content.dropBuiltins()) + } + } + + override fun visit(element: CgPythonIndex) { + visit(element.obj) + print("[") + element.index.accept(this) + print("]") + } + + override fun visit(element: CgPythonFunctionCall) { + print(element.name.dropBuiltins()) + print("(") + val newLinesNeeded = element.parameters.size > maxParametersAmountInOneLine + element.parameters.renderSeparated(newLinesNeeded) + print(")") + } + + override fun visit(element: CgPythonAssertEquals) { + print("${element.keyword} ") + element.expression.accept(this) + println() + } + + override fun visit(element: CgPythonRange) { + print("range(") + listOf(element.start, element.stop, element.step).renderSeparated() + print(")") + } + + override fun visit(element: CgPythonZip) { + print("zip(") + listOf(element.first, element.second).renderSeparated() + print(")") + } + + override fun visit(element: CgPythonList) { + print("[") + element.elements.renderSeparated() + print("]") + } + + override fun visit(element: CgPythonNdarray) { + + print("numpy.ndarray(") + print("dtype=") + print(element.dtype.toString().removePrefix("builtins.")) + print(",") + print("shape=(") + + print(element.shape.joinToString(",") { it.toString() }) + print("), buffer=numpy.array([") + element.elements.renderSeparated() + print("]))") + + + } + + override fun visit(element: CgPythonTuple) { + if (element.elements.isEmpty()) { + print("tuple()") + } else { + print("(") + element.elements.renderSeparated() + if (element.elements.size == 1) { + print(",") + } + print(")") + } + } + + override fun visit(element: CgPythonSet) { + if (element.elements.isEmpty()) + print("set()") + else { + print("{") + element.elements.toList().renderSeparated() + print("}") + } + } + + override fun visit(element: CgPythonIterator) { + print("iter([") + element.elements.renderSeparated() + print("])") + } + + override fun visit(element: CgPythonTree) { + element.value.accept(this) + } + + override fun visit(element: CgPythonWith) { + print("with ") + element.expression.accept(this) + if (element.target != null) { + print(" as ") + element.target.accept(this) + } + println(":") + withIndent { element.statements.forEach { it.accept(this) } } + } + + override fun visit(element: CgPythonNamedArgument) { + element.name?.let { print("$it=") } + element.value.accept(this) + } + + override fun visit(element: CgPythonDict) { + print("{") + element.elements.map { (key, value) -> + key.accept(this) + print(": ") + value.accept(this) + print(", ") + } + print("}") + } + + override fun visit(element: CgForEachLoop) { + print("for ") + element.condition.accept(this) + print(" in ") + element.iterable.accept(this) + println(":") + withIndent { element.statements.forEach { it.accept(this) } } + } + + override fun visit(element: CgLiteral) { + print(element.value.toString().dropBuiltins()) + } + + override fun visit(element: CgFormattedString) { + print("f\"") + element.array.forEachIndexed { index, cgElement -> + if (cgElement is CgLiteral) { + print("{") + print(cgElement.toStringConstant(asRawString = true)) + print("}") + } else { + print("{") + cgElement.accept(this) + print("}") + } + + if (index < element.array.lastIndex) print(" ") + } + print("\"") + } + + override fun String.escapeCharacters(): String = + StringEscapeUtils + .escapeJava(this) + .replace("\"", "\\\"") + .replace("\\f", "\\u000C") + .replace("\\xxx", "\\\u0058\u0058\u0058") +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt new file mode 100644 index 0000000000..2dcc5b10e4 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt @@ -0,0 +1,37 @@ +package org.utbot.python.framework.codegen.model.constructor.visitor + +import org.utbot.framework.codegen.renderer.CgVisitor +import org.utbot.python.framework.codegen.model.tree.CgPythonAssertEquals +import org.utbot.python.framework.codegen.model.tree.CgPythonDict +import org.utbot.python.framework.codegen.model.tree.CgPythonFunctionCall +import org.utbot.python.framework.codegen.model.tree.CgPythonIndex +import org.utbot.python.framework.codegen.model.tree.CgPythonIterator +import org.utbot.python.framework.codegen.model.tree.CgPythonList +import org.utbot.python.framework.codegen.model.tree.CgPythonNamedArgument +import org.utbot.python.framework.codegen.model.tree.CgPythonRange +import org.utbot.python.framework.codegen.model.tree.CgPythonRepr +import org.utbot.python.framework.codegen.model.tree.CgPythonSet +import org.utbot.python.framework.codegen.model.tree.CgPythonTree +import org.utbot.python.framework.codegen.model.tree.CgPythonTuple +import org.utbot.python.framework.codegen.model.tree.CgPythonWith +import org.utbot.python.framework.codegen.model.tree.CgPythonZip +import org.utbot.python.framework.codegen.model.tree.CgPythonNdarray + +interface CgPythonVisitor : CgVisitor { + + fun visit(element: CgPythonRepr): R + fun visit(element: CgPythonIndex): R + fun visit(element: CgPythonAssertEquals): R + fun visit(element: CgPythonFunctionCall): R + fun visit(element: CgPythonRange): R + fun visit(element: CgPythonDict): R + fun visit(element: CgPythonTuple): R + fun visit(element: CgPythonList): R + fun visit(element: CgPythonNdarray): R + fun visit(element: CgPythonSet): R + fun visit(element: CgPythonIterator): R + fun visit(element: CgPythonTree): R + fun visit(element: CgPythonWith): R + fun visit(element: CgPythonNamedArgument): R + fun visit(element: CgPythonZip): R +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt new file mode 100644 index 0000000000..d8dadd090e --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt @@ -0,0 +1,161 @@ +package org.utbot.python.framework.codegen.model.tree + +import org.utbot.framework.codegen.domain.models.CgElement +import org.utbot.framework.codegen.domain.models.CgExpression +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.renderer.CgVisitor +import org.utbot.framework.plugin.api.ClassId +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonDictClassId +import org.utbot.python.framework.api.python.util.pythonIntClassId +import org.utbot.python.framework.api.python.util.pythonIteratorClassId +import org.utbot.python.framework.api.python.util.pythonListClassId +import org.utbot.python.framework.api.python.util.pythonRangeClassId +import org.utbot.python.framework.api.python.util.pythonSetClassId +import org.utbot.python.framework.api.python.util.pythonTupleClassId +import org.utbot.python.framework.api.python.util.pythonNdarrayClassId +import org.utbot.python.framework.codegen.model.constructor.visitor.CgPythonVisitor + +interface CgPythonElement : CgElement { + override fun accept(visitor: CgVisitor): R = visitor.run { + if (visitor is CgPythonVisitor) { + when (val element = this@CgPythonElement) { + is CgPythonRepr -> visitor.visit(element) + is CgPythonIndex -> visitor.visit(element) + is CgPythonAssertEquals -> visitor.visit(element) + is CgPythonFunctionCall -> visitor.visit(element) + is CgPythonRange -> visitor.visit(element) + is CgPythonList -> visitor.visit(element) + is CgPythonSet -> visitor.visit(element) + is CgPythonDict -> visitor.visit(element) + is CgPythonTuple -> visitor.visit(element) + is CgPythonTree -> visitor.visit(element) + is CgPythonWith -> visitor.visit(element) + is CgPythonNamedArgument -> visitor.visit(element) + is CgPythonIterator -> visitor.visit(element) + is CgPythonZip -> visitor.visit(element) + is CgPythonNdarray -> visitor.visit(element) + else -> throw IllegalArgumentException("Can not visit element of type ${element::class}") + } + } else { + super.accept(visitor) + } + } +} + +class CgPythonTree( + override val type: ClassId, + val tree: PythonTree.PythonTreeNode, + val value: CgValue, + val arguments: List = emptyList() +) : CgValue, CgPythonElement + +class CgPythonRepr( + override val type: ClassId, + val content: String +) : CgValue, CgPythonElement + +class CgPythonAssertEquals( + val expression: CgExpression, + val keyword: String = "assert", +) : CgStatement, CgPythonElement + +class CgPythonFunctionCall( + override val type: PythonClassId, + val name: String, + val parameters: List, +) : CgExpression, CgPythonElement + +class CgPythonIndex( + override val type: PythonClassId, + val obj: CgVariable, + val index: CgExpression, +) : CgValue, CgPythonElement + +class CgPythonRange( + val start: CgValue, + val stop: CgValue, + val step: CgValue, +) : CgValue, CgPythonElement { + override val type: PythonClassId + get() = pythonRangeClassId + + constructor(stop: Int) : this( + CgLiteral(pythonIntClassId, 0), + CgLiteral(pythonIntClassId, stop), + CgLiteral(pythonIntClassId, 1), + ) + + constructor(stop: CgValue) : this( + CgLiteral(pythonIntClassId, 0), + stop, + CgLiteral(pythonIntClassId, 1), + ) +} + +class CgPythonZip( + val first: CgValue, + val second: CgValue +): CgValue, CgPythonElement { + override val type: ClassId + get() = PythonClassId("builtins.zip") +} + +class CgPythonList( + val elements: List +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonListClassId +} + +class CgPythonTuple( + val elements: List +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonTupleClassId +} + +class CgPythonSet( + val elements: Set +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonSetClassId +} + +class CgPythonNdarray( + val elements: List, + val shape: List, + val dtype: PythonClassId +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonNdarrayClassId +} + + +class CgPythonDict( + val elements: Map +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonDictClassId +} + +class CgPythonIterator( + val elements: List, +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonIteratorClassId +} + +data class CgPythonWith( + val expression: CgExpression, + val target: CgExpression?, + val statements: List, +) : CgStatement, CgPythonElement + +class CgPythonNamedArgument( + val name: String?, + val value: CgExpression, +) : CgValue, CgPythonElement { + override val type: ClassId = value.type +} + + + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/utils/StringUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/utils/StringUtils.kt new file mode 100644 index 0000000000..40f3b5b831 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/utils/StringUtils.kt @@ -0,0 +1,12 @@ +package org.utbot.python.framework.codegen.utils + +import java.nio.file.FileSystems + + +fun String.toRelativeRawPath(): String { + val dirname = "os.path.dirname(__file__)" + if (this.isEmpty()) { + return dirname + } + return "$dirname + r'${FileSystems.getDefault().separator}${this}'" +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/external/JavaApiProcessor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/JavaApiProcessor.kt new file mode 100644 index 0000000000..5e18044a11 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/JavaApiProcessor.kt @@ -0,0 +1,18 @@ +package org.utbot.python.framework.external + +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.PythonTestSet + +class JavaApiProcessor( + override val configuration: PythonTestGenerationConfig +) : PythonTestGenerationProcessor() { + override fun saveTests(testsCode: String) { + } + + override fun notGeneratedTestsAction(testedFunctions: List) { + } + + override fun processCoverageInfo(testSets: List) { + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/external/PythonObjectName.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/PythonObjectName.kt new file mode 100644 index 0000000000..92b663d905 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/PythonObjectName.kt @@ -0,0 +1,15 @@ +package org.utbot.python.framework.external + +import org.utbot.python.framework.api.python.pythonBuiltinsModuleName +import org.utbot.python.framework.api.python.util.moduleOfType + +data class PythonObjectName( + val moduleName: String, + val name: String, +) { + constructor(fullName: String) : this( + moduleOfType(fullName) ?: pythonBuiltinsModuleName, + fullName.removePrefix(moduleOfType(fullName) ?: pythonBuiltinsModuleName).removePrefix(".") + ) + val fullName = "$moduleName.$name" +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/external/PythonTestMethodInfo.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/PythonTestMethodInfo.kt new file mode 100644 index 0000000000..1269131aca --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/PythonTestMethodInfo.kt @@ -0,0 +1,16 @@ +package org.utbot.python.framework.external + +import org.utbot.python.PythonMethod +import org.utpython.types.pythonTypeName + +class PythonTestMethodInfo( + val methodName: PythonObjectName, + val moduleFilename: String, + val containingClassName: PythonObjectName? = null +) + +fun PythonMethod.toPythonMethodInfo() = PythonTestMethodInfo( + PythonObjectName(this.name), + this.moduleFilename, + this.containingPythonClass?.let { PythonObjectName(it.pythonTypeName()) } +) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/external/PythonUtBotJavaApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/PythonUtBotJavaApi.kt new file mode 100644 index 0000000000..3392756728 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/PythonUtBotJavaApi.kt @@ -0,0 +1,181 @@ +package org.utbot.python.framework.external + +import mu.KLogger +import mu.KotlinLogging +import org.utbot.common.PathUtil.toPath +import org.utbot.framework.UtSettings +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.python.PythonMethodHeader +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.PythonTestSet +import org.utbot.python.TestFileInformation +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.utils.RequirementsInstaller +import org.utbot.python.utils.Success +import org.utbot.python.utils.findCurrentPythonModule +import java.io.File + +object PythonUtBotJavaApi { + private val logger: KLogger = KotlinLogging.logger {} + + /** + * Generate test sets + * + * @param testMethods methods for test generation + * @param pythonPath a path to the Python executable file + * @param pythonRunRoot a path to the directory where test sets will be executed + * @param directoriesForSysPath a collection of strings that specifies the additional search path for modules, usually it is only project root + * @param timeout a timeout to the test generation process (in milliseconds) + * @param executionTimeout a timeout to one concrete execution + */ + @JvmStatic + fun generateTestSets ( + testMethods: List, + pythonPath: String, + pythonRunRoot: String, + directoriesForSysPath: Collection, + timeout: Long, + executionTimeout: Long = UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis, + ): List { + logger.info("Checking requirements...") + + val installer = RequirementsInstaller() + RequirementsInstaller.checkRequirements( + installer, + pythonPath, + emptyList() + ) + val processor = initPythonTestGeneratorProcessor( + testMethods, + pythonPath, + pythonRunRoot, + directoriesForSysPath.toSet(), + timeout, + executionTimeout, + ) + logger.info("Loading information about Python types...") + val mypyConfig = processor.sourceCodeAnalyze() + logger.info("Generating tests...") + return processor.testGenerate(mypyConfig) + } + + /** + * Generate test sets code + * + * @param testSets a list of test sets + * @param pythonRunRoot a path to the directory where test sets will be executed + * @param directoriesForSysPath a collection of strings that specifies the additional search path for modules, usually it is only project root + * @param testFramework a test framework (Unittest or Pytest) + */ + @JvmStatic + fun renderTestSets ( + testSets: List, + pythonRunRoot: String, + directoriesForSysPath: Collection, + testFramework: TestFramework = Unittest, + ): String { + if (testSets.isEmpty()) return "" + + require(testFramework is Unittest || testFramework is Pytest) { "TestFramework should be Unittest or Pytest" } + + testSets.map { it.method.containingPythonClass } .toSet().let { + require(it.size == 1) { "All test methods should be from one class or only top level" } + it.first() + } + + val containingFile = testSets.map { it.method.moduleFilename } .toSet().let { + require(it.size == 1) { "All test methods should be from one module" } + it.first() + } + val moduleUnderTest = findCurrentPythonModule(directoriesForSysPath, containingFile) + require(moduleUnderTest is Success) + + val testMethods = testSets.map { it.method.toPythonMethodInfo() }.toSet().toList() + + val processor = initPythonTestGeneratorProcessor( + testMethods = testMethods, + pythonRunRoot = pythonRunRoot, + directoriesForSysPath = directoriesForSysPath.toSet(), + testFramework = testFramework, + ) + return processor.testCodeGenerate(testSets) + } + + /** + * Generate test sets and render code + * + * @param testMethods methods for test generation + * @param pythonPath a path to the Python executable file + * @param pythonRunRoot a path to the directory where test sets will be executed + * @param directoriesForSysPath a collection of strings that specifies the additional search path for modules, usually it is only project root + * @param timeout a timeout to the test generation process (in milliseconds) + * @param executionTimeout a timeout to one concrete execution + * @param testFramework a test framework (Unittest or Pytest) + */ + @JvmStatic + fun generate( + testMethods: List, + pythonPath: String, + pythonRunRoot: String, + directoriesForSysPath: Collection, + timeout: Long, + executionTimeout: Long = UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis, + testFramework: TestFramework = Unittest, + ): String { + val testSets = + generateTestSets(testMethods, pythonPath, pythonRunRoot, directoriesForSysPath, timeout, executionTimeout) + return renderTestSets(testSets, pythonRunRoot, directoriesForSysPath, testFramework) + } + + private fun initPythonTestGeneratorProcessor ( + testMethods: List, + pythonPath: String = "", + pythonRunRoot: String, + directoriesForSysPath: Set, + timeout: Long = 60_000, + timeoutForRun: Long = 2_000, + testFramework: TestFramework = Unittest, + ): PythonTestGenerationProcessor { + + val pythonFilePath = testMethods.map { it.moduleFilename }.let { + require(it.size == 1) {"All test methods should be from one file"} + it.first() + } + val contentFile = File(pythonFilePath) + val pythonFileContent = contentFile.readText() + + val pythonModule = testMethods.map { it.methodName.moduleName }.let { + require(it.size == 1) {"All test methods should be from one module"} + it.first() + } + + val pythonMethods = testMethods.map { + PythonMethodHeader( + it.methodName.name, + it.moduleFilename, + it.containingClassName?.let { objName -> + PythonClassId(objName.moduleName, objName.name) + }) + } + + return JavaApiProcessor( + PythonTestGenerationConfig( + pythonPath, + TestFileInformation(pythonFilePath, pythonFileContent, pythonModule), + directoriesForSysPath, + pythonMethods, + timeout, + timeoutForRun, + testFramework, + pythonRunRoot.toPath(), + true, + { false }, + RuntimeExceptionTestsBehaviour.FAIL + ) + ) + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/external/RequirementsInstaller.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/RequirementsInstaller.kt new file mode 100644 index 0000000000..e060b3e2a3 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/external/RequirementsInstaller.kt @@ -0,0 +1,18 @@ +package org.utbot.python.framework.external + +import org.utbot.python.utils.RequirementsInstaller +import org.utbot.python.utils.RequirementsUtils + +class RequirementsInstaller : RequirementsInstaller { + override fun checkRequirements(pythonPath: String, requirements: List): Boolean { + return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements) + } + + override fun installRequirements(pythonPath: String, requirements: List) { + val result = RequirementsUtils.installRequirements(pythonPath, requirements) + if (result.exitValue != 0) { + System.err.println(result.stderr) + error("Failed to install requirements: ${requirements.joinToString()}.") + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt new file mode 100644 index 0000000000..50ca857d45 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt @@ -0,0 +1,246 @@ +package org.utbot.python.fuzzing + +import mu.KotlinLogging +import org.utbot.fuzzer.FuzzedContext +import org.utbot.fuzzing.Control +import org.utbot.fuzzing.Description +import org.utbot.fuzzing.Feedback +import org.utbot.fuzzing.Fuzzing +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.Statistic +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.utils.Trie +import org.utbot.python.coverage.PyInstruction +import org.utbot.python.engine.ExecutionFeedback +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.provider.BoolValueProvider +import org.utbot.python.fuzzing.provider.BytearrayValueProvider +import org.utbot.python.fuzzing.provider.BytesValueProvider +import org.utbot.python.fuzzing.provider.ComplexValueProvider +import org.utbot.python.fuzzing.provider.ConstantValueProvider +import org.utbot.python.fuzzing.provider.DictValueProvider +import org.utbot.python.fuzzing.provider.FloatValueProvider +import org.utbot.python.fuzzing.provider.NDArrayValueProvider +import org.utbot.python.fuzzing.provider.IntValueProvider +import org.utbot.python.fuzzing.provider.IteratorValueProvider +import org.utbot.python.fuzzing.provider.ListValueProvider +import org.utbot.python.fuzzing.provider.NoneValueProvider +import org.utbot.python.fuzzing.provider.OptionalValueProvider +import org.utbot.python.fuzzing.provider.RePatternValueProvider +import org.utbot.python.fuzzing.provider.ReduceValueProvider +import org.utbot.python.fuzzing.provider.SetValueProvider +import org.utbot.python.fuzzing.provider.StrValueProvider +import org.utbot.python.fuzzing.provider.SubtypeValueProvider +import org.utbot.python.fuzzing.provider.TupleFixSizeValueProvider +import org.utbot.python.fuzzing.provider.TupleValueProvider +import org.utbot.python.fuzzing.provider.TypeAliasValueProvider +import org.utbot.python.fuzzing.provider.UnionValueProvider +import org.utbot.python.fuzzing.provider.utils.isAny +import org.utpython.types.PythonTypeHintsStorage +import org.utpython.types.general.FunctionType +import org.utpython.types.general.UtType +import org.utbot.python.newtyping.inference.InferredTypeFeedback +import org.utbot.python.newtyping.inference.InvalidTypeFeedback +import org.utbot.python.newtyping.inference.SuccessFeedback +import org.utbot.python.newtyping.inference.baseline.BaselineAlgorithm +import org.utpython.types.pythonModuleName +import org.utpython.types.pythonName +import org.utpython.types.pythonTypeName +import org.utpython.types.pythonTypeRepresentation +import org.utbot.python.utils.ExecutionWithTimeoutMode +import org.utbot.python.utils.FakeWithTimeoutMode +import org.utbot.python.utils.TestGenerationLimitManager +import kotlin.random.Random + +private val logger = KotlinLogging.logger {} + +typealias PythonValueProvider = ValueProvider + +data class PythonFuzzedConcreteValue( + val type: UtType, + val value: Any, + val fuzzedContext: FuzzedContext = FuzzedContext.Unknown, +) + +data class FuzzedUtType( + val utType: UtType, + val fuzzAny: Boolean = false, +) { + + fun isAny(): Boolean = utType.isAny() + fun pythonName(): String = utType.pythonName() + fun pythonTypeName(): String = utType.pythonTypeName() + fun pythonModuleName(): String = utType.pythonModuleName() + fun pythonTypeRepresentation(): String = utType.pythonTypeRepresentation() + + companion object { + fun FuzzedUtType.activateAny() = FuzzedUtType(this.utType, true) + fun FuzzedUtType.activateAnyIf(parent: FuzzedUtType) = FuzzedUtType(this.utType, parent.fuzzAny) + fun Collection.activateAny() = this.map { it.activateAny() } + fun Collection.activateAnyIf(parent: FuzzedUtType) = this.map { it.activateAnyIf(parent) } + fun UtType.toFuzzed() = FuzzedUtType(this) + fun Collection.toFuzzed() = this.map { it.toFuzzed() } + } +} + +class PythonMethodDescription( + val name: String, + val concreteValues: Collection = emptyList(), + val pythonTypeStorage: PythonTypeHintsStorage, + val tracer: Trie, + val random: Random, + val limitManager: TestGenerationLimitManager, + val type: FunctionType, +) : Description(type.arguments.toFuzzed()) + +data class PythonExecutionResult( + val executionFeedback: ExecutionFeedback, + val fuzzingPlatformFeedback: PythonFeedback +) + +data class PythonFeedback( + override val control: Control = Control.CONTINUE, + val result: Trie.Node = Trie.emptyNode(), + val typeInferenceFeedback: InferredTypeFeedback = InvalidTypeFeedback, + val fromCache: Boolean = false, +) : Feedback { + fun fromCache(): PythonFeedback { + return PythonFeedback( + control = control, + result = result, + typeInferenceFeedback = typeInferenceFeedback, + fromCache = true, + ) + } +} + +class PythonFuzzedValue( + val tree: PythonTree.PythonTreeNode, + val summary: String? = null, +) + +fun pythonDefaultValueProviders(typeStorage: PythonTypeHintsStorage) = listOf( + NoneValueProvider, + BoolValueProvider, + IntValueProvider, + FloatValueProvider, + ComplexValueProvider, + StrValueProvider, + ListValueProvider, + SetValueProvider, + DictValueProvider, + TupleValueProvider, + TupleFixSizeValueProvider, + OptionalValueProvider, + UnionValueProvider, + BytesValueProvider, + BytearrayValueProvider, + ReduceValueProvider, + RePatternValueProvider, + ConstantValueProvider, + TypeAliasValueProvider, + IteratorValueProvider, + NDArrayValueProvider(typeStorage), + SubtypeValueProvider(typeStorage) +) + +fun pythonAnyTypeValueProviders() = listOf( + NoneValueProvider, + BoolValueProvider, + IntValueProvider, + FloatValueProvider, + ComplexValueProvider, + StrValueProvider, + BytesValueProvider, + BytearrayValueProvider, + ConstantValueProvider, +) + +class PythonFuzzing( + private val pythonTypeStorage: PythonTypeHintsStorage, + private val typeInferenceAlgorithm: BaselineAlgorithm, + private val globalIsCancelled: () -> Boolean, + val execute: suspend (description: PythonMethodDescription, values: List) -> PythonFeedback, +) : Fuzzing { + + private fun generateDefault(providers: List, description: PythonMethodDescription, type: FuzzedUtType)= sequence { + providers.asSequence().forEach { provider -> + if (provider.accept(type)) { + logger.debug { "Provider ${provider.javaClass.simpleName} accepts type ${type.pythonTypeRepresentation()}" } + yieldAll(provider.generate(description, type)) + } + } + } + + private fun generateAnyProviders(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + pythonAnyTypeValueProviders().asSequence().forEach { provider -> + logger.debug { "Provider ${provider.javaClass.simpleName} accepts type ${type.pythonTypeRepresentation()} with activated any" } + yieldAll(provider.generate(description, type)) + } + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType): Sequence> { + val providers = mutableSetOf>() + if (type.isAny()) { + if (type.fuzzAny) { + providers += generateAnyProviders(description, type) + } else { + logger.debug("Any does not have provider") + } + } else { + providers += generateDefault(pythonDefaultValueProviders(pythonTypeStorage), description, type) + } + + return providers.asSequence() + } + + override suspend fun handle(description: PythonMethodDescription, values: List): PythonFeedback { + val result = execute(description, values) + if (result.typeInferenceFeedback is SuccessFeedback && !result.fromCache) { + typeInferenceAlgorithm.laudType(description.type) + } + if (description.limitManager.isCancelled()) { + typeInferenceAlgorithm.feedbackState(description.type, result.typeInferenceFeedback) + } + return result + } + + private suspend fun forkType(description: PythonMethodDescription, stats: Statistic) { + val type: UtType? = typeInferenceAlgorithm.expandState() + if (type != null) { + val d = PythonMethodDescription( + description.name, + description.concreteValues, + description.pythonTypeStorage, + description.tracer, + description.random, + TestGenerationLimitManager(ExecutionWithTimeoutMode, description.limitManager.until), + type as FunctionType + ) + if (!d.limitManager.isCancelled()) { + logger.debug { "Fork new type" } + fork(d, stats) + } + logger.debug { "Fork ended" } + } else { + description.limitManager.mode = FakeWithTimeoutMode + } + } + + override suspend fun isCancelled( + description: PythonMethodDescription, + stats: Statistic + ): Boolean { + if (globalIsCancelled()) { + return true + } + if (description.limitManager.isCancelled() || description.parameters.any { it.isAny() }) { + forkType(description, stats) + if (description.limitManager.isRootManager) { + return FakeWithTimeoutMode.isCancelled(description.limitManager) + } + } + return description.limitManager.isCancelled() + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/BoolValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/BoolValueProvider.kt new file mode 100644 index 0000000000..945272d609 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/BoolValueProvider.kt @@ -0,0 +1,32 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.seeds.Bool +import org.utbot.fuzzing.seeds.KnownValue +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonBoolClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utbot.python.fuzzing.provider.utils.generateSummary + +object BoolValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonBoolClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + yieldBool(Bool.TRUE()) { true } + yieldBool(Bool.FALSE()) { false } + } + + private suspend fun > SequenceScope>.yieldBool(value: T, block: T.() -> Boolean) { + yield(Seed.Known(value) { + PythonFuzzedValue( + PythonTree.fromBool(block(it)), + it.generateSummary(), + ) + }) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/BytearrayValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/BytearrayValueProvider.kt new file mode 100644 index 0000000000..7ecbb00b80 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/BytearrayValueProvider.kt @@ -0,0 +1,43 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonBytearrayClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider + +object BytearrayValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonBytearrayClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + yield(Seed.Recursive( + construct = Routine.Create( + listOf( + description.pythonTypeStorage.pythonInt, + ).toFuzzed() + ) { v -> + val value = v.first().tree as PythonTree.PrimitiveNode + PythonFuzzedValue( + PythonTree.PrimitiveNode( + pythonBytearrayClassId, + "bytearray(${value.repr})" + ), + "%var% = ${type.pythonTypeRepresentation()}", + ) + }, + empty = Routine.Empty { PythonFuzzedValue( + PythonTree.PrimitiveNode( + pythonBytearrayClassId, + "bytearray()" + ), + "%var% = ${type.pythonTypeRepresentation()}" + ) } + )) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/BytesValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/BytesValueProvider.kt new file mode 100644 index 0000000000..ba107fe158 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/BytesValueProvider.kt @@ -0,0 +1,45 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonBytesClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider + +object BytesValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonBytesClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + yield(Seed.Recursive( + construct = Routine.Create( + listOf( + description.pythonTypeStorage.pythonInt, + ).toFuzzed() + ) { v -> + val value = v.first().tree as PythonTree.PrimitiveNode + PythonFuzzedValue( + PythonTree.PrimitiveNode( + pythonBytesClassId, + "bytes(${value.repr})" + ), + "%var% = ${type.pythonTypeRepresentation()}", + ) + }, + empty = Routine.Empty { + PythonFuzzedValue( + PythonTree.PrimitiveNode( + pythonBytesClassId, + "bytes()" + ), + "%var% = ${type.pythonTypeRepresentation()}" + ) + } + )) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ComplexValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ComplexValueProvider.kt new file mode 100644 index 0000000000..4ccc991f5c --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ComplexValueProvider.kt @@ -0,0 +1,59 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonComplexClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.createPythonUnionType + +object ComplexValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonComplexClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val numberType = createPythonUnionType( + listOf( + description.pythonTypeStorage.pythonFloat, + description.pythonTypeStorage.pythonInt + ) + ) + val emptyValue = + PythonFuzzedValue( + PythonTree.PrimitiveNode( + pythonComplexClassId, + "complex()" + ), + "%var% = ${type.pythonTypeRepresentation()}" + ) + yield(Seed.Recursive( + construct = Routine.Create( + listOf( + numberType, + numberType + ).toFuzzed() + ) { v -> + if (v[0].tree is PythonTree.FakeNode || v[1].tree is PythonTree.FakeNode) { + emptyValue + } else { + val real = v[0].tree as PythonTree.PrimitiveNode + val imag = v[1].tree as PythonTree.PrimitiveNode + val repr = "complex(real=${real.repr}, imag=${imag.repr})" + PythonFuzzedValue( + PythonTree.PrimitiveNode( + pythonComplexClassId, + repr + ), + "%var% = $repr" + ) + } + }, + empty = Routine.Empty { emptyValue } + )) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ConstantValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ConstantValueProvider.kt new file mode 100644 index 0000000000..9201a17d94 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ConstantValueProvider.kt @@ -0,0 +1,41 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utbot.python.fuzzing.value.TypesFromJSONStorage + +object ConstantValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return TypesFromJSONStorage.getTypesFromJsonStorage().containsKey(type.pythonTypeName()) + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType): Sequence> = + sequence { + val storage = TypesFromJSONStorage.getTypesFromJsonStorage() + storage.values.forEach { values -> + val constants = if (values.name == type.pythonTypeName()) { + values.instances + } else { + emptyList() + } + constants.forEach { + yield( + Seed.Simple( + PythonFuzzedValue( + PythonTree.PrimitiveNode( + PythonClassId(values.name), + it + ), + "%var% = $it" + ) + ) + ) + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/DictValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/DictValueProvider.kt new file mode 100644 index 0000000000..fe7f9ad72a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/DictValueProvider.kt @@ -0,0 +1,42 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonDictClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.pythonAnnotationParameters + +object DictValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonDictClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val params = type.utType.pythonAnnotationParameters() + + yield(Seed.Collection( + construct = Routine.Collection { _ -> + PythonFuzzedValue( + PythonTree.DictNode(mutableMapOf()), + "%var% = ${type.pythonTypeRepresentation()}" + ) + }, + modify = Routine.ForEach(params.toFuzzed().activateAnyIf(type)) { instance, _, arguments -> + val key = arguments[0].tree + val value = arguments[1].tree + val dict = instance.tree as PythonTree.DictNode + if (dict.items.keys.toList().contains(key)) { + dict.items.replace(key, value) + } else { + dict.items[key] = value + } + }, + )) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/FloatValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/FloatValueProvider.kt new file mode 100644 index 0000000000..a8f0dee725 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/FloatValueProvider.kt @@ -0,0 +1,58 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.seeds.IEEE754Value +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonFloatClassId +import org.utbot.python.framework.api.python.util.pythonIntClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedConcreteValue +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utbot.python.fuzzing.provider.utils.generateSummary +import org.utpython.types.pythonTypeName +import java.math.BigDecimal +import java.math.BigInteger + +object FloatValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonFloatClassId.canonicalName + } + + private fun getFloatConstants(concreteValues: Collection): List { + return concreteValues + .filter { accept(it.type.toFuzzed()) } + .map { fuzzedValue -> + (fuzzedValue.value as BigDecimal).let { + IEEE754Value.fromValue(it.toDouble()) + } + } + } + + private fun getIntConstants(concreteValues: Collection): List { + return concreteValues + .filter { it.type.pythonTypeName() == pythonIntClassId.canonicalName } + .map { fuzzedValue -> + (fuzzedValue.value as BigInteger).let { + IEEE754Value.fromValue(it.toDouble()) + } + } + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType): Sequence> = sequence { + val floatConstants = getFloatConstants(description.concreteValues) + val intConstants = getIntConstants(description.concreteValues) + val constants = floatConstants + intConstants + listOf(0, 1).map { IEEE754Value.fromValue(it.toDouble()) } + + constants.asSequence().forEach { value -> + yield(Seed.Known(value) { + PythonFuzzedValue( + PythonTree.fromFloat(it.toDouble()), + it.generateSummary() + ) + }) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/IntValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/IntValueProvider.kt new file mode 100644 index 0000000000..35c7371817 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/IntValueProvider.kt @@ -0,0 +1,73 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Configuration +import org.utbot.fuzzing.Mutation +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.KnownValue +import org.utbot.fuzzing.seeds.Signed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonIntClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedConcreteValue +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utbot.python.fuzzing.provider.utils.generateSummary +import java.math.BigInteger +import kotlin.random.Random +import org.utpython.types.pythonTypeName + +object IntValueProvider : PythonValueProvider { + private val randomStubWithNoUsage = Random(0) + private val configurationStubWithNoUsage = Configuration() + + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonIntClassId.canonicalName + } + + private fun BitVectorValue.change(func: BitVectorValue.() -> Unit): BitVectorValue { + return Mutation> { _, _, _ -> + BitVectorValue(this).apply { func() } + }.mutate(this, randomStubWithNoUsage, configurationStubWithNoUsage) as BitVectorValue + } + + private fun getIntConstants(concreteValues: Collection): List { + return concreteValues + .filter { accept(it.type.toFuzzed()) } + .map { fuzzedValue -> + (fuzzedValue.value as BigInteger).let { + BitVectorValue.fromBigInteger(it) + } + } + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence> { + val isNDArray: Boolean = description.type.arguments.any { it.pythonTypeName() == "numpy.ndarray" } + val bits = if (isNDArray) { + 32 + } else { + 128 + } + val integerConstants = getIntConstants(description.concreteValues) + val modifiedConstants = integerConstants.flatMap { value -> + listOf( + value, + value.change { inc() }, + value.change { dec() } + ) + }.toSet() + + val constants = modifiedConstants + Signed.values().map { it.invoke(bits) } + + constants.asSequence().forEach { vector -> + yield(Seed.Known(vector) { + PythonFuzzedValue( + PythonTree.fromInt(it.toBigInteger()), + it.generateSummary() + ) + }) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/IteratorValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/IteratorValueProvider.kt new file mode 100644 index 0000000000..a60075f3e4 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/IteratorValueProvider.kt @@ -0,0 +1,37 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonIteratorClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.pythonAnnotationParameters + +object IteratorValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonIteratorClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val param = type.utType.pythonAnnotationParameters() + yield( + Seed.Collection( + construct = Routine.Collection { + PythonFuzzedValue( + PythonTree.IteratorNode( + emptyMap().toMutableMap(), + ), + "%var% = ${type.pythonTypeRepresentation()}" + ) + }, + modify = Routine.ForEach(param.toFuzzed().activateAnyIf(type)) { self, i, values -> + (self.tree as PythonTree.IteratorNode).items[i] = values.first().tree + } + )) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ListValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ListValueProvider.kt new file mode 100644 index 0000000000..676926c03a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ListValueProvider.kt @@ -0,0 +1,37 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonListClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.pythonAnnotationParameters + +object ListValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonListClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val param = type.utType.pythonAnnotationParameters() + yield( + Seed.Collection( + construct = Routine.Collection { + PythonFuzzedValue( + PythonTree.ListNode( + emptyMap().toMutableMap(), + ), + "%var% = ${type.pythonTypeRepresentation()}" + ) + }, + modify = Routine.ForEach(param.toFuzzed().activateAnyIf(type)) { self, i, values -> + (self.tree as PythonTree.ListNode).items[i] = values.first().tree + } + )) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/NDArrayValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/NDArrayValueProvider.kt new file mode 100644 index 0000000000..6c6e8c5fb2 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/NDArrayValueProvider.kt @@ -0,0 +1,73 @@ +package org.utbot.python.fuzzing.provider + +import mu.KotlinLogging +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonNdarrayClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.* +import kotlin.math.abs + + +private val logger = KotlinLogging.logger {} +private const val SHAPE_SIZE = 3 +private const val MAX_SHAPE_DIGITS = 2 + +class NDArrayValueProvider( + private val typeStorage: PythonTypeHintsStorage +) : PythonValueProvider { + + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonNdarrayClassId.canonicalName + } + + @Suppress("UNCHECKED_CAST") + override fun generate( + description: PythonMethodDescription, type: FuzzedUtType + ) = sequence { + val param = type.utType.pythonAnnotationParameters() + val intType = typeStorage.pythonInt + val listType = typeStorage.pythonList + + yield(Seed.Recursive( + construct = Routine.Create( + listOf( + listType + .pythonDescription() + .createTypeWithNewAnnotationParameters(listType, listOf(intType)).toFuzzed() + ) + ) { + PythonFuzzedValue( + PythonTree.NDArrayNode( + emptyMap().toMutableMap(), + ((it.first().tree as PythonTree.ListNode).items as Map).values.map { node -> + abs(node.repr.take(MAX_SHAPE_DIGITS).toInt()) % 10 + }.take(SHAPE_SIZE).let { self -> + if (self.fold(1, Int::times) == 0){ + listOf(0) + } else { + self + } + } // TODO: Rethink logic + ), "%var% = ${type.pythonTypeRepresentation()}" + ) + }, + modify = sequence { + yield(Routine.Call((0 until 1000000).map { param[1] }.toFuzzed()) { instance, arguments -> + val obj = instance.tree as PythonTree.NDArrayNode + (0 until obj.dimensions.fold(1, Int::times)).map { + obj.items[it] = arguments[it].tree + } + }) + }, + empty = Routine.Empty { PythonFuzzedValue(PythonTree.FakeNode) } + ) + + ) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/NoneValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/NoneValueProvider.kt new file mode 100644 index 0000000000..119c30db13 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/NoneValueProvider.kt @@ -0,0 +1,19 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.PythonNoneTypeDescription + +object NoneValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.utType.meta is PythonNoneTypeDescription + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType): Sequence> = sequence { + yield(Seed.Simple(PythonFuzzedValue(PythonTree.fromNone(), "%var% = None"))) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/OptionalValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/OptionalValueProvider.kt new file mode 100644 index 0000000000..8356958121 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/OptionalValueProvider.kt @@ -0,0 +1,30 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.PythonNoneTypeDescription +import org.utpython.types.PythonUnionTypeDescription +import org.utpython.types.pythonAnnotationParameters + +object OptionalValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.utType.meta is PythonUnionTypeDescription && type.utType.parameters.any { it.meta is PythonNoneTypeDescription } + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val params = type.utType.pythonAnnotationParameters() + params.forEach { unionParam -> + yield(Seed.Recursive( + construct = Routine.Create(listOf(unionParam).toFuzzed().activateAnyIf(type)) { v -> v.first() }, + empty = Routine.Empty { PythonFuzzedValue(PythonTree.fromNone()) } + )) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/RePatternValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/RePatternValueProvider.kt new file mode 100644 index 0000000000..aaf2075391 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/RePatternValueProvider.kt @@ -0,0 +1,50 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonRePatternClassId +import org.utbot.python.framework.api.python.util.toPythonRepr +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utbot.python.fuzzing.provider.utils.makeRawString + +object RePatternValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonRePatternClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + yield(Seed.Recursive( + construct = Routine.Create( + listOf( + description.pythonTypeStorage.pythonStr, + ).toFuzzed() + ) { v -> + val value = v.first().tree as PythonTree.PrimitiveNode + val rawValue = value.repr.toPythonRepr().makeRawString() + PythonFuzzedValue( + PythonTree.ReduceNode( + pythonRePatternClassId, + PythonClassId("re.compile"), + listOf(PythonTree.fromString(rawValue)) + ), + "%var% = re.compile(${rawValue})" + ) + }, + empty = Routine.Empty { + PythonFuzzedValue( + PythonTree.PrimitiveNode( + pythonRePatternClassId, + "re.compile('')" + ), + "%var% = re.compile('')" + ) + } + )) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ReduceValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ReduceValueProvider.kt new file mode 100644 index 0000000000..ee5e25899f --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ReduceValueProvider.kt @@ -0,0 +1,238 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonBoolClassId +import org.utbot.python.framework.api.python.util.pythonNdarrayClassId +import org.utbot.python.framework.api.python.util.pythonBytearrayClassId +import org.utbot.python.framework.api.python.util.pythonBytesClassId +import org.utbot.python.framework.api.python.util.pythonComplexClassId +import org.utbot.python.framework.api.python.util.pythonDictClassId +import org.utbot.python.framework.api.python.util.pythonFloatClassId +import org.utbot.python.framework.api.python.util.pythonIntClassId +import org.utbot.python.framework.api.python.util.pythonListClassId +import org.utbot.python.framework.api.python.util.pythonObjectClassId +import org.utbot.python.framework.api.python.util.pythonRePatternClassId +import org.utbot.python.framework.api.python.util.pythonSetClassId +import org.utbot.python.framework.api.python.util.pythonStrClassId +import org.utbot.python.framework.api.python.util.pythonTupleClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAny +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utbot.python.fuzzing.provider.utils.isAny +import org.utbot.python.fuzzing.provider.utils.isCallable +import org.utbot.python.fuzzing.provider.utils.isConcreteType +import org.utbot.python.fuzzing.provider.utils.isMagic +import org.utbot.python.fuzzing.provider.utils.isPrivate +import org.utbot.python.fuzzing.provider.utils.isProperty +import org.utbot.python.fuzzing.provider.utils.isProtected +import org.utpython.types.PythonCallableTypeDescription +import org.utpython.types.PythonCompositeTypeDescription +import org.utpython.types.PythonDefinition +import org.utpython.types.general.FunctionType +import org.utpython.types.getPythonAttributeByName +import org.utpython.types.getPythonAttributes +import org.utpython.types.pythonDescription +import org.utpython.types.pythonNoneType +import org.utpython.types.pythonTypeName + +object ReduceValueProvider : PythonValueProvider { + private val unsupportedTypes = listOf( + pythonRePatternClassId.canonicalName, + pythonListClassId.canonicalName, + pythonSetClassId.canonicalName, + pythonTupleClassId.canonicalName, + pythonDictClassId.canonicalName, + pythonBytesClassId.canonicalName, + pythonBytearrayClassId.canonicalName, + pythonComplexClassId.canonicalName, + pythonIntClassId.canonicalName, + pythonFloatClassId.canonicalName, + pythonStrClassId.canonicalName, + pythonBoolClassId.canonicalName, + pythonObjectClassId.canonicalName, + pythonNdarrayClassId.canonicalName + + ) + + override fun accept(type: FuzzedUtType): Boolean { + val hasSupportedType = + !unsupportedTypes.contains(type.pythonTypeName()) + return hasSupportedType && isConcreteType(type.utType) && !type.isAny() + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val fields = findFields(description, type) + val modifications = emptyList>().toMutableList() + modifications.addAll(fields.map { field -> + Routine.Call(listOf(field.type).toFuzzed().activateAny()) { instance, arguments -> + val obj = instance.tree as PythonTree.ReduceNode + obj.state[field.meta.name] = arguments.first().tree + } + }) + + val (constructors, newType) = findConstructors(description, type) + constructors + .forEach { + yieldAll(callConstructors(newType, it, modifications.asSequence(), description)) + } + } + + private fun findFields(description: PythonMethodDescription, type: FuzzedUtType): List { + // TODO: here we need to use same as .getPythonAttributeByName but without name + // TODO: now we do not have fields from parents + // TODO: here we should use only attributes from __slots__ + return type.utType.getPythonAttributes().filter { attr -> + !attr.isMagic() && !attr.isProtected() && !attr.isPrivate() && !attr.isProperty() && !attr.isCallable( + description.pythonTypeStorage + ) + } + } + + /* + * 1. Annotated __init__ without functional arguments and mro(__init__) <= mro(__new__) -> listOf(__init__) + * 2. Not 1 and annotated __new__ without functional arguments -> listOf(__new__) + * 3. Not 1 and not 2 and __init__ without tp.Any -> listOf(__init__) + * 4. Not 1 and not 2 and not 3 and type is not generic -> listOf(__init__, __new__) + activateAny + * 5. emptyList() + */ + private fun findConstructors(description: PythonMethodDescription, type: FuzzedUtType): Pair, FuzzedUtType> { + val initMethodName = "__init__" + val newMethodName = "__new__" + val typeDescr = type.utType.pythonDescription() + return if (typeDescr is PythonCompositeTypeDescription) { + val mro = typeDescr.mro(description.pythonTypeStorage, type.utType) + val initParent = mro.indexOfFirst { p -> p.getPythonAttributes().any { it.meta.name == initMethodName } } + val newParent = mro.indexOfFirst { p -> p.getPythonAttributes().any { it.meta.name == newMethodName } } + val initMethod = type.utType.getPythonAttributeByName(description.pythonTypeStorage, initMethodName) + val newMethod = type.utType.getPythonAttributeByName(description.pythonTypeStorage, newMethodName) + + val initWithoutCallable = initMethod?.isCallable(description.pythonTypeStorage) ?: false + val newWithoutCallable = newMethod?.isCallable(description.pythonTypeStorage) ?: false + + val initWithoutAny = (initMethod?.type as? FunctionType)?.arguments?.drop(1)?.all { !it.isAny() } ?: false + val newWithoutAny = (initMethod?.type as? FunctionType)?.arguments?.drop(1)?.all { !it.isAny() } ?: false + + if (initParent <= newParent && initMethod != null && initWithoutCallable && initWithoutAny) { + listOf(initMethod) to type + } else if (newMethod != null && newWithoutCallable && newWithoutAny) { + listOf(newMethod) to type + } else if (initMethod != null && initWithoutAny) { + listOf(initMethod) to type + } else { + listOfNotNull(initMethod, newMethod) to type.activateAny() + } + } else { + emptyList() to type + } + } + + private fun callConstructors( + type: FuzzedUtType, + constructor: PythonDefinition, + modifications: Sequence>, + description: PythonMethodDescription, + ): Sequence> = sequence { + val constructors = emptyList>().toMutableList() + if (constructor.type.pythonTypeName() == "Overload") { + constructor.type.parameters.forEach { + if (it is FunctionType) { + constructors.add(it to constructor.meta.name) + } + } + } else { + constructors.add(constructor.type as FunctionType to constructor.meta.name) + } + constructors.forEach { + yield(constructObject(type, it, modifications, description)) + } + } + + private fun constructObject( + type: FuzzedUtType, + constructorFunction: Pair, + modifications: Sequence>, + description: PythonMethodDescription, + ): Seed.Recursive { + return Seed.Recursive( + construct = buildConstructor(type, constructorFunction), + modify = modifications, + empty = Routine.Empty { PythonFuzzedValue(buildEmptyValue(type, description)) } + ) + } + + private fun buildEmptyValue( + type: FuzzedUtType, + description: PythonMethodDescription, + ): PythonTree.PythonTreeNode { + val newMethodName = "__new__" + val newMethod = type.utType.getPythonAttributeByName(description.pythonTypeStorage, newMethodName) + return if (newMethod?.type?.parameters?.size == 1) { + val classId = PythonClassId(type.pythonModuleName(), type.pythonName()) + PythonTree.ReduceNode( + classId, + PythonClassId(type.pythonModuleName(), "${type.pythonName()}.__new__"), + listOf(PythonTree.PrimitiveNode(classId, classId.name)), + ) + } else { + PythonTree.FakeNode + } + } + + private fun buildConstructor( + type: FuzzedUtType, + constructor: Pair, + ): Routine.Create { + val (constructorFunction, constructorName) = constructor + val newMethodName = "__new__" + if (constructorName == newMethodName) { + val newMethodArgs = constructorFunction.arguments + return if (newMethodArgs.size == 1) { + val classId = PythonClassId(type.pythonModuleName(), type.pythonName()) + Routine.Create(listOf(pythonNoneType).toFuzzed()) { _ -> + PythonFuzzedValue( + PythonTree.ReduceNode( + classId, + PythonClassId(type.pythonModuleName(), "${type.pythonName()}.__new__"), + listOf(PythonTree.PrimitiveNode(classId, classId.name)), + ), + "%var% = ${type.pythonTypeRepresentation()}" + ) + } + } else { + val classId = PythonClassId(type.pythonModuleName(), type.pythonName()) + Routine.Create(listOf(pythonNoneType).toFuzzed()) { _ -> + PythonFuzzedValue( + PythonTree.ReduceNode( + classId, + PythonClassId(pythonObjectClassId.moduleName, "object.__new__"), + listOf(PythonTree.PrimitiveNode(classId, classId.name)), + ), + "%var% = ${type.pythonTypeRepresentation()}" + ) + } + } + } else { + val typeDescription = constructorFunction.pythonDescription() as PythonCallableTypeDescription + val positionalArgs = typeDescription.argumentKinds.count { it == PythonCallableTypeDescription.ArgKind.ARG_POS } + val arguments = constructorFunction.arguments.take(positionalArgs) + val nonSelfArgs = arguments.drop(1) + return Routine.Create(nonSelfArgs.toFuzzed().activateAnyIf(type)) { v -> + PythonFuzzedValue( + PythonTree.ReduceNode( + PythonClassId(type.pythonModuleName(), type.pythonName()), + PythonClassId(type.pythonModuleName(), type.pythonName()), + v.map { it.tree }, + ), + "%var% = ${type.pythonTypeRepresentation()}" + ) + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/SetValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/SetValueProvider.kt new file mode 100644 index 0000000000..20def0c7d3 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/SetValueProvider.kt @@ -0,0 +1,37 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonSetClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.pythonAnnotationParameters + +object SetValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonSetClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val params = type.utType.pythonAnnotationParameters() + + yield(Seed.Collection( + construct = Routine.Collection { _ -> + PythonFuzzedValue( + PythonTree.SetNode(mutableSetOf()), + "%var% = ${type.pythonTypeRepresentation()}" + ) + }, + modify = Routine.ForEach(params.toFuzzed().activateAnyIf(type)) { instance, _, arguments -> + val item = arguments[0].tree + val set = instance.tree as PythonTree.SetNode + set.items.add(item) + }, + )) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/StrValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/StrValueProvider.kt new file mode 100644 index 0000000000..7e05b51737 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/StrValueProvider.kt @@ -0,0 +1,66 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.seeds.KnownValue +import org.utbot.fuzzing.seeds.RegexValue +import org.utbot.fuzzing.seeds.StringValue +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonStrClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedConcreteValue +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utbot.python.fuzzing.provider.utils.generateSummary +import org.utbot.python.fuzzing.provider.utils.isPattern +import org.utbot.python.fuzzing.provider.utils.transformQuotationMarks +import org.utbot.python.fuzzing.provider.utils.transformRawString + +object StrValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonStrClassId.canonicalName + } + + private fun getConstants(concreteValues: Collection): List { + return concreteValues + .filter { accept(it.type.toFuzzed()) } + .map { it.value as String } + } + + private fun getStrConstants(concreteValues: Collection): List { + return getConstants(concreteValues) + .filterNot { it.isPattern() } + .map { it.transformQuotationMarks() } + } + + private fun getRegexConstants(concreteValues: Collection): List { + return getConstants(concreteValues) + .filter { it.isPattern() } + .map { it.transformRawString().transformQuotationMarks() } + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val strConstants = getStrConstants(description.concreteValues) + listOf( + "pythön", + "foo", + "", + ) + strConstants.forEach { yieldStrings(StringValue(it)) { value } } + + val regexConstants = getRegexConstants(description.concreteValues) + regexConstants.forEach { + val maxLength = listOf(16, 256, 2048).random(description.random).coerceAtLeast(it.length) + yieldStrings(RegexValue(it, description.random, maxLength), StringValue::value) + } + } + + private suspend fun > SequenceScope>.yieldStrings(value: T, block: T.() -> Any) { + yield(Seed.Known(value) { + PythonFuzzedValue( + PythonTree.fromString(block(it).toString()), + it.generateSummary(), + ) + }) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/SubtypeValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/SubtypeValueProvider.kt new file mode 100644 index 0000000000..41d4fe14fa --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/SubtypeValueProvider.kt @@ -0,0 +1,45 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utbot.python.fuzzing.provider.utils.isConcreteType +import org.utpython.types.PythonConcreteCompositeTypeDescription +import org.utpython.types.PythonProtocolDescription +import org.utpython.types.PythonSubtypeChecker.Companion.checkIfRightIsSubtypeOfLeft +import org.utpython.types.PythonTypeHintsStorage +import org.utpython.types.general.DefaultSubstitutionProvider +import org.utpython.types.pythonAnyType +import org.utpython.types.pythonDescription + +class SubtypeValueProvider( + private val typeStorage: PythonTypeHintsStorage +) : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.utType.meta is PythonProtocolDescription || + ((type.utType.meta as? PythonConcreteCompositeTypeDescription)?.isAbstract == true) + } + + private val concreteTypes = typeStorage.simpleTypes.filter { + isConcreteType(it) && it.pythonDescription().name.name.first() != '_' // Don't substitute private classes + }.map { + DefaultSubstitutionProvider.substituteAll(it, it.parameters.map { pythonAnyType }) + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val subtypes = concreteTypes.filter { checkIfRightIsSubtypeOfLeft(type.utType, it, typeStorage) } + subtypes.forEach { subtype -> + yield( + Seed.Recursive( + construct = Routine.Create(listOf(subtype).toFuzzed().activateAnyIf(type)) { v -> v.first() }, + empty = Routine.Empty { PythonFuzzedValue(PythonTree.FakeNode) } + )) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/TupleFixSizeValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/TupleFixSizeValueProvider.kt new file mode 100644 index 0000000000..813a63b0a6 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/TupleFixSizeValueProvider.kt @@ -0,0 +1,40 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.PythonTupleTypeDescription +import org.utpython.types.pythonAnnotationParameters + +object TupleFixSizeValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.utType.meta is PythonTupleTypeDescription + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val params = type.utType.pythonAnnotationParameters() + val length = params.size + val modifications = emptyList>().toMutableList() + for (i in 0 until length) { + modifications.add(Routine.Call(listOf(params[i]).toFuzzed().activateAnyIf(type)) { instance, arguments -> + (instance.tree as PythonTree.TupleNode).items[i] = arguments.first().tree + }) + } + yield(Seed.Recursive( + construct = Routine.Create(params.toFuzzed().activateAnyIf(type)) { v -> + PythonFuzzedValue( + PythonTree.TupleNode(v.withIndex().associate { it.index to it.value.tree }.toMutableMap()), + "%var% = ${type.pythonTypeRepresentation()}" + ) + }, + modify = modifications.asSequence(), + empty = Routine.Empty { PythonFuzzedValue(PythonTree.FakeNode) } + )) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/TupleValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/TupleValueProvider.kt new file mode 100644 index 0000000000..b15466b873 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/TupleValueProvider.kt @@ -0,0 +1,57 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonTupleClassId +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.PythonSubtypeChecker +import org.utpython.types.pythonAnnotationParameters +import org.utpython.types.pythonAnyType +import org.utpython.types.typesAreEqual + +object TupleValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.pythonTypeName() == pythonTupleClassId.canonicalName + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + yieldAll(getConstants(description, type)) + val param = type.utType.pythonAnnotationParameters() + yield( + Seed.Collection( + construct = Routine.Collection { + PythonFuzzedValue( + PythonTree.TupleNode( + emptyMap().toMutableMap(), + ), + "%var% = ${type.pythonTypeRepresentation()}" + ) + }, + modify = Routine.ForEach(param.toFuzzed().activateAnyIf(type)) { self, i, values -> + (self.tree as PythonTree.TupleNode).items[i] = values.first().tree + } + )) + } + + private fun getConstants(description: PythonMethodDescription, type: FuzzedUtType): List> { + if (!typesAreEqual(type.utType.parameters.first(), pythonAnyType)) + return getSuitableConstantsFromCode(description, type) + return emptyList() + } + private fun getSuitableConstantsFromCode(description: PythonMethodDescription, type: FuzzedUtType): List> { + return description.concreteValues.filter { + PythonSubtypeChecker.checkIfRightIsSubtypeOfLeft(type.utType, it.type, description.pythonTypeStorage) + }.mapNotNull { value -> + PythonTree.fromParsedConstant(Pair(value.type, value.value))?.let { + Seed.Simple(PythonFuzzedValue(it)) + } + } + } + +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/TypeAliasValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/TypeAliasValueProvider.kt new file mode 100644 index 0000000000..a4f640c465 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/TypeAliasValueProvider.kt @@ -0,0 +1,31 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.PythonTypeAliasDescription + +object TypeAliasValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.utType.meta is PythonTypeAliasDescription + } + + override fun generate( + description: PythonMethodDescription, + type: FuzzedUtType + ): Sequence> { + val compositeType = PythonTypeAliasDescription.castToCompatibleTypeApi(type.utType) + return sequenceOf( + Seed.Recursive( + construct = Routine.Create(listOf(compositeType.members[0]).toFuzzed().activateAnyIf(type)) { v -> v.first() }, + empty = Routine.Empty { PythonFuzzedValue(PythonTree.FakeNode) } + ) + ) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/UnionValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/UnionValueProvider.kt new file mode 100644 index 0000000000..f7ac3bef77 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/UnionValueProvider.kt @@ -0,0 +1,30 @@ +package org.utbot.python.fuzzing.provider + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.fuzzing.FuzzedUtType +import org.utbot.python.fuzzing.FuzzedUtType.Companion.activateAnyIf +import org.utbot.python.fuzzing.FuzzedUtType.Companion.toFuzzed +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utbot.python.fuzzing.PythonValueProvider +import org.utpython.types.PythonNoneTypeDescription +import org.utpython.types.PythonUnionTypeDescription +import org.utpython.types.pythonAnnotationParameters + +object UnionValueProvider : PythonValueProvider { + override fun accept(type: FuzzedUtType): Boolean { + return type.utType.meta is PythonUnionTypeDescription && type.utType.parameters.all { it.meta !is PythonNoneTypeDescription } + } + + override fun generate(description: PythonMethodDescription, type: FuzzedUtType) = sequence { + val params = type.utType.pythonAnnotationParameters() + params.forEach { unionParam -> + yield(Seed.Recursive( + construct = Routine.Create(listOf(unionParam).toFuzzed().activateAnyIf(type)) { v -> v.first() }, + empty = Routine.Empty { PythonFuzzedValue(PythonTree.FakeNode) } + )) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/utils/ProviderUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/utils/ProviderUtils.kt new file mode 100644 index 0000000000..c54917c298 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/utils/ProviderUtils.kt @@ -0,0 +1,48 @@ +package org.utbot.python.fuzzing.provider.utils + +import org.utbot.fuzzing.Seed +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.fuzzing.PythonFuzzedValue +import org.utbot.python.fuzzing.PythonMethodDescription +import org.utpython.types.* +import org.utpython.types.general.UtType + +fun UtType.isAny(): Boolean { + return meta is PythonAnyTypeDescription +} + +fun getSuitableConstantsFromCode(description: PythonMethodDescription, type: UtType): List> { + return description.concreteValues.filter { + PythonSubtypeChecker.checkIfRightIsSubtypeOfLeft(type, it.type, description.pythonTypeStorage) + }.mapNotNull { value -> + PythonTree.fromParsedConstant(Pair(value.type, value.value))?.let { + Seed.Simple(PythonFuzzedValue(it)) + } + } +} + +fun isConcreteType(type: UtType): Boolean { + return (type.meta as? PythonConcreteCompositeTypeDescription)?.isAbstract == false +} + +fun PythonDefinition.isProtected(): Boolean { + val name = this.meta.name + return name.startsWith("_") && !this.isMagic() +} + +fun PythonDefinition.isPrivate(): Boolean { + val name = this.meta.name + return name.startsWith("__") && !this.isMagic() +} + +fun PythonDefinition.isMagic(): Boolean { + return this.meta.name.startsWith("__") && this.meta.name.endsWith("__") && this.meta.name.length >= 4 +} + +fun PythonDefinition.isProperty(): Boolean { + return (this.meta as? PythonVariableDescription)?.isProperty == true +} + +fun PythonDefinition.isCallable(typeStorage: PythonTypeHintsStorage): Boolean { + return this.type.getPythonAttributeByName(typeStorage, "__call__") != null +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/utils/StringUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/utils/StringUtils.kt new file mode 100644 index 0000000000..2d413a1ceb --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/utils/StringUtils.kt @@ -0,0 +1,51 @@ +package org.utbot.python.fuzzing.provider.utils + +import org.utbot.fuzzing.seeds.isSupportedPattern + +fun String.transformQuotationMarks(): String { + + val doubleQuotationMarks = this.startsWith("\"") && this.endsWith("\"") +// val oneQuotationMarks = this.startsWith("'") && this.endsWith("'") + + val tripleDoubleQuotationMarks = this.startsWith("\"\"\"") && this.endsWith("\"\"\"") + val tripleOneQuotationMarks = this.startsWith("'''") && this.endsWith("'''") + + if (tripleOneQuotationMarks || tripleDoubleQuotationMarks) { + return this.drop(3).dropLast(3) + } + + if (doubleQuotationMarks) { + return this.drop(1).dropLast(1) + } + + return this +} + +fun String.transformRawString(): String { + return if (this.isRawString()) { + this.substring(2, this.length-1) + } else { + this + } +} + +fun String.makeRawString(): String { + return if (this.isRawString()) { + this + } else { + "r${this}" + } +} + +fun String.isRawString(): Boolean { + val rawStringWithDoubleQuotationMarks = this.startsWith("r\"") && this.endsWith("\"") + val rawStringWithOneQuotationMarks = this.startsWith("r'") && this.endsWith("'") + return rawStringWithOneQuotationMarks || rawStringWithDoubleQuotationMarks +} + +fun String.isPattern(): Boolean { + return if (this.isRawString()) { + val stringContent = this.transformRawString() + return stringContent.isSupportedPattern() + } else false +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/utils/SummaryGeneration.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/utils/SummaryGeneration.kt new file mode 100644 index 0000000000..80475457e8 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/utils/SummaryGeneration.kt @@ -0,0 +1,53 @@ +package org.utbot.python.fuzzing.provider.utils + +import org.utbot.fuzzing.seeds.BitVectorValue +import org.utbot.fuzzing.seeds.DefaultFloatBound +import org.utbot.fuzzing.seeds.IEEE754Value +import org.utbot.fuzzing.seeds.KnownValue +import org.utbot.fuzzing.seeds.Signed +import org.utbot.fuzzing.seeds.StringValue + +fun > T.valueToString(): String { + when (this) { + is BitVectorValue -> { + for (defaultBound in Signed.values()) { + if (defaultBound.test(this)) { + return defaultBound.name.lowercase() + } + } + return when (size) { + 1 -> get(0).toString().uppercase() + else -> toString(10) + } + } + is IEEE754Value -> { + for (defaultBound in DefaultFloatBound.values()) { + if (defaultBound.test(this)) { + return defaultBound.name.lowercase().replace("_", " ") + } + } + return when { + is32Float() -> toFloat().toString() + is64Float() -> toDouble().toString() + else -> toString() + } + } + is StringValue -> { + if (value.contains("\"\"\"")) { + val newValue = value.replace("\"", "\\\"") + return "'$newValue'" + } + return "'$value'" + } + else -> return toString() + } +} + +fun > T.generateSummary(): String { + return buildString { + append("%var% = ${valueToString()}") + if (mutatedFrom != null) { + append(" (mutated from ${mutatedFrom?.valueToString()})") + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/value/PreprocessedValueStorage.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/value/PreprocessedValueStorage.kt new file mode 100644 index 0000000000..25f5a87e03 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/value/PreprocessedValueStorage.kt @@ -0,0 +1,37 @@ +package org.utbot.python.fuzzing.value + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +data class PreprocessedValueFromJSON( + val name: String, + val instances: List +) + +object TypesFromJSONStorage { + private val preprocessedTypes: List + + init { + val typesAsString = TypesFromJSONStorage::class.java.getResource("/preprocessed_values.json") + ?.readText(Charsets.UTF_8) + ?: error("Didn't find preprocessed_values.json") + val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() + val typesAdapter: JsonAdapter> = moshi.adapter(Types.newParameterizedType(List::class.java, PreprocessedValueFromJSON::class.java)) + preprocessedTypes = typesAdapter.fromJson(typesAsString) ?: emptyList() + } + + private val typeNameMap: Map by lazy { + val result = mutableMapOf() + preprocessedTypes.forEach { type -> + result[type.name] = type + } + result + } + + fun getTypesFromJsonStorage(): Map { + return typeNameMap + } +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/ArgBinding.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/ArgBinding.kt new file mode 100644 index 0000000000..fe43034ef3 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/ArgBinding.kt @@ -0,0 +1,18 @@ +package org.utbot.python.newtyping.ast + +import org.utpython.types.PythonSubtypeChecker +import org.utpython.types.PythonTypeHintsStorage +import org.utpython.types.general.FunctionType + +// TODO: consider different types of parameters +fun signaturesAreCompatible( + functionSignature: FunctionType, + callSignature: FunctionType, + storage: PythonTypeHintsStorage +): Boolean { + if (functionSignature.arguments.size != callSignature.arguments.size) + return false + return (functionSignature.arguments zip callSignature.arguments).all { (funcArg, callArg) -> + PythonSubtypeChecker.checkIfRightIsSubtypeOfLeft(funcArg, callArg, storage) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/ParseUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/ParseUtils.kt new file mode 100644 index 0000000000..47186a3366 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/ParseUtils.kt @@ -0,0 +1,238 @@ +package org.utbot.python.newtyping.ast + +import org.parsers.python.Node +import org.parsers.python.PythonConstants +import org.parsers.python.ast.AdditiveExpression +import org.parsers.python.ast.Argument +import org.parsers.python.ast.Assignment +import org.parsers.python.ast.Block +import org.parsers.python.ast.ClassDefinition +import org.parsers.python.ast.Comparison +import org.parsers.python.ast.Conjunction +import org.parsers.python.ast.Decorators +import org.parsers.python.ast.Delimiter +import org.parsers.python.ast.Disjunction +import org.parsers.python.ast.DotName +import org.parsers.python.ast.ForStatement +import org.parsers.python.ast.FunctionCall +import org.parsers.python.ast.FunctionDefinition +import org.parsers.python.ast.Group +import org.parsers.python.ast.IfStatement +import org.parsers.python.ast.Inversion +import org.parsers.python.ast.InvocationArguments +import org.parsers.python.ast.Keyword +import org.parsers.python.ast.MultiplicativeExpression +import org.parsers.python.ast.Name +import org.parsers.python.ast.Operator +import org.parsers.python.ast.Slice +import org.parsers.python.ast.SliceExpression +import org.parsers.python.ast.StarNamedExpressions +import org.parsers.python.ast.Tuple + +data class ParsedFunctionDefinition(val name: Name, val body: Block, val decorators: List) +data class ParsedClassDefinition(val name: Name, val body: Block) +data class ParsedForStatement(val forVariable: ForVariable, val iterable: Node) +sealed class ForVariable +data class SimpleForVariable(val variable: Name) : ForVariable() +data class TupleForVariable(val elems: List) : ForVariable() +data class ParsedSliceExpression(val head: Node, val slices: ParsedSlices) +sealed class ParsedSlices +data class SimpleSlice(val indexValue: Node) : ParsedSlices() +data class SlicedSlice(val start: Node?, val end: Node?, val step: Node?) : ParsedSlices() +data class TupleSlice(val elems: List) : ParsedSlices() +data class ParsedIfStatement(val condition: Node) +data class ParsedConjunction(val left: Node, val right: Node) +data class ParsedDisjunction(val left: Node, val right: Node) +data class ParsedInversion(val expr: Node) +data class ParsedGroup(val expr: Node) +data class ParsedList(val elems: List) +sealed class ParsedAssignment +data class SimpleAssign(val targets: List, val value: Node) : ParsedAssignment() +data class OpAssign(val target: Node, val op: Delimiter, val value: Node) : ParsedAssignment() +data class ParsedMultiplicativeExpression(val cases: List) +data class ParsedBinaryOperation(val left: Node, val op: Node, val right: Node) +data class ParsedDotName(val head: Node, val tail: Node) +data class ParsedFunctionCall(val function: Node, val args: List) +data class ParsedComparison(val cases: List) +data class PrimitiveComparison(val left: Node, val op: Delimiter, val right: Node) +data class ParsedDecorator(val name: Name, val arguments: InvocationArguments? = null) + +fun parseFunctionDefinition(node: FunctionDefinition): ParsedFunctionDefinition? { + val name = (node.children().first { it is Name } ?: return null) as Name + val body = (node.children().find { it is Block } ?: return null) as Block + val decoratorNodes = node.children().filterIsInstance().firstOrNull() + return ParsedFunctionDefinition(name, body, parseDecorators(decoratorNodes)) +} + +fun parseClassDefinition(node: ClassDefinition): ParsedClassDefinition? { + val name = (node.children().first { it is Name } ?: return null) as Name + val body = (node.children().find { it is Block } ?: return null) as Block + return ParsedClassDefinition(name, body) +} + +fun isIdentification(node: Node): Boolean { + val name = node as? Name ?: return false + val parent = name.parent as? DotName ?: return true + return parent.children().first() == node +} + +fun parseForVariable(node: Node): ForVariable? { + if (node is Name) + return SimpleForVariable(node) + if (node is Tuple) { + return TupleForVariable( + node.children().mapNotNull { child -> + if (child is Delimiter) + return@mapNotNull null + parseForVariable(child) + } + ) + } + return null +} + +fun parseForStatement(node: ForStatement): ParsedForStatement? { + val children = node.children() + val forVariable = parseForVariable(children[1]) ?: return null + val iterable = children[3] + return ParsedForStatement(forVariable, iterable) +} + +fun parseSliceExpression(node: SliceExpression): ParsedSliceExpression? { + val children = node.children() + if (children.size != 2) + return null + val slicesChildren = children[1].children() + val slices = parseSlices(slicesChildren) ?: return null + return ParsedSliceExpression(children[0], slices) +} + +fun parseSlices(children: List): ParsedSlices? { + if (children.any { it is Delimiter && it.toString() == "," }) { + var i = 1 + val slices = mutableListOf() + while (i < children.size) { + var j = children.drop(i).indexOfFirst { it is Delimiter && it.toString() == "," } + if (j == -1) + j = children.size - 1 + else + j += i + val child = parseSlices(children.subList(i, j)) ?: return null + slices.add(child) + i = j + 1 + } + return TupleSlice(slices) + } + val index = if (children.size == 3) 1 else if (children.size == 1) 0 else return null + if (children[index] is Delimiter && children[index].toString() == ":") { + return SlicedSlice(null, null, null) + } + if (children[index] is Slice) { + val sliceChildren = children[index].children() + val i = sliceChildren.indexOfFirst { it is Delimiter && it.toString() == ":" } + var j = sliceChildren.drop(i + 1).indexOfFirst { it is Delimiter && it.toString() == ":" } + if (j == -1) + j = sliceChildren.size + val start = if (i == 0) null else if (i == 1) sliceChildren[0] else return null + val end = if (j - i == 1) null else if (j - i == 2) sliceChildren[i + 1] else return null + val step = + if (j >= sliceChildren.size - 1) null else if (j == sliceChildren.size - 2) sliceChildren.last() else return null + return SlicedSlice(start, end, step) + } + return SimpleSlice(children[index]) +} + +fun parseIfStatement(node: IfStatement): ParsedIfStatement = + ParsedIfStatement(node.children().first { it !is Keyword }) + +fun parseConjunction(node: Conjunction): ParsedConjunction = + ParsedConjunction(node.children()[0], node.children()[2]) + +fun parseDisjunction(node: Disjunction): ParsedDisjunction = + ParsedDisjunction(node.children()[0], node.children()[2]) + +fun parseInversion(node: Inversion): ParsedInversion = + ParsedInversion(node.children()[1]) + +fun parseGroup(node: Group): ParsedGroup = + ParsedGroup(node.children().first { it !is Delimiter }) + +fun parseAdditiveExpression(node: AdditiveExpression): ParsedBinaryOperation? { // TODO + val op = (node.children()[1] as? Operator) ?: return null + return ParsedBinaryOperation(node.children()[0], op, node.children()[2]) +} + +fun parseMultiplicativeExpression(node: MultiplicativeExpression): ParsedMultiplicativeExpression { + val binaries: MutableList = mutableListOf() + val children = node.children() + for (i in 0 until children.size - 2) { + if (children[i + 1] !is Operator && children[i + 1] !is Delimiter) + continue + binaries.add(ParsedBinaryOperation(children[i], children[i + 1], children[i + 2])) + } + return ParsedMultiplicativeExpression(binaries) +} + +fun parseList(node: org.parsers.python.ast.List): ParsedList { + if (node.children().size <= 2) // only delimiters + return ParsedList(emptyList()) + val expr = node.children()[1] + return if (expr is StarNamedExpressions) + ParsedList(expr.children().filter { it !is Delimiter }) + else + ParsedList(listOf(expr)) +} + +fun parseAssignment(node: Assignment): ParsedAssignment? { + val op = node.children()[1] as? Delimiter ?: return null + if (op.type == PythonConstants.TokenType.ASSIGN) { + val targets = node.children().dropLast(1).filter { it !is Delimiter } + return SimpleAssign(targets, node.children().last()) + } + if (node.children().size != 3) + return null + return OpAssign(node.children()[0], op, node.children()[2]) +} + +fun parseDotName(node: DotName): ParsedDotName = + ParsedDotName(node.children()[0], node.children()[2]) + +fun parseFunctionCall(node: FunctionCall): ParsedFunctionCall? { + val function = node.children()[0] + val args = (node.children()[1] as? InvocationArguments ?: return null).children().filter { + if (it is Argument) // for now ignore function calls with different argument kinds + return null + it !is Delimiter + } + return ParsedFunctionCall(function, args) +} + +fun parseComparison(node: Comparison): ParsedComparison { + val primitives: MutableList = mutableListOf() + val children = node.children() + for (i in 0 until children.size - 2) { + if (children[i + 1] !is Delimiter) + continue + primitives.add(PrimitiveComparison(children[i], children[i + 1] as Delimiter, children[i + 2])) + } + return ParsedComparison(primitives) +} + +fun parseDecorators(decoratorNodes: Decorators?): List { + return decoratorNodes?.children()?.let { + val decoratorIndexes = + it.mapIndexedNotNull { index, node -> if (node is Delimiter && node.tokenType == PythonConstants.TokenType.AT) index + 1 else null } + decoratorIndexes.mapNotNull { index -> + val decorator = it[index] + when (decorator) { + is Name -> ParsedDecorator(decorator) + is FunctionCall -> { + val name = decorator.children().filterIsInstance().first() + val args = decorator.children().filterIsInstance().first() + ParsedDecorator(name, args) + } + else -> null + } + } + } ?: emptyList() +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/TypeUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/TypeUtils.kt new file mode 100644 index 0000000000..3fe668987a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/TypeUtils.kt @@ -0,0 +1,17 @@ +package org.utbot.python.newtyping.ast + +import org.parsers.python.PythonConstants +import org.parsers.python.ast.NumericalLiteral +import org.utpython.types.PythonTypeHintsStorage +import org.utpython.types.general.UtType +import org.utpython.types.pythonAnyType + +fun typeOfNumericalLiteral(node: NumericalLiteral, storage: PythonTypeHintsStorage): UtType = + when (node.type) { + PythonConstants.TokenType.DECNUMBER, + PythonConstants.TokenType.HEXNUMBER, + PythonConstants.TokenType.OCTNUMBER -> storage.pythonInt + PythonConstants.TokenType.FLOAT -> storage.pythonFloat + PythonConstants.TokenType.COMPLEX -> storage.pythonComplex + else -> pythonAnyType + } \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/test.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/test.kt new file mode 100644 index 0000000000..4cf982e559 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/test.kt @@ -0,0 +1,18 @@ +package org.utbot.python.newtyping.ast + +import org.parsers.python.PythonParser +import org.utbot.python.PythonMethodHeader +import org.utbot.python.code.PythonCode + +/*fun main() { + val content = """ + class A: + @decorator + def func(x, y = 1, *args, **kwargs): + return 1 + """.trimIndent() + + val root = PythonParser(content).Module() + val y = PythonCode.findFunctionDefinition(root, PythonMethodHeader("func", "", null)) + val x = root +}*/ \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/Collector.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/Collector.kt new file mode 100644 index 0000000000..07f7ae3487 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/Collector.kt @@ -0,0 +1,9 @@ +package org.utbot.python.newtyping.ast.visitor + +import org.parsers.python.Node + +abstract class Collector { + open fun collectFromNodeBeforeRecursion(node: Node) = run { } + open fun collectFromNodeAfterRecursion(node: Node) = run { } + open fun finishCollection() = run { } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/Visitor.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/Visitor.kt new file mode 100644 index 0000000000..8261fe0941 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/Visitor.kt @@ -0,0 +1,29 @@ +package org.utbot.python.newtyping.ast.visitor + +import org.parsers.python.Node + +class Visitor(private val collectors: List) { + fun visit(node: Node) { + fixOffsets(node) // bug in parser? + innerVisit(node) + collectors.forEach { + it.finishCollection() + } + } + + private fun fixOffsets(node: Node) { + node.children().forEach { fixOffsets(it) } + if (node.children().isNotEmpty()) { + node.beginOffset = node.children().first().beginOffset + node.endOffset = node.children().last().endOffset + } + } + + private fun innerVisit(node: Node) { + collectors.forEach { it.collectFromNodeBeforeRecursion(node) } + node.children().forEach { + innerVisit(it) + } + collectors.forEach { it.collectFromNodeAfterRecursion(node) } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/constants/ConstantCollector.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/constants/ConstantCollector.kt new file mode 100644 index 0000000000..2bc3f65c35 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/constants/ConstantCollector.kt @@ -0,0 +1,85 @@ +package org.utbot.python.newtyping.ast.visitor.constants + +import org.parsers.python.Node +import org.parsers.python.PythonConstants +import org.parsers.python.ast.NumericalLiteral +import org.parsers.python.ast.SliceExpression +import org.parsers.python.ast.StringLiteral +import org.parsers.python.ast.UnaryExpression +import org.utpython.types.PythonTypeHintsStorage +import org.utbot.python.newtyping.ast.SimpleSlice +import org.utbot.python.newtyping.ast.TupleSlice +import org.utbot.python.newtyping.ast.parseSliceExpression +import org.utbot.python.newtyping.ast.typeOfNumericalLiteral +import org.utbot.python.newtyping.ast.visitor.Collector +import org.utpython.types.createPythonTupleType +import org.utpython.types.general.UtType +import java.math.BigDecimal +import java.math.BigInteger + +class ConstantCollector(private val storage: PythonTypeHintsStorage) : Collector() { + private val knownConstants = mutableMapOf, Pair>() + + private fun add(node: Node, type: UtType, value: Any) { + knownConstants[Pair(node.beginOffset, node.endOffset)] = Pair(type, value) + } + + private fun get(node: Node): Pair? { + return knownConstants[Pair(node.beginOffset, node.endOffset)] + } + + val result: List> + get() = knownConstants.map { it.value } + + override fun collectFromNodeAfterRecursion(node: Node) { + when (node) { + is NumericalLiteral -> processNumericalLiteral(node) + is StringLiteral -> processStringLiteral(node) + is SliceExpression -> processSliceExpression(node) + is UnaryExpression -> processUnaryExpression(node) + } + } + + private fun processNumericalLiteral(node: NumericalLiteral) { + val type = typeOfNumericalLiteral(node, storage) + val value = when (type) { + storage.pythonInt -> { + if (node.type == PythonConstants.TokenType.DECNUMBER) + node.toString().toBigInteger() + else return + } + storage.pythonFloat -> node.toString().toBigDecimal() + else -> return + } + add(node, type, value) + } + + private fun processStringLiteral(node: StringLiteral) { + if (node.toString().first() != 'f') + add(node, storage.pythonStr, node.toString()) + } + + private fun processSliceExpression(node: SliceExpression) { + val parsed = parseSliceExpression(node) ?: return + if (parsed.slices !is TupleSlice) + return + val elemNodes = parsed.slices.elems.map { it as? SimpleSlice ?: return } + val elems = elemNodes.map { get(it.indexValue) ?: return } + val type = createPythonTupleType(elems.map { it.first }) + knownConstants[Pair(elemNodes.first().indexValue.beginOffset, elemNodes.last().indexValue.endOffset)] = + Pair(type, elems) + } + + private fun processUnaryExpression(node: UnaryExpression) { + if (node.children().size != 2 || node.children().first().toString() != "-") + return + val child = get(node.children()[1]) ?: return + val value = child.second + if (value is BigInteger) { + add(node, storage.pythonInt, -value) + } + if (value is BigDecimal) { + add(node, storage.pythonFloat, -value) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/hints/HintCollector.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/hints/HintCollector.kt new file mode 100644 index 0000000000..6dfaba253a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/hints/HintCollector.kt @@ -0,0 +1,649 @@ +package org.utbot.python.newtyping.ast.visitor.hints + +import org.parsers.python.Node +import org.parsers.python.PythonConstants +import org.parsers.python.ast.AdditiveExpression +import org.parsers.python.ast.Argument +import org.parsers.python.ast.Assignment +import org.parsers.python.ast.Block +import org.parsers.python.ast.Comparison +import org.parsers.python.ast.Conjunction +import org.parsers.python.ast.DedentToken +import org.parsers.python.ast.Delimiter +import org.parsers.python.ast.Disjunction +import org.parsers.python.ast.DotName +import org.parsers.python.ast.ForStatement +import org.parsers.python.ast.FunctionCall +import org.parsers.python.ast.Group +import org.parsers.python.ast.IfStatement +import org.parsers.python.ast.IndentToken +import org.parsers.python.ast.Inversion +import org.parsers.python.ast.InvocationArguments +import org.parsers.python.ast.Keyword +import org.parsers.python.ast.MultiplicativeExpression +import org.parsers.python.ast.Name +import org.parsers.python.ast.Newline +import org.parsers.python.ast.NumericalLiteral +import org.parsers.python.ast.Operator +import org.parsers.python.ast.ReturnStatement +import org.parsers.python.ast.Slice +import org.parsers.python.ast.SliceExpression +import org.parsers.python.ast.Slices +import org.parsers.python.ast.Statement +import org.parsers.python.ast.StringLiteral +import org.utbot.python.PythonMethod +import org.utpython.types.PythonCallableTypeDescription +import org.utpython.types.PythonOverloadTypeDescription +import org.utpython.types.PythonTypeHintsStorage +import org.utbot.python.newtyping.ast.OpAssign +import org.utbot.python.newtyping.ast.ParsedFunctionCall +import org.utbot.python.newtyping.ast.SimpleAssign +import org.utbot.python.newtyping.ast.SimpleForVariable +import org.utbot.python.newtyping.ast.SimpleSlice +import org.utbot.python.newtyping.ast.SlicedSlice +import org.utbot.python.newtyping.ast.TupleSlice +import org.utbot.python.newtyping.ast.isIdentification +import org.utbot.python.newtyping.ast.parseAdditiveExpression +import org.utbot.python.newtyping.ast.parseAssignment +import org.utbot.python.newtyping.ast.parseComparison +import org.utbot.python.newtyping.ast.parseConjunction +import org.utbot.python.newtyping.ast.parseDisjunction +import org.utbot.python.newtyping.ast.parseDotName +import org.utbot.python.newtyping.ast.parseForStatement +import org.utbot.python.newtyping.ast.parseFunctionCall +import org.utbot.python.newtyping.ast.parseGroup +import org.utbot.python.newtyping.ast.parseIfStatement +import org.utbot.python.newtyping.ast.parseInversion +import org.utbot.python.newtyping.ast.parseList +import org.utbot.python.newtyping.ast.parseMultiplicativeExpression +import org.utbot.python.newtyping.ast.parseSliceExpression +import org.utbot.python.newtyping.ast.signaturesAreCompatible +import org.utbot.python.newtyping.ast.typeOfNumericalLiteral +import org.utbot.python.newtyping.ast.visitor.Collector +import org.utpython.types.createBinaryProtocol +import org.utpython.types.createCallableProtocol +import org.utpython.types.createIterableWithCustomReturn +import org.utpython.types.createProtocolWithAttribute +import org.utpython.types.createPythonCallableType +import org.utpython.types.general.DefaultSubstitutionProvider +import org.utpython.types.general.FunctionTypeCreator +import org.utpython.types.general.UtType +import org.utpython.types.getPythonAttributeByName +import org.utpython.types.inference.TypeInferenceEdgeWithBound +import org.utpython.types.inference.addEdge +import org.utpython.types.mypy.GlobalNamesStorage +import org.utpython.types.pythonAnyType +import org.utpython.types.pythonDescription +import org.utpython.types.pythonNoneType +import org.utpython.types.supportsBoolProtocol +import java.util.* + +class HintCollector( + private val function: PythonMethod, + private val storage: PythonTypeHintsStorage, + private val mypyTypes: Map, UtType>, + private val globalNamesStorage: GlobalNamesStorage, + private val moduleOfSources: String +) : Collector() { + private val parameterToNode: Map = + (function.argumentsNames zip function.methodType.arguments).associate { + it.first to HintCollectorNode(it.second) + } + private val astNodeToHintCollectorNode: MutableMap = mutableMapOf() + private val identificationToNode: MutableMap> = mutableMapOf() + private val blockStack = Stack() + + init { + val argNames = function.argumentsNames + assert(argNames.all { it != "" }) + identificationToNode[null] = mutableMapOf() + argNames.forEach { + identificationToNode[null]!![it] = parameterToNode[it]!! + } + } + + lateinit var result: HintCollectorResult + override fun finishCollection() { + val allNodes: MutableSet = mutableSetOf() + (parameterToNode + astNodeToHintCollectorNode).forEach { + if (!allNodes.contains(it.value)) + collectAllNodes(it.value, allNodes) + } + result = HintCollectorResult(parameterToNode, function.methodType, allNodes) + } + + private fun collectAllNodes(cur: HintCollectorNode, visited: MutableSet) { + visited.add(cur) + cur.outgoingEdges.forEach { + val to = it.to + if (!visited.contains(to)) + collectAllNodes(to, visited) + } + } + + override fun collectFromNodeBeforeRecursion(node: Node) { + if (node is Block) { + blockStack.add(node) + identificationToNode[node] = mutableMapOf() + } + } + + override fun collectFromNodeAfterRecursion(node: Node) { + when (node) { + is FunctionCall -> processFunctionCall(node) + is Keyword -> processKeyword(node) + is NumericalLiteral -> processNumericalLiteral(node) + is StringLiteral -> processStringLiteral(node) + is Block -> processBlock(node) + is Name -> processName(node) + is ForStatement -> processForStatement(node) + is IfStatement -> processIfStatement(node) + is Conjunction -> processConjunction(node) + is Disjunction -> processDisjunction(node) + is Inversion -> processInversion(node) + is Group -> processGroup(node) + is AdditiveExpression -> processAdditiveExpression(node) + is MultiplicativeExpression -> processMultiplicativeExpression(node) + is Comparison -> processComparison(node) + is org.parsers.python.ast.List -> processList(node) + is Assignment -> processAssignment(node) + is DotName -> processDotName(node) + is SliceExpression -> processSliceExpression(node) + // TODO: UnaryExpression, Power, ShiftExpression, BitwiseAnd, BitwiseOr, BitwiseXor + // TODO: Set, Dict, comprehensions + is Newline, is IndentToken, is Delimiter, is Operator, is DedentToken, is ReturnStatement, + is Statement, is InvocationArguments, is Argument, is Slice, is Slices -> Unit + else -> astNodeToHintCollectorNode[node] = HintCollectorNode(pythonAnyType) + } + } + + private fun processFunctionCall(node: FunctionCall) { + val parsed = parseFunctionCall(node) + if (parsed == null) { + astNodeToHintCollectorNode[node] = HintCollectorNode(pythonAnyType) + return + } + if (processIsinstanceCall(parsed, node)) + return + val rawType = mypyTypes[parsed.function.beginOffset to parsed.function.endOffset] + val attr = rawType?.getPythonAttributeByName( + storage, + "__call__" + ) + val type = attr?.type + val typeDescription = type?.pythonDescription() + val callType = createPythonCallableType( + parsed.args.size, + List(parsed.args.size) { PythonCallableTypeDescription.ArgKind.ARG_POS }, + List(parsed.args.size) { "" } + ) { + FunctionTypeCreator.InitializationData(List(parsed.args.size) { pythonAnyType }, pythonAnyType) + } + when (typeDescription) { + is PythonCallableTypeDescription -> { + astNodeToHintCollectorNode[node] = + HintCollectorNode(typeDescription.castToCompatibleTypeApi(type).returnValue) + if (parsed.args.size != typeDescription.numberOfArguments) + return + (parsed.args zip typeDescription.castToCompatibleTypeApi(type).arguments).forEach { (argNode, bound) -> + astNodeToHintCollectorNode[argNode]!!.upperBounds.add(bound) + } + } + is PythonOverloadTypeDescription -> { + val hintNode = HintCollectorNode(pythonAnyType) + astNodeToHintCollectorNode[node] = hintNode + typeDescription.getAnnotationParameters(type).forEach { typeCandidate -> + val descr = typeCandidate.pythonDescription() as? PythonCallableTypeDescription ?: return@forEach + val typeCandidateFunctionType = descr.castToCompatibleTypeApi(typeCandidate) + if (!signaturesAreCompatible(typeCandidateFunctionType, callType, storage)) + return@forEach + (parsed.args zip typeCandidateFunctionType.arguments).forEach { (argNode, argType) -> + astNodeToHintCollectorNode[argNode]!!.upperBounds.add(argType) + } + hintNode.upperBounds.add(typeCandidateFunctionType.returnValue) + } + } + else -> { + astNodeToHintCollectorNode[node] = HintCollectorNode(pythonAnyType) + val funcNode = parsed.function + astNodeToHintCollectorNode[funcNode]!!.upperBounds.add( + createCallableProtocol(List(parsed.args.size) { pythonAnyType }, pythonAnyType) + ) + } + } + } + + private fun processIsinstanceCall(parsedFunctionCall: ParsedFunctionCall, node: FunctionCall): Boolean { + if (parsedFunctionCall.function !is Name || parsedFunctionCall.function.toString() != "isinstance" || + parsedFunctionCall.args.size != 2) + return false + + val typeAsString = parsedFunctionCall.args[1].toString() + val type = globalNamesStorage.resolveTypeName(moduleOfSources, typeAsString) ?: return false + + astNodeToHintCollectorNode[node] = HintCollectorNode(storage.pythonBool) + val objNode = astNodeToHintCollectorNode[parsedFunctionCall.args[0]]!! + objNode.lowerBounds.add(type) + return true + } + + private fun processKeyword(node: Keyword) { + val type = when (node.type) { + PythonConstants.TokenType.TRUE, PythonConstants.TokenType.FALSE -> storage.pythonBool + PythonConstants.TokenType.NONE -> pythonNoneType + PythonConstants.TokenType.FOR, PythonConstants.TokenType.IF, + PythonConstants.TokenType.ELSE, PythonConstants.TokenType.ELIF, + PythonConstants.TokenType.WHILE, PythonConstants.TokenType.IN, + PythonConstants.TokenType.RETURN, PythonConstants.TokenType.OR, PythonConstants.TokenType.AND, + PythonConstants.TokenType.NOT -> return + else -> pythonAnyType + } + astNodeToHintCollectorNode[node] = HintCollectorNode(type) + } + + private fun processNumericalLiteral(node: NumericalLiteral) { + val type = typeOfNumericalLiteral(node, storage) + astNodeToHintCollectorNode[node] = HintCollectorNode(type) + } + + private fun processStringLiteral(node: StringLiteral) { + astNodeToHintCollectorNode[node] = HintCollectorNode(storage.pythonStr) + } + + private fun processBlock(node: Block) { + blockStack.pop() + val prevBlock = if (blockStack.isEmpty()) null else blockStack.peek() + identificationToNode[node]!!.forEach { (id, hintNode) -> + val prevHintNode = identificationToNode[prevBlock]!![id] ?: HintCollectorNode(pythonAnyType) + identificationToNode[prevBlock]!![id] = prevHintNode + val edgeFromPrev = HintEdgeWithBound( + from = prevHintNode, + to = hintNode, + source = EdgeSource.Identification, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { upperType -> listOf(upperType) } + val edgeToPrev = HintEdgeWithBound( + from = hintNode, + to = prevHintNode, + source = EdgeSource.Identification, + boundType = TypeInferenceEdgeWithBound.BoundType.Lower + ) { lowerType -> listOf(lowerType) } + + addEdge(edgeFromPrev) + addEdge(edgeToPrev) + } + } + + private fun processName(node: Name) { + if (!isIdentification(node)) + return + val name = node.toString() + val block = blockStack.peek() + if (identificationToNode[block]!![name] == null) { + val type = mypyTypes[node.beginOffset to node.endOffset] ?: pythonAnyType + identificationToNode[block]!![name] = HintCollectorNode(type) + } + astNodeToHintCollectorNode[node] = identificationToNode[block]!![name]!! + } + + private fun processForStatement(node: ForStatement) { + val parsed = parseForStatement(node) ?: return + // TODO: case of TupleForVariable + val iterableNode = astNodeToHintCollectorNode[parsed.iterable]!! + if (parsed.forVariable !is SimpleForVariable) { + addProtocol(iterableNode, createIterableWithCustomReturn(pythonAnyType)) + return + } + val variableNode = astNodeToHintCollectorNode[parsed.forVariable.variable]!! + val edgeFromVariableToIterable = HintEdgeWithBound( + from = variableNode, + to = iterableNode, + source = EdgeSource.ForStatement, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { varType -> listOf(createIterableWithCustomReturn(varType)) } + val edgeFromIterableToVariable = HintEdgeWithBound( + from = iterableNode, + to = variableNode, + source = EdgeSource.ForStatement, + boundType = TypeInferenceEdgeWithBound.BoundType.Lower + ) { iterType -> + val iterReturnType = iterType.getPythonAttributeByName(storage, "__iter__")?.type + ?: return@HintEdgeWithBound emptyList() + listOf(iterReturnType) + } + + addEdge(edgeFromVariableToIterable) + addEdge(edgeFromIterableToVariable) + } + + private fun processIfStatement(node: IfStatement) { + val parsed = parseIfStatement(node) + addProtocol(astNodeToHintCollectorNode[parsed.condition]!!, supportsBoolProtocol(storage)) + } + + private fun processConjunction(node: Conjunction) { + processBoolExpression(node) + val parsed = parseConjunction(node) + addProtocol(astNodeToHintCollectorNode[parsed.left]!!, supportsBoolProtocol(storage)) + addProtocol(astNodeToHintCollectorNode[parsed.right]!!, supportsBoolProtocol(storage)) + } + + private fun processDisjunction(node: Disjunction) { + processBoolExpression(node) + val parsed = parseDisjunction(node) + addProtocol(astNodeToHintCollectorNode[parsed.left]!!, supportsBoolProtocol(storage)) + addProtocol(astNodeToHintCollectorNode[parsed.right]!!, supportsBoolProtocol(storage)) + } + + private fun processInversion(node: Inversion) { + processBoolExpression(node) + val parsed = parseInversion(node) + addProtocol(astNodeToHintCollectorNode[parsed.expr]!!, supportsBoolProtocol(storage)) + } + + private fun processGroup(node: Group) { + val parsed = parseGroup(node) + val exprNode = astNodeToHintCollectorNode[parsed.expr]!! + astNodeToHintCollectorNode[node] = exprNode + } + + private fun processAdditiveExpression(node: AdditiveExpression) { + val curNode = HintCollectorNode(pythonAnyType) + astNodeToHintCollectorNode[node] = curNode + val parsed = parseAdditiveExpression(node) ?: return + processBinaryExpression( + curNode, + parsed.left, + parsed.right, + getOperationOfOperator(parsed.op.toString()) ?: return + ) + } + + private fun processMultiplicativeExpression(node: MultiplicativeExpression) { + val curNode = HintCollectorNode(pythonAnyType) + astNodeToHintCollectorNode[node] = curNode + val parsed = parseMultiplicativeExpression(node) + parsed.cases.forEach { + processBinaryExpression( + curNode, + it.left, + it.right, + getOperationOfOperator(it.op.toString()) ?: return, + source = EdgeSource.Multiplication + ) + } + } + + private fun processComparison(node: Comparison) { + val parsed = parseComparison(node) + parsed.cases.forEach { (left, op, right) -> + val comp = getComparison(op.toString()) ?: return@forEach + val leftNode = astNodeToHintCollectorNode[left]!! + val rightNode = astNodeToHintCollectorNode[right]!! + + val edgeFromLeftToRight = HintEdgeWithBound( + from = leftNode, + to = rightNode, + source = EdgeSource.Comparison, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { rightType -> listOf(createBinaryProtocol(comp.method, rightType, storage.pythonBool)) } + val edgeFromRightToLeft = HintEdgeWithBound( + from = rightNode, + to = leftNode, + source = EdgeSource.Comparison, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { leftType -> listOf(createBinaryProtocol(reverseComparison(comp).method, leftType, storage.pythonBool)) } + + addEdge(edgeFromLeftToRight) + addEdge(edgeFromRightToLeft) + } + processBoolExpression(node) + } + + private fun processList(node: org.parsers.python.ast.List) { + val parsed = parseList(node) + val partialType = DefaultSubstitutionProvider.substituteByIndex(storage.pythonList, 0, pythonAnyType) + val curNode = HintCollectorNode(partialType) + astNodeToHintCollectorNode[node] = curNode + val innerTypeNode = HintCollectorNode(pythonAnyType) + val edgeFromInnerToCur = HintEdgeWithValue( + from = innerTypeNode, + to = curNode, + annotationParameterId = 0 + ) + addEdge(edgeFromInnerToCur) + + parsed.elems.forEach { elem -> + val elemNode = astNodeToHintCollectorNode[elem]!! + val edge = HintEdgeWithBound( + from = elemNode, + to = innerTypeNode, + source = EdgeSource.CollectionElement, + boundType = TypeInferenceEdgeWithBound.BoundType.Lower + ) { listOf(it) } + addEdge(edge) + } + } + + private fun processAssignment(node: Assignment) { + val parsed = parseAssignment(node) ?: return + when (parsed) { + is SimpleAssign -> { + val targetNodes = parsed.targets.map { astNodeToHintCollectorNode[it]!! } + val valueNode = astNodeToHintCollectorNode[parsed.value]!! + targetNodes.forEach { target -> + val edgeFromValue = HintEdgeWithBound( + from = valueNode, + to = target, + source = EdgeSource.Assign, + boundType = TypeInferenceEdgeWithBound.BoundType.Lower + ) { listOf(it) } + val edgeFromTarget = HintEdgeWithBound( + from = target, + to = valueNode, + source = EdgeSource.Assign, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { listOf(it) } + addEdge(edgeFromValue) + addEdge(edgeFromTarget) + } + } + is OpAssign -> { + val targetNode = astNodeToHintCollectorNode[parsed.target]!! + val op = getOperationOfOpAssign(parsed.op.toString()) ?: return + val nodeForOpResult = HintCollectorNode(pythonAnyType) + processBinaryExpression(nodeForOpResult, parsed.target, parsed.value, op) + + val edgeToTarget = HintEdgeWithBound( + from = nodeForOpResult, + to = targetNode, + source = EdgeSource.OpAssign, + boundType = TypeInferenceEdgeWithBound.BoundType.Lower + ) { listOf(it) } + val edgeFromTarget = HintEdgeWithBound( + from = targetNode, + to = nodeForOpResult, + source = EdgeSource.OpAssign, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { listOf(it) } + + addEdge(edgeToTarget) + addEdge(edgeFromTarget) + } + } + } + + private fun processDotName(node: DotName) { + val parsed = parseDotName(node) + val type = mypyTypes[node.beginOffset to node.endOffset] ?: pythonAnyType + val curNode = HintCollectorNode(type) + astNodeToHintCollectorNode[node] = curNode + val headNode = astNodeToHintCollectorNode[parsed.head]!! + val attribute = parsed.tail.toString() + // addProtocol(headNode, createProtocolWithAttribute(attribute, pythonAnyType)) + + val edgeFromHead = HintEdgeWithBound( + from = headNode, + to = curNode, + source = EdgeSource.Attribute, + boundType = TypeInferenceEdgeWithBound.BoundType.Lower + ) { headType -> + headType.getPythonAttributeByName(storage, attribute)?.let { listOf(it.type) } ?: emptyList() + } + val edgeToHead = HintEdgeWithBound( + from = curNode, + to = headNode, + source = EdgeSource.Attribute, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { curType -> + listOf(createProtocolWithAttribute(attribute, curType)) + } + addEdge(edgeFromHead) + addEdge(edgeToHead) + } + + private fun processSliceExpression(node: SliceExpression) { + val curNode = HintCollectorNode(pythonAnyType) + astNodeToHintCollectorNode[node] = curNode + val parsed = parseSliceExpression(node) ?: return + val headNode = astNodeToHintCollectorNode[parsed.head]!! + when (parsed.slices) { + is SimpleSlice -> { + val indexNode = astNodeToHintCollectorNode[parsed.slices.indexValue]!! + val edgeFromIndexToHead = HintEdgeWithBound( + from = indexNode, + to = headNode, + source = EdgeSource.Slice, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { indexType -> + listOf(createBinaryProtocol("__getitem__", indexType, pythonAnyType)) + } + val edgeFromHeadToIndex = HintEdgeWithBound( + from = headNode, + to = indexNode, + source = EdgeSource.Slice, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { headType -> + val attr = headType.getPythonAttributeByName(storage, "__getitem__") + ?: return@HintEdgeWithBound emptyList() + // TODO: case of Overload + val descr = attr.type.pythonDescription() as? PythonCallableTypeDescription + ?: return@HintEdgeWithBound emptyList() + if (descr.numberOfArguments != 2) + return@HintEdgeWithBound emptyList() + listOf(attr.type.parameters[1]) + } + addEdge(edgeFromIndexToHead) + addEdge(edgeFromHeadToIndex) + } + is SlicedSlice -> { + addProtocol(headNode, createBinaryProtocol("__getitem__", storage.pythonSlice, pythonAnyType)) + // not necessary, but usually. TODO: remove if it spoils anything + for (slicePart in listOf(parsed.slices.start, parsed.slices.end, parsed.slices.step)) { + if (slicePart == null) + continue + val sliceNode = astNodeToHintCollectorNode[slicePart]!! + sliceNode.lowerBounds.add(storage.pythonInt) + } + } + is TupleSlice -> { + addProtocol(headNode, createBinaryProtocol("__getitem__", storage.tupleOfAny, pythonAnyType)) + } + } + val edgeFromCurToHead = HintEdgeWithBound( + from = curNode, + to = headNode, + source = EdgeSource.Slice, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { curType -> + listOf(createBinaryProtocol("__getitem__", pythonAnyType, curType)) + } + val edgeFromHeadToCur = HintEdgeWithBound( + from = headNode, + to = curNode, + source = EdgeSource.Slice, + boundType = TypeInferenceEdgeWithBound.BoundType.Lower + ) { headType -> + val attr = headType.getPythonAttributeByName(storage, "__getitem__") + ?: return@HintEdgeWithBound emptyList() + // TODO: case of Overload + val descr = attr.type.pythonDescription() as? PythonCallableTypeDescription + ?: return@HintEdgeWithBound emptyList() + listOf(descr.castToCompatibleTypeApi(attr.type).returnValue) + } + addEdge(edgeFromCurToHead) + addEdge(edgeFromHeadToCur) + } + + private fun processBoolExpression(node: Node) { + val hintCollectorNode = HintCollectorNode(storage.pythonBool) + astNodeToHintCollectorNode[node] = hintCollectorNode + } + + private fun addProtocol(node: HintCollectorNode, protocol: UtType) { + node.upperBounds.add(protocol) + } + + private fun processBinaryExpression( + curNode: HintCollectorNode, + left: Node, + right: Node, + op: Operation, + source: EdgeSource = EdgeSource.Operation + ) { + val leftNode = astNodeToHintCollectorNode[left]!! + val rightNode = astNodeToHintCollectorNode[right]!! + val methodName = op.method + + val edgeFromLeftToCur = HintEdgeWithBound( + from = leftNode, + to = curNode, + source = source, + boundType = TypeInferenceEdgeWithBound.BoundType.Lower + ) { leftType -> + listOf( + (leftType.getPythonAttributeByName(storage, methodName) + ?: return@HintEdgeWithBound emptyList()).type + ) + } + val edgeFromRightToCur = HintEdgeWithBound( + from = rightNode, + to = curNode, + source = source, + boundType = TypeInferenceEdgeWithBound.BoundType.Lower + ) { rightType -> + listOf( + (rightType.getPythonAttributeByName(storage, methodName) + ?: return@HintEdgeWithBound emptyList()).type + ) + } + addEdge(edgeFromLeftToCur) + addEdge(edgeFromRightToCur) + + val edgeFromLeftToRight = HintEdgeWithBound( + from = leftNode, + to = rightNode, + source = source, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { leftType -> listOf(createBinaryProtocol(methodName, leftType, pythonAnyType)) } + val edgeFromRightToLeft = HintEdgeWithBound( + from = rightNode, + to = leftNode, + source = source, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { rightType -> listOf(createBinaryProtocol(methodName, rightType, pythonAnyType)) } + addEdge(edgeFromLeftToRight) + addEdge(edgeFromRightToLeft) + + listOf(leftNode, rightNode).forEach { + val edgeFromCurToUp = HintEdgeWithBound( + from = curNode, + to = it, + source = source, + boundType = TypeInferenceEdgeWithBound.BoundType.Upper + ) { curType -> listOf(createBinaryProtocol(methodName, pythonAnyType, curType)) } + addEdge(edgeFromCurToUp) + } + + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/hints/HintCollectorStructures.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/hints/HintCollectorStructures.kt new file mode 100644 index 0000000000..f7fa189a3d --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/hints/HintCollectorStructures.kt @@ -0,0 +1,53 @@ +package org.utbot.python.newtyping.ast.visitor.hints + +import org.utpython.types.general.FunctionType +import org.utpython.types.general.UtType +import org.utpython.types.inference.TypeInferenceEdge +import org.utpython.types.inference.TypeInferenceEdgeWithBound +import org.utpython.types.inference.TypeInferenceEdgeWithValue +import org.utpython.types.inference.TypeInferenceNode + +sealed class HintEdge( + override val from: HintCollectorNode, + override val to: HintCollectorNode, +): TypeInferenceEdge + +class HintEdgeWithBound( + from: HintCollectorNode, + to: HintCollectorNode, + val source: EdgeSource, + override val boundType: TypeInferenceEdgeWithBound.BoundType, + override val dependency: (UtType) -> List +): HintEdge(from, to), TypeInferenceEdgeWithBound + +class HintEdgeWithValue( + from: HintCollectorNode, + to: HintCollectorNode, + override val annotationParameterId: Int +): HintEdge(from, to), TypeInferenceEdgeWithValue + +class HintCollectorNode(override val partialType: UtType): TypeInferenceNode { + val lowerBounds: MutableList = mutableListOf() + val upperBounds: MutableList = mutableListOf() + override val outgoingEdges: MutableSet = mutableSetOf() + override val ingoingEdges: MutableSet = mutableSetOf() +} + +class HintCollectorResult( + val parameterToNode: Map, + val initialSignature: FunctionType, + val allNodes: Set +) + +enum class EdgeSource { + ForStatement, + Identification, + Operation, + Multiplication, + CollectionElement, + Assign, + OpAssign, + Attribute, + Comparison, + Slice +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/hints/Protocols.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/hints/Protocols.kt new file mode 100644 index 0000000000..2c2e888487 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/ast/visitor/hints/Protocols.kt @@ -0,0 +1,63 @@ +package org.utbot.python.newtyping.ast.visitor.hints + +enum class Operation(val method: String) { + Add("__add__"), + Sub("__sub__"), + Mul("__mul__"), + TrueDiv("__truediv__"), + FloorDiv("__floordiv__"), + Mod("__mod__"), + Pow("__pow__"), + MatMul("__matmul__"), + And("__and__") +} + +fun getOperationOfOperator(op: String): Operation? = + when (op) { + "+" -> Operation.Add + "-" -> Operation.Sub + "*" -> Operation.Mul + "/" -> Operation.TrueDiv + "//" -> Operation.FloorDiv + "%" -> Operation.Mod + "**" -> Operation.Pow + "@" -> Operation.MatMul + "&" -> Operation.And + else -> null + } + +enum class ComparisonSign(val method: String) { + Lt("__lt__"), + Le("__le__"), + Eq("__eq__"), + Ne("__ne__"), + Gt("__gt__"), + Ge("__ge__") +} + +fun getComparison(op: String): ComparisonSign? = + when (op) { + "<" -> ComparisonSign.Lt + "<=" -> ComparisonSign.Le + "==" -> ComparisonSign.Eq + "!=" -> ComparisonSign.Ne + ">" -> ComparisonSign.Gt + ">=" -> ComparisonSign.Ge + else -> null + } + +fun reverseComparison(comp: ComparisonSign): ComparisonSign = + when (comp) { + ComparisonSign.Lt -> ComparisonSign.Gt + ComparisonSign.Le -> ComparisonSign.Ge + ComparisonSign.Eq -> ComparisonSign.Eq + ComparisonSign.Ne -> ComparisonSign.Ne + ComparisonSign.Gt -> ComparisonSign.Lt + ComparisonSign.Ge -> ComparisonSign.Le + } + +fun getOperationOfOpAssign(op: String): Operation? { + if (op.last() != '=') + return null + return getOperationOfOperator(op.dropLast(1)) +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/HintCollectorResultUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/HintCollectorResultUtils.kt new file mode 100644 index 0000000000..e6ff936f70 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/HintCollectorResultUtils.kt @@ -0,0 +1,46 @@ +package org.utbot.python.newtyping.inference + +import org.utbot.python.newtyping.ast.visitor.hints.HintCollectorNode +import org.utbot.python.newtyping.ast.visitor.hints.HintEdge +import org.utbot.python.newtyping.ast.visitor.hints.HintEdgeWithBound +import org.utpython.types.general.UtType +import org.utpython.types.inference.TypeInferenceEdgeWithBound +import org.utpython.types.pythonAnyType + +fun visitNodesByReverseEdges( + node: HintCollectorNode, + visited: MutableSet, + edgePredicate: (HintEdge) -> Boolean +) { + visited.add(node) + node.ingoingEdges.forEach { edge -> + if (edgePredicate(edge) && !visited.contains(edge.from)) + visitNodesByReverseEdges(edge.from, visited, edgePredicate) + } +} + +fun collectBoundsFromEdges(node: HintCollectorNode): Pair, List> { + val lowerBounds: MutableList = mutableListOf() + val upperBounds: MutableList = mutableListOf() + node.ingoingEdges.forEach { edge -> + if (edge !is HintEdgeWithBound) + return@forEach + val hints = edge.dependency(pythonAnyType) + when (edge.boundType) { + TypeInferenceEdgeWithBound.BoundType.Lower -> lowerBounds.addAll(hints) + TypeInferenceEdgeWithBound.BoundType.Upper -> upperBounds.addAll(hints) + } + } + return Pair(lowerBounds, upperBounds) +} + +fun collectBoundsFromComponent(component: Collection): Pair, List> { + val lowerBounds: MutableList = mutableListOf() + val upperBounds: MutableList = mutableListOf() + component.forEach { visitedNode -> + val (lowerFromEdges, upperFromEdges) = collectBoundsFromEdges(visitedNode) + lowerBounds.addAll(visitedNode.lowerBounds + lowerFromEdges + visitedNode.partialType) + upperBounds.addAll(visitedNode.upperBounds + upperFromEdges + visitedNode.partialType) + } + return Pair(lowerBounds, upperBounds) +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/TypeInferenceAlgorithmAPI.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/TypeInferenceAlgorithmAPI.kt new file mode 100644 index 0000000000..6ed0686157 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/TypeInferenceAlgorithmAPI.kt @@ -0,0 +1,15 @@ +package org.utbot.python.newtyping.inference + +import org.utpython.types.general.UtType + +abstract class TypeInferenceAlgorithm { + abstract suspend fun run( + isCancelled: () -> Boolean, + annotationHandler: suspend (UtType) -> InferredTypeFeedback, + ): Int +} + +sealed interface InferredTypeFeedback + +object SuccessFeedback : InferredTypeFeedback +object InvalidTypeFeedback : InferredTypeFeedback \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/TypeInferenceProcessor.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/TypeInferenceProcessor.kt new file mode 100644 index 0000000000..24bcfd0b12 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/TypeInferenceProcessor.kt @@ -0,0 +1,187 @@ +package org.utbot.python.newtyping.inference + +import kotlinx.coroutines.runBlocking +import org.parsers.python.PythonParser +import org.parsers.python.ast.ClassDefinition +import org.parsers.python.ast.FunctionDefinition +import org.utbot.python.PythonBaseMethod +import org.utpython.types.PythonFunctionDefinition +import org.utpython.types.PythonTypeHintsStorage +import org.utbot.python.newtyping.ast.parseClassDefinition +import org.utbot.python.newtyping.ast.parseFunctionDefinition +import org.utbot.python.newtyping.ast.visitor.Visitor +import org.utbot.python.newtyping.ast.visitor.hints.HintCollector +import org.utpython.types.general.CompositeType +import org.utpython.types.general.UtType +import org.utpython.types.getPythonAttributeByName +import org.utbot.python.newtyping.inference.baseline.BaselineAlgorithm +import org.utbot.python.newtyping.inference.baseline.MethodAndVars +import org.utbot.python.newtyping.mypy.dropInitFile +import org.utpython.types.mypy.GlobalNamesStorage +import org.utpython.types.mypy.MypyBuildDirectory +import org.utpython.types.mypy.MypyInfoBuild +import org.utpython.types.mypy.getErrorNumber +import org.utpython.types.mypy.readMypyAnnotationStorageAndInitialErrors +import org.utpython.types.pythonTypeRepresentation +import org.utpython.types.utils.getOffsetLine +import org.utbot.python.utils.Cleaner +import org.utbot.python.utils.Fail +import org.utbot.python.utils.Optional +import org.utbot.python.utils.RequirementsUtils +import org.utbot.python.utils.Success +import org.utbot.python.utils.TemporaryFileManager +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +class TypeInferenceProcessor( + private val pythonPath: String, + private val directoriesForSysPath: Set, + sourceFile: String, + private val moduleOfSourceFile: String, + private val functionName: String, + private val className: String? = null +) { + + private val path: Path = Paths.get(File(sourceFile).canonicalPath) + private val sourceFileContent = File(sourceFile).readText() + private val parsedFile = PythonParser(sourceFileContent).Module() + + fun inferTypes( + cancel: () -> Boolean, + processSignature: (UtType) -> Unit = {}, + checkRequirementsAction: () -> Unit = {}, + missingRequirementsAction: () -> Unit = {}, + loadingInfoAboutTypesAction: () -> Unit = {}, + analyzingCodeAction: () -> Unit = {}, + pythonMethodExtractionFailAction: (String) -> Unit = {}, + startingTypeInferenceAction: () -> Unit = {} + ): List { + Cleaner.restart() + try { + checkRequirementsAction() + + if (!RequirementsUtils.requirementsAreInstalled(pythonPath)) { + missingRequirementsAction() + return emptyList() + } + + loadingInfoAboutTypesAction() + + val (mypyBuild, report) = readMypyAnnotationStorageAndInitialErrors( + pythonPath, + path.toString(), + moduleOfSourceFile.dropInitFile(), + MypyBuildDirectory( + TemporaryFileManager.assignTemporaryFile(tag = "mypyBuildRoot"), + directoriesForSysPath + ) + ) + + val namesInModule = mypyBuild.names[moduleOfSourceFile]!!.map { it.name }.filter { + it.length < 4 || !it.startsWith("__") || !it.endsWith("__") + } + + analyzingCodeAction() + + val typeStorage = PythonTypeHintsStorage.get(mypyBuild) + val pythonMethodOpt = getPythonMethod(mypyBuild, typeStorage) + if (pythonMethodOpt is Fail) { + pythonMethodExtractionFailAction(pythonMethodOpt.message) + return emptyList() + } + + val pythonMethod = (pythonMethodOpt as Success).value + + val mypyExpressionTypes = mypyBuild.exprTypes[moduleOfSourceFile]!!.associate { + Pair(it.startOffset.toInt(), it.endOffset.toInt() + 1) to it.type.asUtBotType + } + val namesStorage = GlobalNamesStorage(mypyBuild) + val collector = + HintCollector(pythonMethod, typeStorage, mypyExpressionTypes, namesStorage, moduleOfSourceFile) + val visitor = Visitor(listOf(collector)) + visitor.visit(pythonMethod.ast) + + val algo = BaselineAlgorithm( + typeStorage, + collector.result, + pythonPath, + MethodAndVars(pythonMethod, ""), + emptyList(), + directoriesForSysPath, + moduleOfSourceFile, + namesInModule, + getErrorNumber( + report, + path.toString(), + getOffsetLine(sourceFileContent, pythonMethod.ast.beginOffset), + getOffsetLine(sourceFileContent, pythonMethod.ast.endOffset) + ), + mypyBuild.buildRoot.configFile, + dMypyTimeout = null + ) + + startingTypeInferenceAction() + val annotations = emptyList().toMutableList() + runBlocking { + algo.run(cancel) { + annotations.add(it) + processSignature(it) + SuccessFeedback + } + } + return annotations + } finally { + Cleaner.doCleaning() + } + } + + private fun getPythonMethod(mypyInfoBuild: MypyInfoBuild, typeStorage: PythonTypeHintsStorage): Optional { + if (className == null) { + val funcDef = parsedFile.children().firstNotNullOfOrNull { node -> + val res = (node as? FunctionDefinition)?.let { parseFunctionDefinition(it) } + if (res?.name?.toString() == functionName) res else null + } ?: return Fail("Couldn't find top-level function $functionName") + + val def = + mypyInfoBuild.definitions[moduleOfSourceFile]!![functionName]!!.getUtBotDefinition() as? PythonFunctionDefinition + ?: return Fail("$functionName is not a function") + + val result = PythonBaseMethod( + functionName, + path.toString(), + null, + sourceFileContent.substring(funcDef.body.beginOffset, funcDef.body.endOffset).trimIndent(), + def, + funcDef.body + ) + return Success(result) + } + val classDef = parsedFile.children().firstNotNullOfOrNull { node -> + val res = (node as? ClassDefinition)?.let { parseClassDefinition(it) } + if (res?.name?.toString() == className) res else null + } ?: return Fail("Couldn't find top-level class $className") + val funcDef = classDef.body.children().firstNotNullOfOrNull { node -> + val res = (node as? FunctionDefinition)?.let { parseFunctionDefinition(it) } + if (res?.name?.toString() == functionName) res else null + } ?: return Fail("Couldn't find method $functionName in class $className") + + val typeOfClass = mypyInfoBuild.definitions[moduleOfSourceFile]!![className]!!.getUtBotType() + as? CompositeType ?: return Fail("$className is not a class") + + val defOfFunc = typeOfClass.getPythonAttributeByName(typeStorage, functionName) as? PythonFunctionDefinition + ?: return Fail("$functionName is not a function") + + println(defOfFunc.type.pythonTypeRepresentation()) + + val result = PythonBaseMethod( + functionName, + path.toString(), + typeOfClass, + sourceFileContent.substring(funcDef.body.beginOffset, funcDef.body.endOffset).trimIndent(), + defOfFunc, + funcDef.body + ) + return Success(result) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/BaselineAlgorithm.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/BaselineAlgorithm.kt new file mode 100644 index 0000000000..1c709c3150 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/BaselineAlgorithm.kt @@ -0,0 +1,269 @@ +package org.utbot.python.newtyping.inference.baseline + +import mu.KotlinLogging +import org.utbot.python.PythonMethod +import org.utpython.types.PythonTypeHintsStorage +import org.utbot.python.newtyping.ast.visitor.hints.EdgeSource +import org.utbot.python.newtyping.ast.visitor.hints.HintCollectorNode +import org.utbot.python.newtyping.ast.visitor.hints.HintCollectorResult +import org.utbot.python.newtyping.ast.visitor.hints.HintEdgeWithBound +import org.utpython.types.createPythonUnionType +import org.utpython.types.general.FunctionType +import org.utpython.types.general.UtType +import org.utbot.python.newtyping.inference.InferredTypeFeedback +import org.utbot.python.newtyping.inference.InvalidTypeFeedback +import org.utbot.python.newtyping.inference.SuccessFeedback +import org.utbot.python.newtyping.inference.TypeInferenceAlgorithm +import org.utpython.types.inference.addEdge +import org.utbot.python.newtyping.inference.collectBoundsFromComponent +import org.utbot.python.newtyping.inference.visitNodesByReverseEdges +import org.utbot.python.newtyping.mypy.checkSuggestedSignatureWithDMypy +import org.utpython.types.pythonTypeRepresentation +import org.utpython.types.typesAreEqual +import org.utbot.python.newtyping.utils.weightedRandom +import org.utbot.python.utils.TemporaryFileManager +import java.io.File +import kotlin.random.Random +import org.utpython.types.pythonTypeName + +private val EDGES_TO_LINK = listOf( + EdgeSource.Identification, + EdgeSource.Assign, + EdgeSource.OpAssign, + EdgeSource.Operation, + EdgeSource.Comparison +) + +private val logger = KotlinLogging.logger {} + +data class MethodAndVars( + val method: PythonMethod, + val additionalVars: String, +) + +class BaselineAlgorithm( + private val storage: PythonTypeHintsStorage, + hintCollectorResult: HintCollectorResult, + private val pythonPath: String, + private val pythonMethod: MethodAndVars, + methodModifications: List = emptyList(), + private val directoriesForSysPath: Set, + private val moduleToImport: String, + private val namesInModule: Collection, + private val initialErrorNumber: Int, + private val configFile: File, + private val randomTypeFrequency: Int = 0, + private val dMypyTimeout: Long? +) : TypeInferenceAlgorithm() { + private val random = Random(0) + + private val initialPythonMethod: PythonMethod = pythonMethod.method + + private val generalRating = createGeneralTypeRating(hintCollectorResult, storage) + private val initialStates = methodModifications.ifEmpty { listOf(pythonMethod) } .map { + getInitialState(hintCollectorResult, generalRating, it.method.argumentsNames, it.method.methodType, it.additionalVars) + } + private val initialState = initialStates.first() + private val states: MutableList = initialStates.toMutableList() + private val fileForMypyRuns = TemporaryFileManager.assignTemporaryFile(tag = "mypy.py") + private var iterationCounter = 0 + private var randomTypeCounter = 0 + + private val simpleTypes = simplestTypes(storage) + private val mixtureType = createPythonUnionType(simpleTypes) + + private val expandedStates: MutableMap> = mutableMapOf() + private val statistic: MutableMap = mutableMapOf() + + private val checkedSignatures: MutableSet = mutableSetOf() + + private fun getRandomType(): UtType? { // TODO: Remove random type when ndarray + val weights = states.map { 1.0 / (it.anyNodes.size * it.anyNodes.size + 1) } + val state = weightedRandom(states, weights, random) + val newState = expandState(state, storage, state.anyNodes.map { mixtureType }) + if (newState != null) { + logger.info("Random type: ${newState.signature.pythonTypeRepresentation()}") + expandedStates[newState.signature] = newState to state + return newState.signature + } + return null + } + + private fun getLaudedType(): UtType? { + if (statistic.isEmpty()) return null + val sum = statistic.values.sum() + val weights = statistic.values.map { it.toDouble() / sum } + val newType = weightedRandom(statistic.keys.toList(), weights, random) + logger.info("Lauded type: ${newType.pythonTypeRepresentation()}") + return newType + } + + fun expandState(): UtType? { + if (states.isEmpty()) return null + + logger.debug("State number: ${states.size}") + iterationCounter++ + + if (randomTypeFrequency > 0 && iterationCounter % randomTypeFrequency == 0) { + randomTypeCounter++ + if (randomTypeCounter % 2 == 0) { + val laudedType = getLaudedType() + if (laudedType != null) return laudedType + } + val isNDArray = pythonMethod.method.methodType.arguments.any{ it.pythonTypeName() == "numpy.ndarray" } + val randomType = if (!isNDArray) { getRandomType() } else { null } + if (randomType != null) return randomType + } + + val state = chooseState(states) + val newState = expandState(state, storage) + if (newState != null) { + logger.info("Checking new state ${newState.signature.pythonTypeRepresentation()}") + if (checkSignature(newState.signature as FunctionType, newState.additionalVars, fileForMypyRuns, configFile)) { + logger.debug("Found new state!") + expandedStates[newState.signature] = newState to state + return newState.signature + } + } else if (state.anyNodes.isEmpty()) { + if (state.signature in checkedSignatures) { + logger.debug("Good type ${state.signature.pythonTypeRepresentation()}") + return state.signature + } + logger.debug("Checking ${state.signature.pythonTypeRepresentation()}") + if (checkSignature(state.signature as FunctionType, state.additionalVars, fileForMypyRuns, configFile)) { + logger.debug("${state.signature.pythonTypeRepresentation()} is good") + checkedSignatures.add(state.signature) + return state.signature + } else { + states.remove(state) + } + } else { + logger.debug("Remove ${state.signature.pythonTypeRepresentation()} because of any nodes") + states.remove(state) + } + return expandState() + } + + fun feedbackState(signature: UtType, feedback: InferredTypeFeedback) { + val stateInfo = expandedStates[signature] + val lauded = statistic[signature] != 0 + if (stateInfo != null) { + val (newState, parent) = stateInfo + when { + feedback is SuccessFeedback || lauded -> { + states.add(newState) + parent.children += 1 + } + + feedback is InvalidTypeFeedback -> { + states.remove(newState) + } + } + expandedStates.remove(signature) + } else if (feedback is InvalidTypeFeedback && !lauded) { + initialStates.forEach { + if (typesAreEqual(signature, it.signature)) { + states.remove(it) + } + } + } + } + + + override suspend fun run( + isCancelled: () -> Boolean, + annotationHandler: suspend (UtType) -> InferredTypeFeedback, + ): Int { + run breaking@ { + while (states.isNotEmpty()) { + if (isCancelled()) + return@breaking + logger.debug("State number: ${states.size}") + iterationCounter++ + + if (randomTypeFrequency > 0 && iterationCounter % randomTypeFrequency == 0) { + val weights = states.map { 1.0 / (it.anyNodes.size * it.anyNodes.size + 1) } + val state = weightedRandom(states, weights, random) + val newState = expandState(state, storage, state.anyNodes.map { mixtureType }) + if (newState != null) { + logger.info("Random type: ${newState.signature.pythonTypeRepresentation()}") + annotationHandler(newState.signature) + } + } + + val state = chooseState(states) + val newState = expandState(state, storage) + if (newState != null) { + if (iterationCounter == 1) { + annotationHandler(initialState.signature) + } + logger.info("Checking ${newState.signature.pythonTypeRepresentation()}") + if (checkSignature(newState.signature as FunctionType, newState.additionalVars, fileForMypyRuns, configFile)) { + logger.debug("Found new state!") + when (annotationHandler(newState.signature)) { + SuccessFeedback -> { + states.add(newState) + state.children += 1 + } + InvalidTypeFeedback -> {} + } + } + } else { + states.remove(state) + } + } + } + return iterationCounter + } + + private fun checkSignature(signature: FunctionType, newAdditionalVars: String, fileForMypyRuns: File, configFile: File): Boolean { + val methodCopy = initialPythonMethod.makeCopyWithNewType(signature) + return checkSuggestedSignatureWithDMypy( + methodCopy, + directoriesForSysPath, + moduleToImport, + namesInModule, + fileForMypyRuns, + pythonPath, + configFile, + initialErrorNumber, + newAdditionalVars, + timeout = dMypyTimeout + ) + } + + private fun chooseState(states: List): BaselineAlgorithmState { + val weights = states.map { 1.0 / (it.children * it.children + 1) } + return weightedRandom(states, weights, random) + } + + private fun getInitialState( + hintCollectorResult: HintCollectorResult, + generalRating: List, + paramNames: List, + methodType: FunctionType, + additionalVars: String = "", + ): BaselineAlgorithmState { + val root = PartialTypeNode(methodType, true) + val allNodes: MutableSet = mutableSetOf(root) + val argumentRootNodes = paramNames.map { hintCollectorResult.parameterToNode[it]!! } + argumentRootNodes.forEachIndexed { index, node -> + val visited: MutableSet = mutableSetOf() + visitNodesByReverseEdges(node, visited) { it is HintEdgeWithBound && EDGES_TO_LINK.contains(it.source) } + val (lowerBounds, upperBounds) = collectBoundsFromComponent(visited) + val decomposed = decompose(node.partialType, lowerBounds, upperBounds, 1, storage) + allNodes.addAll(decomposed.nodes) + val edge = BaselineAlgorithmEdge( + from = decomposed.root, + to = root, + annotationParameterId = index + ) + addEdge(edge) + } + return BaselineAlgorithmState(allNodes, generalRating, storage, additionalVars) + } + + fun laudType(type: FunctionType) { + statistic[type] = statistic[type]?.plus(1) ?: 1 + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/StateExpansion.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/StateExpansion.kt new file mode 100644 index 0000000000..33f364152a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/StateExpansion.kt @@ -0,0 +1,88 @@ +package org.utbot.python.newtyping.inference.baseline + +import org.utpython.types.PythonTypeHintsStorage +import org.utpython.types.general.UtType +import org.utpython.types.inference.addEdge +import org.utpython.types.pythonAnnotationParameters +import org.utpython.types.pythonDescription +import java.util.* + +fun expandState(state: BaselineAlgorithmState, typeStorage: PythonTypeHintsStorage): BaselineAlgorithmState? { + if (state.anyNodes.isEmpty()) + return null + val types = state.candidateGraph.getNext() ?: return null + return expandState(state, typeStorage, types) +} + +fun expandState(state: BaselineAlgorithmState, typeStorage: PythonTypeHintsStorage, types: List): BaselineAlgorithmState? { + if (types.isEmpty()) + return null + val substitution = (state.anyNodes zip types).associate { it } + return expandNodes(state, substitution, state.generalRating, typeStorage) +} + +private fun expandNodes( + state: BaselineAlgorithmState, + substitution: Map, + generalRating: List, + storage: PythonTypeHintsStorage +): BaselineAlgorithmState { + val (newAnyNodeMap, allNewNodes) = substitution.entries.fold( + Pair(emptyMap(), emptySet()) + ) { (map, allNodeSet), (node, newType) -> + val (newNodeForAny, additionalNodes) = + decompose(newType, node.lowerBounds, node.upperBounds, node.nestedLevel, storage) + Pair(map + (node to newNodeForAny), allNodeSet + additionalNodes) + } + val newNodeMap = expansionBFS(substitution, newAnyNodeMap).toMutableMap() + state.nodes.forEach { cur -> newNodeMap[cur] = newNodeMap[cur] ?: cur.copy() } + state.nodes.forEach { cur -> + val new = newNodeMap[cur]!! + cur.outgoingEdges.forEach { edge -> + val newEdge = BaselineAlgorithmEdge( + from = new, + to = newNodeMap[edge.to]!!, + annotationParameterId = edge.annotationParameterId + ) + addEdge(newEdge) + } + } + return BaselineAlgorithmState(newNodeMap.values.toSet() + allNewNodes, generalRating, storage, state.additionalVars) +} + +private fun expansionBFS( + substitution: Map, + nodeMap: Map +): Map { + val queue: Queue> = LinkedList(substitution.keys.map { Pair(it, 0) }) + val newParams: MutableMap> = mutableMapOf() + val newNodes: MutableMap = nodeMap.toMutableMap() + val lastModified: MutableMap = mutableMapOf() + var timer = 0 + while (!queue.isEmpty()) { + timer++ + val (oldNode, putTime) = queue.remove() + if ((lastModified[oldNode] ?: 0) != putTime) + continue + if (!newNodes.keys.contains(oldNode)) { + val oldType = oldNode.partialType + val newType = if (substitution.keys.contains(oldNode)) + substitution[oldNode]!! + else + oldType.pythonDescription().createTypeWithNewAnnotationParameters( + oldType, + newParams[oldNode] ?: oldType.pythonAnnotationParameters() + ) + newNodes[oldNode] = PartialTypeNode(newType, oldNode.isRoot) + } + val newType = newNodes[oldNode]!!.partialType + oldNode.outgoingEdges.forEach { edge -> + val params = newParams[edge.to] ?: edge.to.partialType.pythonAnnotationParameters().toMutableList() + params[edge.annotationParameterId] = newType + newParams[edge.to] = params + lastModified[edge.to] = timer + queue.add(Pair(edge.to, timer)) + } + } + return newNodes +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/Structures.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/Structures.kt new file mode 100644 index 0000000000..4a01e45a09 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/Structures.kt @@ -0,0 +1,42 @@ +package org.utbot.python.newtyping.inference.baseline + +import org.utpython.types.PythonTypeHintsStorage +import org.utpython.types.general.UtType +import org.utpython.types.inference.TypeInferenceEdgeWithValue +import org.utpython.types.inference.TypeInferenceNode +import org.utpython.types.pythonAnyType + +sealed class BaselineAlgorithmNode(val isRoot: Boolean) : TypeInferenceNode { + override val ingoingEdges: MutableList = mutableListOf() + override val outgoingEdges: MutableList = mutableListOf() + abstract fun copy(): BaselineAlgorithmNode +} + +class PartialTypeNode(override val partialType: UtType, isRoot: Boolean) : BaselineAlgorithmNode(isRoot) { + override fun copy(): BaselineAlgorithmNode = PartialTypeNode(partialType, isRoot) +} + +class AnyTypeNode(val lowerBounds: List, val upperBounds: List, val nestedLevel: Int) : + BaselineAlgorithmNode(false) { + override val partialType: UtType = pythonAnyType + override fun copy(): BaselineAlgorithmNode = AnyTypeNode(lowerBounds, upperBounds, nestedLevel) +} + +class BaselineAlgorithmEdge( + override val from: BaselineAlgorithmNode, + override val to: BaselineAlgorithmNode, + override val annotationParameterId: Int +) : TypeInferenceEdgeWithValue + +class BaselineAlgorithmState( + val nodes: Set, + val generalRating: List, + typeStorage: PythonTypeHintsStorage, + val additionalVars: String = "", +) { + val signature: UtType + get() = nodes.find { it.isRoot }!!.partialType + val anyNodes: List = nodes.mapNotNull { it as? AnyTypeNode } + val candidateGraph = CandidateGraph(anyNodes, generalRating, typeStorage) + var children: Int = 0 +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/TypeCandidateGeneration.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/TypeCandidateGeneration.kt new file mode 100644 index 0000000000..9bd132b357 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/TypeCandidateGeneration.kt @@ -0,0 +1,195 @@ +package org.utbot.python.newtyping.inference.baseline + +import mu.KotlinLogging +import org.utpython.types.* +import org.utbot.python.newtyping.ast.visitor.hints.HintCollectorResult +import org.utpython.types.general.DefaultSubstitutionProvider +import org.utpython.types.general.UtType +import org.utpython.types.general.getBoundedParameters +import org.utpython.types.general.getOrigin +import org.utbot.python.newtyping.inference.collectBoundsFromEdges +import org.utpython.types.PythonTypeHintsStorage + +private val logger = KotlinLogging.logger {} + +class TypeRating(scores: List>) { + val typesInOrder: List> = scores.map { Pair(it.second, it.first) }.sortedBy { -it.first } + val types: List = typesInOrder.map { it.second } +} + +data class TypeRatingPosition( + val typeRating: TypeRating, + val pos: Int +) { + val hasNext: Boolean + get() = pos < typeRating.types.size - 1 + val curPenalty: Double + get() = typeRating.typesInOrder.first().first - typeRating.typesInOrder[pos].first + val type: UtType + get() = typeRating.types[pos] + + fun makeMove(): TypeRatingPosition { + assert(hasNext) + return TypeRatingPosition(typeRating, pos + 1) + } +} + +// TODO: memory usage can be reduced +class CandidateGraph( + anyNodes: List, + initialRating: List, + storage: PythonTypeHintsStorage, +) { + private val typeRatings: List = + anyNodes.map { createTypeRating(initialRating, it.lowerBounds, it.upperBounds, storage, it.nestedLevel) } + private val vertices: MutableList = + mutableListOf(CandidateGraphVertex(typeRatings.map { TypeRatingPosition(it, 0) })) + private var lastUsed: Int = -1 + private var lastExpanded: Int = -1 + + fun getNext(): List? { + if (lastUsed < lastExpanded) { + lastUsed += 1 + return vertices[lastUsed].types + } + if (lastExpanded == vertices.size - 1) + return null + + lastExpanded += 1 + vertices[lastExpanded].generateSuccessors().forEach(::insertVertexIfNotAlready) + + return getNext() + } + + private fun insertVertexIfNotAlready(vertex: CandidateGraphVertex) { + for (i in 0 until vertices.size) { + if (vertices[i] == vertex) + return + if (vertices[i].penalty > vertex.penalty) { + vertices.add(i, vertex) + return + } + } + vertices.add(vertex) + } + + init { + logger.debug("Created new CandidateGraph") + } +} + +data class CandidateGraphVertex(val positions: List) { + val penalty by lazy { + positions.fold(0.0) { acc, typeRatingPosition -> acc + typeRatingPosition.curPenalty } + } + + fun generateSuccessors(): List = + positions.mapNotNull { pos -> + CandidateGraphVertex( + positions.map { + if (it == pos) { + if (!pos.hasNext) + return@mapNotNull null + pos.makeMove() + } else + it + } + ) + } + + val types by lazy { + positions.map { it.type } + } +} + +private const val MAX_NESTING = 3 + +private fun changeScores( + initialRating: List, + storage: PythonTypeHintsStorage, + bounds: List, + hintScores: MutableMap, + withPenalty: Boolean, + isUpper: Boolean +) { + bounds.forEach { constraint -> + val (fitting, notFitting) = initialRating.partition { typeFromList -> + val type = DefaultSubstitutionProvider.substitute( + typeFromList, + typeFromList.getBoundedParameters().associateWith { pythonAnyType } + ) + if (isUpper) + PythonSubtypeChecker.checkIfRightIsSubtypeOfLeft(constraint, type, storage) + else + PythonSubtypeChecker.checkIfRightIsSubtypeOfLeft(type, constraint, storage) + } + if (withPenalty) + notFitting.forEach { + val wrapper = PythonTypeWrapperForEqualityCheck(it) + hintScores[wrapper] = (hintScores[wrapper] ?: 0.0) - 1 + } + fitting.forEach { + val wrapper = PythonTypeWrapperForEqualityCheck(it) + hintScores[wrapper] = (hintScores[wrapper] ?: 0.0) + 1.0 / fitting.size + } + } +} + +fun createTypeRating( + initialRating: List, + lowerBounds: List, + upperBounds: List, + storage: PythonTypeHintsStorage, + level: Int, + withPenalty: Boolean = true +): TypeRating { + val hintScores = mutableMapOf() + changeScores(initialRating, storage, lowerBounds, hintScores, withPenalty, isUpper = false) + changeScores(initialRating, storage, upperBounds, hintScores, withPenalty, isUpper = true) + val scores: List> = initialRating.mapNotNull { typeFromList -> + if (level == MAX_NESTING && typeFromList.getBoundedParameters().isNotEmpty()) + return@mapNotNull null + val type = DefaultSubstitutionProvider.substitute( + typeFromList, + typeFromList.getBoundedParameters().associateWith { pythonAnyType } + ) + val wrapper = PythonTypeWrapperForEqualityCheck(type) + Pair(type, hintScores[wrapper] ?: 0.0) + } + return TypeRating(scores) +} + +fun simplestTypes(storage: PythonTypeHintsStorage): List { + val int = storage.pythonInt + val listOfAny = DefaultSubstitutionProvider.substituteAll(storage.pythonList, listOf(pythonAnyType)) + val str = storage.pythonStr + val bool = storage.pythonBool + val float = storage.pythonFloat + val dictOfAny = DefaultSubstitutionProvider.substituteAll(storage.pythonDict, listOf(pythonAnyType, pythonAnyType)) + return listOf(int, listOfAny, str, bool, float, dictOfAny) +} + +fun createGeneralTypeRating(hintCollectorResult: HintCollectorResult, storage: PythonTypeHintsStorage): List { + val allLowerBounds: MutableList = mutableListOf() + val allUpperBounds: MutableList = mutableListOf() + hintCollectorResult.allNodes.forEach { node -> + val (lowerFromEdges, upperFromEdges) = collectBoundsFromEdges(node) + allLowerBounds.addAll((lowerFromEdges + node.lowerBounds + node.partialType).filter { + !typesAreEqual(it, pythonAnyType) + }) + allUpperBounds.addAll((upperFromEdges + node.upperBounds + node.partialType).filter { + !typesAreEqual(it, pythonAnyType) + }) + } + val prefix = simplestTypes(storage) + val rating = createTypeRating( + storage.simpleTypes.filter { !prefix.any { type -> typesAreEqual(type.getOrigin(), it) } }, + allLowerBounds, + allUpperBounds, + storage, + 1, + withPenalty = false + ) + val prefixRating = createTypeRating(prefix, allLowerBounds, allUpperBounds, storage, 1, withPenalty = false) + return prefixRating.types + rating.types +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/TypeDecomposition.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/TypeDecomposition.kt new file mode 100644 index 0000000000..90e049ba7f --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/baseline/TypeDecomposition.kt @@ -0,0 +1,70 @@ +package org.utbot.python.newtyping.inference.baseline + +import org.utpython.types.* +import org.utpython.types.general.UtType +import org.utpython.types.inference.addEdge + +data class DecompositionResult( + val root: BaselineAlgorithmNode, + val nodes: Set +) + +fun decompose( + partialType: UtType, + lowerBounds: List, + upperBounds: List, + level: Int, + storage: PythonTypeHintsStorage +): DecompositionResult { + if (typesAreEqual(partialType, pythonAnyType)) { + val root = AnyTypeNode( + lowerBounds.filter { !typesAreEqual(it, pythonAnyType) }, + upperBounds.filter { !typesAreEqual(it, pythonAnyType) }, + level + ) + return DecompositionResult(root, setOf(root)) + } + val newNodes: MutableSet = mutableSetOf() + val root = PartialTypeNode(partialType, false) + newNodes.add(root) + val children = partialType.pythonAnnotationParameters() + val constraints: Map> = + List(children.size) { it }.associateWith { mutableListOf() } + lowerBounds.forEach { boundType -> + val cur = propagateConstraint(partialType, TypeConstraint(boundType, ConstraintKind.LowerBound), storage) + cur.forEach { constraints[it.key]!!.add(it.value) } + } + upperBounds.forEach { boundType -> + val cur = propagateConstraint(partialType, TypeConstraint(boundType, ConstraintKind.UpperBound), storage) + cur.forEach { constraints[it.key]!!.add(it.value) } + } + constraints.forEach { (index, constraintList) -> + val childLowerBounds: MutableList = mutableListOf() + val childUpperBounds: MutableList = mutableListOf() + constraintList.forEach { constraint -> + when (constraint.kind) { + ConstraintKind.LowerBound -> childLowerBounds.add(constraint.type) + ConstraintKind.UpperBound -> childUpperBounds.add(constraint.type) + ConstraintKind.BothSided -> { + childLowerBounds.add(constraint.type) + childUpperBounds.add(constraint.type) + } + } + } + val (childBaselineAlgorithmNode, nodes) = decompose( + children[index], + childLowerBounds, + childUpperBounds, + level + 1, + storage + ) + newNodes.addAll(nodes) + val edge = BaselineAlgorithmEdge( + from = childBaselineAlgorithmNode, + to = root, + annotationParameterId = index + ) + addEdge(edge) + } + return DecompositionResult(root, newNodes) +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/test.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/test.kt new file mode 100644 index 0000000000..83d9df08cd --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/inference/test.kt @@ -0,0 +1,16 @@ +package org.utbot.python.newtyping.inference + +import org.utpython.types.pythonTypeRepresentation + +fun main() { + TypeInferenceProcessor( + "python3.9", + directoriesForSysPath = setOf("/home/tochilinak/Documents/projects/utbot/UTBotJava/utbot-python/samples"), + "/home/tochilinak/Documents/projects/utbot/UTBotJava/utbot-python/samples/easy_samples/generics.py", + moduleOfSourceFile = "easy_samples.generics", + "set", + className = "LoggedVar" + ).inferTypes(cancel = { false }).forEach { + println(it.pythonTypeRepresentation()) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/RunDMypy.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/RunDMypy.kt new file mode 100644 index 0000000000..d9320d1364 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/RunDMypy.kt @@ -0,0 +1,59 @@ +package org.utbot.python.newtyping.mypy + +import mu.KotlinLogging +import org.utbot.python.PythonMethod +import org.utbot.python.code.PythonCodeGenerator.generateMypyCheckCode +import org.utbot.python.utils.TemporaryFileManager +import org.utpython.types.mypy.getErrorNumber +import org.utpython.types.mypy.getErrorsAndNotes +import org.utbot.python.utils.runCommand +import java.io.File + +private val logger = KotlinLogging.logger {} + +fun checkWithDMypy(pythonPath: String, fileWithCodePath: String, configFile: File, timeout: Long? = null): String? { + val result = runCommand( + listOf( + pythonPath, + "-m", + "mypy.dmypy", + "run", + "--", + fileWithCodePath, + "--config-file", + configFile.path + ), + timeout = timeout + ) + if (result.terminatedByTimeout) + return null + return result.stdout +} + +fun checkSuggestedSignatureWithDMypy( + method: PythonMethod, + directoriesForSysPath: Set, + moduleToImport: String, + namesInModule: Collection, + fileForMypyCode: File, + pythonPath: String, + configFile: File, + initialErrorNumber: Int, + additionalVars: String, + timeout: Long? = null +): Boolean { + val annotationMap = + (method.argumentsNames zip method.methodType.arguments).associate { + Pair(it.first, it.second) + } + val mypyCode = generateMypyCheckCode(method, annotationMap, directoriesForSysPath, moduleToImport, namesInModule, additionalVars) + // logger.debug(mypyCode) + TemporaryFileManager.writeToAssignedFile(fileForMypyCode, mypyCode) + val mypyOutput = checkWithDMypy(pythonPath, fileForMypyCode.canonicalPath, configFile, timeout = timeout) + ?: return true + val report = getErrorsAndNotes(mypyOutput) + val errorNumber = getErrorNumber(report, fileForMypyCode.canonicalPath, 0, mypyCode.length) + if (errorNumber > initialErrorNumber) + logger.debug(mypyOutput) + return errorNumber <= initialErrorNumber +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/Utils.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/Utils.kt new file mode 100644 index 0000000000..96ced37886 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/Utils.kt @@ -0,0 +1,11 @@ +package org.utbot.python.newtyping.mypy + +/** + * Remove .__init__ suffix if it exists. +It resolves problem with duplication module names in mypy, e.g. mod.__init__ and mod + */ +fun String.dropInitFile(): String { + return if (this.endsWith(".__init__")) { + this.dropLast(9) + } else this +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/utils/Utils.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/utils/Utils.kt new file mode 100644 index 0000000000..ce9da99af0 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/utils/Utils.kt @@ -0,0 +1,10 @@ +package org.utbot.python.newtyping.utils + +import org.utbot.fuzzing.utils.chooseOne +import kotlin.random.Random + + +fun weightedRandom(elems: List, weights: List, random: Random): T { + val index = random.chooseOne(weights.toDoubleArray()) + return elems[index] +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt new file mode 100644 index 0000000000..cc29c36562 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt @@ -0,0 +1,22 @@ +package org.utbot.python.utils + +object Cleaner { + private var clean: () -> Unit = {} + + fun addFunction(f: () -> Unit) { + val oldClean = clean + val newClean = { + f() + oldClean() + } + clean = newClean + } + + fun restart() { + clean = {} + } + + fun doCleaning() { + clean() + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/FindCurrentPythonModule.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/FindCurrentPythonModule.kt new file mode 100644 index 0000000000..7852d3de36 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/FindCurrentPythonModule.kt @@ -0,0 +1,18 @@ +package org.utbot.python.utils + +import java.io.File + +fun findCurrentPythonModule( + directoriesForSysPath: Collection, + sourceFile: String +): Optional { + directoriesForSysPath.forEach { path -> + val module = getModuleName(path.toAbsolutePath(), sourceFile.toAbsolutePath()) + if (module != null) + return Success(module) + } + return Fail("Couldn't find path for $sourceFile in --sys-path option. Please, specify it.") +} + +fun String.toAbsolutePath(): String = + File(this).canonicalPath diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/Optional.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/Optional.kt new file mode 100644 index 0000000000..40e5330760 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/Optional.kt @@ -0,0 +1,25 @@ +package org.utbot.python.utils + +sealed class Optional +class Fail(val message: String) : Optional() +class Success(val value: A) : Optional() + +fun bind( + value: Optional, + f: (A) -> Optional +): Optional = + when (value) { + is Fail -> Fail(value.message) + is Success -> f(value.value) + } + +fun pack(vararg values: Optional): Optional> { + val result = mutableListOf() + for (elem in values) { + when (elem) { + is Fail -> return Fail(elem.message) + is Success -> result.add(elem.value) + } + } + return Success(result) +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt new file mode 100644 index 0000000000..5cd6ca501f --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt @@ -0,0 +1,41 @@ +package org.utbot.python.utils + +import java.lang.Integer.min + +class PriorityCartesianProduct(private val lists: List>) { + + private fun generateFixedSumRepresentation( + sum: Int, + index: Int = 0, + curRepr: List = emptyList() + ): Sequence> { + val itemNumber = lists.size + var result = emptySequence>() + if (index == itemNumber && sum == 0) { + return sequenceOf(curRepr) + } else if (index < itemNumber && sum >= 0) { + for (i in 0..min(sum, lists[index].size - 1)) { + result += generateFixedSumRepresentation( + sum - i, + index + 1, + curRepr + listOf(i) + ) + } + } + return result + } + + fun getSequence(): Sequence> { + var curSum = 0 + val maxSum = lists.fold(0) { acc, elem -> acc + elem.size } + val combinations = generateSequence { + if (curSum > maxSum) + null + else + generateFixedSumRepresentation(curSum++) + } + return combinations.flatten().map { combination: List -> + combination.mapIndexed { element, value -> lists[element][value] } + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt new file mode 100644 index 0000000000..379cfc0f7d --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt @@ -0,0 +1,50 @@ +package org.utbot.python.utils + +import java.io.BufferedReader +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit + +data class CmdResult( + val stdout: String, + val stderr: String, + val exitValue: Int, + val terminatedByTimeout: Boolean = false +) + +fun startProcess( + command: List, + environmentVariables: Map = emptyMap() +): Process { + val pb = ProcessBuilder(command) + val env = pb.environment() + env += environmentVariables + return pb.start() +} + +fun getResult(process: Process, timeout: Long? = null): CmdResult { + if (timeout != null) { + if (!process.waitFor(timeout, TimeUnit.MILLISECONDS)) { + process.destroy() + return CmdResult("", "", 1, terminatedByTimeout = true) + } + } + + val reader = BufferedReader(InputStreamReader(process.inputStream)) + var stdout = "" + var line: String? = "" + while (line != null) { + stdout += "$line\n" + line = reader.readLine() + } + + if (timeout == null) + process.waitFor() + + val stderr = process.errorStream.readBytes().decodeToString().trimIndent() + return CmdResult(stdout.trimIndent(), stderr, process.exitValue()) +} + +fun runCommand(command: List, timeout: Long? = null, environmentVariables: Map = emptyMap()): CmdResult { + val process = startProcess(command, environmentVariables) + return getResult(process, timeout) +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/PythonVersionChecker.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/PythonVersionChecker.kt new file mode 100644 index 0000000000..a0e81e7a07 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/PythonVersionChecker.kt @@ -0,0 +1,22 @@ +package org.utbot.python.utils + +object PythonVersionChecker { + private val minimalPythonVersion = Triple(3, 10, 0) + + fun checkPythonVersion(pythonPath: String): Boolean { + try { + val version = runCommand(listOf( + pythonPath, + "-c", + "\"import sys; print('.'.join(map(str, sys.version_info[:3])))\"" + )) + if (version.exitValue == 0) { + val (major, minor, patch) = version.stdout.split(".") + return (major.toInt() >= minimalPythonVersion.first && minor.toInt() >= minimalPythonVersion.second && patch.toInt() >= minimalPythonVersion.third) + } + return false + } catch (_: Exception) { + return false + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsInstaller.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsInstaller.kt new file mode 100644 index 0000000000..2aace97dec --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsInstaller.kt @@ -0,0 +1,18 @@ +package org.utbot.python.utils + +interface RequirementsInstaller { + fun checkRequirements(pythonPath: String, requirements: List): Boolean + fun installRequirements(pythonPath: String, requirements: List) + + companion object { + fun checkRequirements(requirementsInstaller: RequirementsInstaller, pythonPath: String, additionalRequirements: List): Boolean { + val requirements = RequirementsUtils.requirements + additionalRequirements + if (!requirementsInstaller.checkRequirements(pythonPath, requirements)) { + requirementsInstaller.installRequirements(pythonPath, requirements) + return requirementsInstaller.checkRequirements(pythonPath, requirements) + } + return true + } + + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt new file mode 100644 index 0000000000..2e955a5b92 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt @@ -0,0 +1,62 @@ +package org.utbot.python.utils + +import org.utbot.python.UtbotExecutor +import org.utpython.types.mypy.MypyInfoBuild + +object RequirementsUtils { + private val utbotMypyRunnerVersion = // local_pip_setup should be used only for debugging + RequirementsUtils::class.java.getResource("/local_pip_setup/local_utbot_mypy_version")?.readText() + ?: MypyInfoBuild::class.java.getResource("/utbot_mypy_runner_version")!!.readText() + private val utbotExecutorVersion = + UtbotExecutor::class.java.getResource("/utbot_executor_version")!!.readText() + private val useLocalPythonPackages = // "true" must be set only for debugging + this::class.java.getResource("/local_pip_setup/use_local_python_packages")?.readText()?.toBoolean() ?: false + private val localMypyRunnerPath = + this::class.java.getResource("/local_pip_setup/local_utbot_mypy_path")?.readText() + private val localExecutorPath = + this::class.java.getResource("/local_pip_setup/local_utbot_executor_path")?.readText() + private val pipFindLinks: List = + if (useLocalPythonPackages) listOfNotNull(localMypyRunnerPath, localExecutorPath) else emptyList() + val requirements: List = listOf( + "utbot-mypy-runner==$utbotMypyRunnerVersion", + "utbot-executor==$utbotExecutorVersion", + ) + + private val requirementsScriptContent: String = + RequirementsUtils::class.java.getResource("/check_requirements.py") + ?.readText() + ?: error("Didn't find /check_requirements.py") + + fun requirementsAreInstalled(pythonPath: String): Boolean { + return requirementsAreInstalled(pythonPath, requirements) + } + + fun requirementsAreInstalled(pythonPath: String, requirementList: List): Boolean { + val requirementsScript = + TemporaryFileManager.createTemporaryFile(requirementsScriptContent, tag = "requirements") + val result = runCommand( + listOf( + pythonPath, + requirementsScript.path + ) + requirementList + ) + requirementsScript.delete() + return result.exitValue == 0 + } + + fun installRequirements(pythonPath: String): CmdResult { + return installRequirements(pythonPath, requirements) + } + + fun installRequirements(pythonPath: String, moduleNames: List): CmdResult { + return runCommand( + listOf( + pythonPath, + "-m", + "pip", + "install" + ) + moduleNames, + environmentVariables = mapOf("PIP_FIND_LINKS" to pipFindLinks.joinToString(" ")) + ) + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt new file mode 100644 index 0000000000..2daa636802 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt @@ -0,0 +1,47 @@ +package org.utbot.python.utils + +import org.utbot.common.PathUtil.toPath +import java.io.File +import java.nio.file.Paths + +// numeration from zero +fun getLineNumber(content: String, pos: Int) = + content.substring(0, pos).count { it == '\n' } + +fun getLineOfFunction(code: String, functionName: String? = null): Int? { + val regex = + if (functionName != null) + """(?m)^def +$functionName\(""".toRegex() + else + """(?m)^def""".toRegex() + + val trimmedCode = code.replaceIndent() + return regex.find(trimmedCode)?.range?.first?.let { getLineNumber(trimmedCode, it) } +} + +fun String.camelToSnakeCase(): String { + val camelRegex = "(?<=[a-zA-Z])[\\dA-Z]".toRegex() + return camelRegex.replace(this) { + "_${it.value}" + }.lowercase() +} + +fun moduleOfType(typeName: String): String? { + val lastIndex = typeName.lastIndexOf('.') + return if (lastIndex == -1) null else typeName.substring(0, lastIndex) +} + +fun checkIfFileLiesInPath(path: String, fileWithClassPath: String): Boolean { + val parentPath = Paths.get(path).toAbsolutePath() + val childPath = Paths.get(fileWithClassPath).toAbsolutePath() + return childPath.startsWith(parentPath) +} + +fun getModuleNameWithoutCheck(path: File, fileWithClass: File): String = + path.toURI().relativize(fileWithClass.toURI()).path.removeSuffix(".py").toPath().joinToString(".") + +fun getModuleName(path: String, fileWithClassPath: String): String? { + if (checkIfFileLiesInPath(path, fileWithClassPath)) + return getModuleNameWithoutCheck(File(path), File(fileWithClassPath)) + return null +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt new file mode 100644 index 0000000000..7dcc6c382f --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt @@ -0,0 +1,47 @@ +package org.utbot.python.utils + +import org.utbot.common.FileUtil +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +object TemporaryFileManager { + private var tmpDirectory: Path + private var nextId = 0 + + init { + tmpDirectory = initialize() + } + + fun initialize(): Path { + tmpDirectory = FileUtil.createTempDirectory("python-test-generation-${nextId++}") + Cleaner.addFunction { tmpDirectory.toFile().deleteRecursively() } + return tmpDirectory + } + + fun assignTemporaryFile(fileName_: String? = null, tag: String? = null, addToCleaner: Boolean = true): File { + val fileName = fileName_ ?: ("tmp_${nextId++}_" + (tag ?: "")) + val fullpath = Paths.get(tmpDirectory.toString(), fileName) + val result = fullpath.toFile() + if (addToCleaner) + Cleaner.addFunction { result.delete() } + return result + } + + fun writeToAssignedFile(file: File, content: String) { + file.writeText(content) + file.parentFile?.mkdirs() + file.createNewFile() + } + + fun createTemporaryFile( + content: String, + fileName: String? = null, + tag: String? = null, + addToCleaner: Boolean = true + ): File { + val file = assignTemporaryFile(fileName, tag, addToCleaner) + writeToAssignedFile(file, content) + return file + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/TestGenerationLimitManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/TestGenerationLimitManager.kt new file mode 100644 index 0000000000..a1b89262de --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/TestGenerationLimitManager.kt @@ -0,0 +1,89 @@ +package org.utbot.python.utils + +class TestGenerationLimitManager( + // global settings + var mode: LimitManagerMode, + val until: Long, + + // local settings: one type inference iteration + var executions: Int = 150, + var invalidExecutions: Int = 10, + var cacheNodeExecutions: Int = 20, + var fakeNodeExecutions: Int = 40, + var missedLines: Int? = null, + val isRootManager: Boolean = false, +) { + private val initExecution = executions + private val initInvalidExecutions = invalidExecutions + private val initCacheNodeExecutions = cacheNodeExecutions + private val initFakeNodeExecutions = fakeNodeExecutions + private val initMissedLines = missedLines + + fun restart() { + executions = initExecution + invalidExecutions = initInvalidExecutions + cacheNodeExecutions = initCacheNodeExecutions + fakeNodeExecutions = initFakeNodeExecutions + missedLines = initMissedLines + } + + fun addSuccessExecution() { + executions -= 1 + } + + fun addInvalidExecution() { + invalidExecutions -= 1 + } + + fun addFakeNodeExecutions() { + fakeNodeExecutions -= 1 + } + + fun restartFakeNode() { + fakeNodeExecutions = initFakeNodeExecutions + } + + fun isCancelled(): Boolean { + return mode.isCancelled(this) + } +} + +interface LimitManagerMode { + fun isCancelled(manager: TestGenerationLimitManager): Boolean +} + +object MaxCoverageMode : LimitManagerMode { + override fun isCancelled(manager: TestGenerationLimitManager): Boolean { + return manager.missedLines?.equals(0) == true + } +} + +object TimeoutMode : LimitManagerMode { + override fun isCancelled(manager: TestGenerationLimitManager): Boolean { + return System.currentTimeMillis() >= manager.until + } +} + +object ExecutionMode : LimitManagerMode { + override fun isCancelled(manager: TestGenerationLimitManager): Boolean { + return manager.invalidExecutions <= 0 || manager.executions <= 0 || manager.fakeNodeExecutions <= 0 || manager.cacheNodeExecutions <= 0 + } +} + +object MaxCoverageWithTimeoutMode : LimitManagerMode { + override fun isCancelled(manager: TestGenerationLimitManager): Boolean { + return MaxCoverageMode.isCancelled(manager) || TimeoutMode.isCancelled(manager) + } +} + +object ExecutionWithTimeoutMode : LimitManagerMode { + override fun isCancelled(manager: TestGenerationLimitManager): Boolean { + return ExecutionMode.isCancelled(manager) || TimeoutMode.isCancelled(manager) + } +} + +object FakeWithTimeoutMode : LimitManagerMode { + override fun isCancelled(manager: TestGenerationLimitManager): Boolean { + return manager.fakeNodeExecutions <= 0 || TimeoutMode.isCancelled(manager) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/TimeoutUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/TimeoutUtils.kt new file mode 100644 index 0000000000..8bcb1b796a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/TimeoutUtils.kt @@ -0,0 +1,31 @@ +package org.utbot.python.utils + +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.max + +fun separateUntil(until: Long, currentIndex: Int, itemsCount: Int): Long { + return when (val itemsLeft = itemsCount - currentIndex) { + 0 -> 0 + 1 -> until + else -> { + val now = System.currentTimeMillis() + max((until - now) / itemsLeft + now, 0) + } + } +} + +fun separateTimeout(timeout: Long, itemsCount: Int): Long { + return when (itemsCount) { + 0 -> 0 + else -> { + timeout / itemsCount + } + } +} + +fun Long.convertToTime(): String { + val date = Date(this) + val format = SimpleDateFormat("HH:mm:ss.SSS") + return format.format(date) +} diff --git a/utbot-python/src/main/resources/check_requirements.py b/utbot-python/src/main/resources/check_requirements.py new file mode 100644 index 0000000000..e1b466d627 --- /dev/null +++ b/utbot-python/src/main/resources/check_requirements.py @@ -0,0 +1,7 @@ +import pkg_resources +import sys + + +if __name__ == "__main__": + dependencies = sys.argv[1:] + pkg_resources.require(dependencies) diff --git a/utbot-python/src/main/resources/example_code/__init__.py b/utbot-python/src/main/resources/example_code/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/src/main/resources/example_code/arithmetic.py b/utbot-python/src/main/resources/example_code/arithmetic.py new file mode 100644 index 0000000000..6f26aa94f4 --- /dev/null +++ b/utbot-python/src/main/resources/example_code/arithmetic.py @@ -0,0 +1,17 @@ +import math + + +def calculate_function_value(x, y): + """ + Calculate value `f` + | sqrt(x - 2y) , x > 100 + f(x, y) = | (3x^2 - 2xy + y^2) / sin(x) , -100 < x <= 100 + | (0.01 * x) ^ log2(y) , x < -100 + """ + + if x > 100: + return math.sqrt(x - 2 * y) + elif -100 < x <= 100: + return (3*x**2 - 2*x*y + y**2) / math.sin(x) + else: + return (0.01 * x) ** math.log2(y) diff --git a/utbot-python/src/main/resources/example_code/inner_dir/__init__.py b/utbot-python/src/main/resources/example_code/inner_dir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/src/main/resources/example_code/inner_dir/inner_module.py b/utbot-python/src/main/resources/example_code/inner_dir/inner_module.py new file mode 100644 index 0000000000..8ab929bcd8 --- /dev/null +++ b/utbot-python/src/main/resources/example_code/inner_dir/inner_module.py @@ -0,0 +1,8 @@ +class InnerClass: + x: int + + def __init__(self, x: int): + self.x = x + + def f(self, y: int): + return y**2 + self.x*y + 1 diff --git a/utbot-python/src/main/resources/preprocessed_values.json b/utbot-python/src/main/resources/preprocessed_values.json new file mode 100644 index 0000000000..19f47bdf4d --- /dev/null +++ b/utbot-python/src/main/resources/preprocessed_values.json @@ -0,0 +1,429 @@ +[ + { + "name": "builtins.float", + "instances": [ + "float('-inf')", + "float('inf')", + "float('nan')" + ] + }, + { + "name": "builtins.BaseException", + "instances": [ + "BaseException()" + ] + }, + { + "name": "builtins.object", + "instances": [ + "object()" + ] + }, + { + "name": "builtins.type", + "instances": [ + "type(len)", + "type('123')", + "type(1.0)", + "type({})", + "type('foo', (), {})", + "type('A', (), {'__qualname__': 'B.C'})", + "type(lambda x: x)", + "type((True).real)", + "type(list.append)", + "type('blah', (), {})", + "type(())", + "type('A', (), {})", + "type(list)", + "type({}.items())", + "type({}.values())", + "type('NewClass', (object,), {})", + "type(list.__add__)", + "type(classmethod(lambda c: None))", + "type(range(0))", + "type(None)", + "type(iter(range(0)))", + "type('C', (object,), {'__hash__': None})", + "type(complex('1' * 500))", + "type(staticmethod(lambda : None))", + "type(iter(range(1 << 1000)))", + "type('C', (), {})", + "type({}.keys())" + ] + }, + { + "name": "datetime.date", + "instances": [ + "datetime.date(1, 1, 1)", + "datetime.date(1995, 4, 12)", + "datetime.date(2011, 1, 1)", + "datetime.date(2002, 3, 4)", + "datetime.date(1993, 8, 26)", + "datetime.date(2000, 1, 2)", + "datetime.date(1970, 1, 1)" + ] + }, + { + "name": "datetime.datetime", + "instances": [ + "datetime.datetime(2011, 1, 1)", + "datetime.datetime(1970, 1, 1)", + "datetime.datetime(2015, 4, 5, 1, 45)", + "datetime.datetime(1, 1, 1)", + "datetime.datetime(2014, 11, 2, 1, 30)", + "datetime.datetime(1, 2, 3, 4, 5, 6, 7)", + "datetime.datetime(2002, 4, 7, 2)", + "datetime.datetime(1, 1, 1, fold=1)", + "datetime.datetime(2010, 1, 1)", + "datetime.datetime(2011, 1, 1, 12, 30)", + "datetime.datetime(1993, 8, 26, 22, 12, 55, 99999)", + "datetime.datetime(1, 4, 1, 2)", + "datetime.datetime(1995, 4, 12)", + "datetime.datetime(1, 10, 25, 1)", + "datetime.datetime(10, 10, 10, 10, 10, 10, 10)", + "datetime.datetime(2002, 10, 27, 1)" + ] + }, + { + "name": "datetime.time", + "instances": [ + "datetime.time(fold=1)", + "datetime.time()", + "datetime.time(0, fold=1)", + "datetime.time(22, 12, 55, 99999)", + "datetime.time(0)", + "datetime.time(microsecond=40)", + "datetime.time(18, 45, 3, 1234)", + "datetime.time(12, 0)", + "datetime.time(12, 30)" + ] + }, + { + "name": "datetime.timedelta", + "instances": [ + "datetime.timedelta(days=100, weeks=-7, hours=-24 * (100 - 49), minutes=-3, seconds=12, microseconds=(3 * 60 - 12) * 1000000)", + "datetime.timedelta(hours=24)", + "datetime.timedelta(hours=23, minutes=59)", + "datetime.timedelta(0, 4000, 1)", + "datetime.timedelta(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999)", + "datetime.timedelta(minutes=1440)", + "datetime.timedelta(microseconds=1)", + "datetime.timedelta(minutes=24)", + "datetime.timedelta(minutes=60)", + "datetime.timedelta(weeks=13)", + "datetime.timedelta(minutes=2, seconds=1, microseconds=3)", + "datetime.timedelta(26, 55, 99999)", + "datetime.timedelta(seconds=0.5)", + "datetime.timedelta(minutes=-200)", + "datetime.timedelta(seconds=30)", + "datetime.timedelta(days=-999999999)", + "datetime.timedelta(minutes=-2)", + "datetime.timedelta(minutes=2 * 1439)", + "datetime.timedelta(hours=12, minutes=32, seconds=30)", + "datetime.timedelta(hours=-5)", + "datetime.timedelta(hours=5)", + "datetime.timedelta(42)", + "datetime.timedelta(minutes=23)", + "datetime.timedelta(minutes=3)", + "datetime.timedelta(microseconds=-1)", + "datetime.timedelta(0, 0, 1000)", + "datetime.timedelta(minutes=1)", + "datetime.timedelta(days=365)", + "datetime.timedelta(minutes=0)", + "datetime.timedelta(microseconds=-81)", + "datetime.timedelta(hours=9.5)", + "datetime.timedelta(minutes=2, seconds=30)", + "datetime.timedelta(hours=-4)", + "datetime.timedelta(minutes=-300)", + "datetime.timedelta(days=1, seconds=2, microseconds=3)", + "datetime.timedelta(hours=2)", + "datetime.timedelta(0)" + ] + }, + { + "name": "decimal.Decimal", + "instances": [ + "decimal.Decimal('22.2')", + "decimal.Decimal('1.234e7')", + "decimal.Decimal('sNaN')", + "decimal.Decimal(3)", + "decimal.Decimal('45.34')", + "decimal.Decimal('580')", + "decimal.Decimal((0, (0,), 0))", + "decimal.Decimal('3.4e200')", + "decimal.Decimal('1e2')", + "decimal.Decimal('2.59')", + "decimal.Decimal((1, (0, 0, 0), 37))", + "decimal.Decimal(10000)", + "decimal.Decimal('10e99999')", + "decimal.Decimal(7.5)", + "decimal.Decimal('1.1')", + "decimal.Decimal(10 ** (19 * 24))", + "decimal.Decimal('-inf')", + "decimal.Decimal(' 3.45679 ')", + "decimal.Decimal('1.0e-20')", + "decimal.Decimal('-0.8')", + "decimal.Decimal('1652.9E100')", + "decimal.Decimal('-10')", + "decimal.Decimal('0E10')", + "decimal.Decimal('2.54')", + "decimal.Decimal('15.32')", + "decimal.Decimal('-0')", + "decimal.Decimal('0.00390625')", + "decimal.Decimal(5)", + "decimal.Decimal((1, (0, 0, 0), 'N'))", + "decimal.Decimal('-3.141590000')", + "decimal.Decimal('0')", + "decimal.Decimal(45)", + "decimal.Decimal('1234e9999')", + "decimal.Decimal(2)", + "decimal.Decimal('1.634E100')", + "decimal.Decimal('4.125')", + "decimal.Decimal('4.2084')", + "decimal.Decimal('56531E100')", + "decimal.Decimal((1, [4, 3, 4, 9, 1, 3, 5, 3, 4], -25))", + "decimal.Decimal('9.99')", + "decimal.Decimal('45')", + "decimal.Decimal('100.0')", + "decimal.Decimal('7')", + "decimal.Decimal('1.01')", + "decimal.Decimal('111')", + "decimal.Decimal(2 ** 16)", + "decimal.Decimal('0.0012885819')", + "decimal.Decimal('1')", + "decimal.Decimal(-12)", + "decimal.Decimal('1.12345')", + "decimal.Decimal('-0.5')", + "decimal.Decimal((0, (4, 5, 3, 4), -2))", + "decimal.Decimal('-0.4')", + "decimal.Decimal('-33.3')", + "decimal.Decimal(2 ** 578)", + "decimal.Decimal('1.00000001e-20')", + "decimal.Decimal('10001111111')", + "decimal.Decimal([0, [0], 0])", + "decimal.Decimal('INF')", + "decimal.Decimal(2 ** 64 + 2 ** 32 - 1)", + "decimal.Decimal('4.9712')", + "decimal.Decimal('8.392')", + "decimal.Decimal('1e-9999')", + "decimal.Decimal('9.123')", + "decimal.Decimal('1e99')", + "decimal.Decimal('1.47e5')", + "decimal.Decimal(23)", + "decimal.Decimal('3.0')", + "decimal.Decimal('152587890625')", + "decimal.Decimal('3.1234')", + "decimal.Decimal(+45)", + "decimal.Decimal(float('nan'))", + "decimal.Decimal('32')", + "decimal.Decimal('snan123')", + "decimal.Decimal(10101)", + "decimal.Decimal('28.5')", + "decimal.Decimal('0.00')", + "decimal.Decimal('11.68')", + "decimal.Decimal('99999999999999999999999999.9')", + "decimal.Decimal('1_0_0_0')", + "decimal.Decimal('5')", + "decimal.Decimal('1.00000001e-100')", + "decimal.Decimal('0.28')", + "decimal.Decimal('NaN')", + "decimal.Decimal((0, (), 'F'))", + "decimal.Decimal('-NaN')", + "decimal.Decimal('9.87654321')", + "decimal.Decimal('10912837129')", + "decimal.Decimal('1.00000001')", + "decimal.Decimal('2E+1')", + "decimal.Decimal('-1')", + "decimal.Decimal('Nan891287828')", + "decimal.Decimal('-11.1')", + "decimal.Decimal((1, (), 37))", + "decimal.Decimal('1e1')", + "decimal.Decimal('NAN')", + "decimal.Decimal('9.8765e-12')", + "decimal.Decimal(99)", + "decimal.Decimal('625')", + "decimal.Decimal(200)", + "decimal.Decimal(123)", + "decimal.Decimal('1.00')", + "decimal.Decimal('81.3971')", + "decimal.Decimal('123456789.1')", + "decimal.Decimal('0.372')", + "decimal.Decimal('1.23')", + "decimal.Decimal(1234)", + "decimal.Decimal('-21.1')", + "decimal.Decimal('10901935')", + "decimal.Decimal('-0E12')", + "decimal.Decimal('Inf')", + "decimal.Decimal((0, (0,), 'F'))", + "decimal.Decimal('-0.6')", + "decimal.Decimal('9e2')", + "decimal.Decimal('35.719')", + "decimal.Decimal('390625')", + "decimal.Decimal(10 ** (19 * 25))", + "decimal.Decimal('-2.5')", + "decimal.Decimal('3.571')", + "decimal.Decimal('1e797')", + "decimal.Decimal('1e-425000000')", + "decimal.Decimal('188.83E100')", + "decimal.Decimal('0.001')", + "decimal.Decimal((1, (4, 3, 4, 9, 1, 3, 5, 3, 4), -25))", + "decimal.Decimal('100E-425000010')", + "decimal.Decimal('1e100000')", + "decimal.Decimal('3.5e-2')", + "decimal.Decimal('100000000000000000000000000')", + "decimal.Decimal('-5')", + "decimal.Decimal(11)", + "decimal.Decimal('1.0e20')", + "decimal.Decimal('1e4')", + "decimal.Decimal(1001)", + "decimal.Decimal('-4.34913534E-17')", + "decimal.Decimal('-23.00000')", + "decimal.Decimal('-25e55')", + "decimal.Decimal('456789')", + "decimal.Decimal('10')", + "decimal.Decimal('999.9')", + "decimal.Decimal([1, (4, 3, 4, 9, 1, 3, 5, 3, 4), -25])", + "decimal.Decimal(0)", + "decimal.Decimal('-0.0625')", + "decimal.Decimal('9.99e-5')", + "decimal.Decimal('256e7')", + "decimal.Decimal('1.3E4 \\n')", + "decimal.Decimal()", + "decimal.Decimal('10.0')", + "decimal.Decimal('3.1415926')", + "decimal.Decimal(0.1)", + "decimal.Decimal('0.25')", + "decimal.Decimal('9.8182731e181273')", + "decimal.Decimal('16.1')", + "decimal.Decimal('nan')", + "decimal.Decimal('-3.217160342717258261933904529E-7')", + "decimal.Decimal('1e9999')", + "decimal.Decimal('0.05')", + "decimal.Decimal('25')", + "decimal.Decimal(100)", + "decimal.Decimal(-2)", + "decimal.Decimal('NaN12345')", + "decimal.Decimal('1e-99')", + "decimal.Decimal('0.' + '9' * 30)", + "decimal.Decimal(1221 ** 1271)", + "decimal.Decimal('32.9714')", + "decimal.Decimal((1, (0, 2, 7, 1), 'F'))", + "decimal.Decimal((1, (4, 5), 0))", + "decimal.Decimal(50)", + "decimal.Decimal([1, [4, 3, 4, 9, 1, 3, 5, 3, 4], -25])", + "decimal.Decimal('45e2')", + "decimal.Decimal('2.234e2000')", + "decimal.Decimal(1000)", + "decimal.Decimal('7.33')", + "decimal.Decimal('5e3')", + "decimal.Decimal('-0.0')", + "decimal.Decimal('1.0e-100')", + "decimal.Decimal('5.5')", + "decimal.Decimal('-75')", + "decimal.Decimal('1.2')", + "decimal.Decimal('-0.625')", + "decimal.Decimal((0, (0, 0, 4, 0, 5, 3, 4), 'n'))", + "decimal.Decimal('33.3')", + "decimal.Decimal(9)", + "decimal.Decimal(4)", + "decimal.Decimal('1230E100')", + "decimal.Decimal('.000e20')", + "decimal.Decimal((0, (0, 0, 4, 0, 5, 3, 4), -2))", + "decimal.Decimal(67)", + "decimal.Decimal('sNAN')", + "decimal.Decimal(float('-inf'))", + "decimal.Decimal(123456789000)", + "decimal.Decimal('NaN123')", + "decimal.Decimal(True)", + "decimal.Decimal('5e-3')", + "decimal.Decimal('-6.1')", + "decimal.Decimal('-25')", + "decimal.Decimal('2')", + "decimal.Decimal('-1.25')", + "decimal.Decimal('0.1')", + "decimal.Decimal('1.0')", + "decimal.Decimal('90.697E100')", + "decimal.Decimal(152587890625)", + "decimal.Decimal(10 ** (9 * 24))", + "decimal.Decimal('12.7')", + "decimal.Decimal('66')", + "decimal.Decimal(-100)", + "decimal.Decimal('.1')", + "decimal.Decimal('7.34')", + "decimal.Decimal(float('inf'))", + "decimal.Decimal('7.335')", + "decimal.Decimal('1.2345')", + "decimal.Decimal('0.2')", + "decimal.Decimal((0, (4, 5, 3, 4), 'F'))", + "decimal.Decimal('Infinity')", + "decimal.Decimal('0.871831e800')", + "decimal.Decimal('1.00000001e20')", + "decimal.Decimal('3.1415')", + "decimal.Decimal('-Inf')", + "decimal.Decimal('-nan')", + "decimal.Decimal(456)", + "decimal.Decimal('194')", + "decimal.Decimal('0.025')", + "decimal.Decimal('snan')", + "decimal.Decimal(567)", + "decimal.Decimal('9.8765e12')", + "decimal.Decimal('3.1')", + "decimal.Decimal('-1.5')", + "decimal.Decimal('inf')", + "decimal.Decimal('1.3')", + "decimal.Decimal('-4.5678E50')", + "decimal.Decimal(5 ** 2659)", + "decimal.Decimal('33e+33')", + "decimal.Decimal('1e-100000')", + "decimal.Decimal(float('-0.0'))", + "decimal.Decimal('1.23456789')", + "decimal.Decimal('1e-10')", + "decimal.Decimal(12)", + "decimal.Decimal('nan123')", + "decimal.Decimal('0.0')", + "decimal.Decimal('-0.000')", + "decimal.Decimal('152587890625e7')", + "decimal.Decimal('-16.1')", + "decimal.Decimal('0.333333333333333333')", + "decimal.Decimal(-1)", + "decimal.Decimal('43.24')", + "decimal.Decimal('9.9')", + "decimal.Decimal('3.1416')", + "decimal.Decimal('2.1')", + "decimal.Decimal('16807')", + "decimal.Decimal('62.4802')", + "decimal.Decimal('-15')", + "decimal.Decimal(500000123)", + "decimal.Decimal('-38.3')", + "decimal.Decimal('0.333333333333333333333333')", + "decimal.Decimal('1e425000000')", + "decimal.Decimal('1.5')", + "decimal.Decimal(768)", + "decimal.Decimal(10 ** (9 * 25))", + "decimal.Decimal((1, (), 'n'))", + "decimal.Decimal('11.1')", + "decimal.Decimal('0.01')", + "decimal.Decimal(-45)", + "decimal.Decimal('9.99e10')", + "decimal.Decimal('1e-3')", + "decimal.Decimal('100000000.123')", + "decimal.Decimal('0.5')", + "decimal.Decimal('3')", + "decimal.Decimal(1)", + "decimal.Decimal(10)", + "decimal.Decimal('20')", + "decimal.Decimal('0.1234')", + "decimal.Decimal('-Infinity')", + "decimal.Decimal('.01')", + "decimal.Decimal('23.42')", + "decimal.Decimal(' -7.89')", + "decimal.Decimal(False)", + "decimal.Decimal('1_3.3e4_0')", + "decimal.Decimal('2.234e-2000')", + "decimal.Decimal('-1E+1')", + "decimal.Decimal('1.50001')", + "decimal.Decimal('8.71E+799')", + "decimal.Decimal('20.686')" + ] + } +] diff --git a/utbot-python/src/test/java/org/utbot/python/framework/external/PythonUtBotJavaApiTest.java b/utbot-python/src/test/java/org/utbot/python/framework/external/PythonUtBotJavaApiTest.java new file mode 100644 index 0000000000..0d79f03c60 --- /dev/null +++ b/utbot-python/src/test/java/org/utbot/python/framework/external/PythonUtBotJavaApiTest.java @@ -0,0 +1,114 @@ +package org.utbot.python.framework.external; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.utbot.python.PythonTestSet; +import org.utbot.python.framework.codegen.model.Unittest; +import org.utbot.python.utils.Cleaner; +import org.utbot.python.utils.TemporaryFileManager; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +public class PythonUtBotJavaApiTest { + private final String pythonPath = ""; // Set your path to Python before testing + + @BeforeEach + public void setUp() { + TemporaryFileManager.INSTANCE.initialize(); + Cleaner.INSTANCE.restart(); + } + + @AfterEach + public void tearDown() { + Cleaner.INSTANCE.doCleaning(); + } + private File loadResource(String name) { + URL resource = getClass().getClassLoader().getResource(name); + if (resource == null) { + throw new IllegalArgumentException("file not found!"); + } else { + try { + return new File(resource.toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void testSimpleFunction() { + File fileWithCode = loadResource("example_code/arithmetic.py"); + String pythonRunRoot = fileWithCode.getParentFile().getAbsolutePath(); + String moduleFilename = fileWithCode.getAbsolutePath(); + PythonObjectName testMethodName = new PythonObjectName("arithmetic", "calculate_function_value"); + PythonTestMethodInfo methodInfo = new PythonTestMethodInfo(testMethodName, moduleFilename, null); + ArrayList testMethods = new ArrayList<>(1); + testMethods.add(methodInfo); + ArrayList directoriesForSysPath = new ArrayList<>(); + directoriesForSysPath.add(pythonRunRoot); + String testCode = PythonUtBotJavaApi.generate( + testMethods, + pythonPath, + pythonRunRoot, + directoriesForSysPath, + 10_000, + 1_000, + Unittest.INSTANCE + ); + Assertions.assertFalse(testCode.isEmpty()); + } + + @Test + public void testSimpleFunctionTestCase() { + File fileWithCode = loadResource("example_code/arithmetic.py"); + String pythonRunRoot = fileWithCode.getParentFile().getAbsolutePath(); + String moduleFilename = fileWithCode.getAbsolutePath(); + PythonObjectName testMethodName = new PythonObjectName("arithmetic", "calculate_function_value"); + PythonTestMethodInfo methodInfo = new PythonTestMethodInfo(testMethodName, moduleFilename, null); + ArrayList testMethods = new ArrayList<>(1); + testMethods.add(methodInfo); + ArrayList directoriesForSysPath = new ArrayList<>(); + directoriesForSysPath.add(pythonRunRoot); + List testCase = PythonUtBotJavaApi.generateTestSets( + testMethods, + pythonPath, + pythonRunRoot, + directoriesForSysPath, + 10_000, + 1_000 + ); + Assertions.assertFalse(testCase.isEmpty()); + Assertions.assertFalse(testCase.get(0).component2().isEmpty()); + } + + @Test + public void testSimpleClassMethod() { + File fileWithCode = loadResource("example_code/inner_dir/inner_module.py"); + File projectRoot = loadResource("example_code/"); + String pythonRunRoot = projectRoot.getAbsolutePath(); + String moduleFilename = fileWithCode.getAbsolutePath(); + PythonObjectName containingClassName = new PythonObjectName("inner_dir.inner_module", "InnerClass"); + PythonObjectName testMethodName = new PythonObjectName("inner_dir.inner_module", "f"); + PythonTestMethodInfo methodInfo = new PythonTestMethodInfo(testMethodName, moduleFilename, containingClassName); + ArrayList testMethods = new ArrayList<>(1); + testMethods.add(methodInfo); + ArrayList directoriesForSysPath = new ArrayList<>(); + directoriesForSysPath.add(pythonRunRoot); + String testCode = PythonUtBotJavaApi.generate( + testMethods, + pythonPath, + pythonRunRoot, + directoriesForSysPath, + 10_000, + 1_000, + Unittest.INSTANCE + ); + Assertions.assertFalse(testCode.isEmpty()); + } +} diff --git a/utbot-python/src/test/kotlin/org/utbot/python/framework/api/python/utils/PythonTreeComparatorTest.kt b/utbot-python/src/test/kotlin/org/utbot/python/framework/api/python/utils/PythonTreeComparatorTest.kt new file mode 100644 index 0000000000..77b2177eac --- /dev/null +++ b/utbot-python/src/test/kotlin/org/utbot/python/framework/api/python/utils/PythonTreeComparatorTest.kt @@ -0,0 +1,380 @@ +package org.utbot.python.framework.api.python.utils + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.comparePythonTree +import org.utbot.python.framework.api.python.util.pythonIntClassId +import org.utbot.python.framework.api.python.util.pythonStrClassId + +internal class PythonTreeComparatorTest { + @Test + fun testEqualPrimitive() { + val left = PythonTree.PrimitiveNode(pythonIntClassId, "1") + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualList() { + val left = PythonTree.ListNode(mapOf( + 0 to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualTuple() { + val left = PythonTree.TupleNode(mapOf( + 0 to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualDict() { + val left = PythonTree.DictNode(mapOf( + PythonTree.PrimitiveNode(pythonStrClassId, "'a'") to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + PythonTree.PrimitiveNode(pythonStrClassId, "'b'") to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualSet() { + val left = PythonTree.SetNode(setOf( + PythonTree.PrimitiveNode(pythonIntClassId, "1"), + PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableSet()) + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualEmptyReduce() { + val left = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(PythonTree.PrimitiveNode(pythonIntClassId, "2")), + ) + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualNotEmptyReduce() { + val left = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(PythonTree.PrimitiveNode(pythonIntClassId, "2")), + ) + left.state["my_field"] = PythonTree.PrimitiveNode(pythonIntClassId, "2") + left.state["my_field_1"] = PythonTree.PrimitiveNode(pythonIntClassId, "1") + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualReduceWithListItems() { + val left = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(PythonTree.PrimitiveNode(pythonIntClassId, "2")), + ) + left.listitems = listOf( + PythonTree.PrimitiveNode(pythonIntClassId, "1"), + PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ) + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualReduceWithDictItems() { + val left = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(PythonTree.PrimitiveNode(pythonIntClassId, "2")), + ) + left.dictitems = mapOf( + PythonTree.PrimitiveNode(pythonStrClassId, "'a'") to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + PythonTree.PrimitiveNode(pythonStrClassId, "'b'") to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ) + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualRecursiveReduce() { + val left = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val child = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val child2 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + left.state["children"] = PythonTree.ListNode( + mapOf(0 to child).toMutableMap() + ) + child.state["children"] = PythonTree.ListNode( + mapOf(0 to child2).toMutableMap() + ) + child2.state["children"] = PythonTree.ListNode( + mapOf(0 to left).toMutableMap() + ) + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testEqualHardRecursiveReduce() { + val left = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val child = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val child2 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val child3 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val child4 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val child5 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val child6 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + left.state["children"] = PythonTree.ListNode(mapOf( + 0 to child, + 1 to child2, + 2 to child3, + ).toMutableMap()) + child.state["children"] = PythonTree.ListNode(mapOf( + 0 to child2, + 1 to child3, + ).toMutableMap()) + child2.state["children"] = PythonTree.ListNode(mapOf( + 0 to child2, + 1 to child3, + 2 to left, + 3 to child4, + ).toMutableMap()) + child3.state["children"] = PythonTree.ListNode(mapOf( + 0 to left, + ).toMutableMap()) + child4.state["children"] = PythonTree.ListNode(mapOf( + 0 to left, + 1 to child5, + ).toMutableMap()) + child5.state["children"] = PythonTree.ListNode(mapOf( + 0 to child2, + ).toMutableMap()) + child6.state["children"] = PythonTree.ListNode(mapOf( + 0 to child2, + ).toMutableMap()) + + assertTrue(comparePythonTree(left, left)) + } + + @Test + fun testNotEqualPrimitive() { + val left = PythonTree.PrimitiveNode(pythonIntClassId, "1") + val right = PythonTree.PrimitiveNode(pythonIntClassId, "2") + + assertFalse(comparePythonTree(left, right)) + } + + @Test + fun testNotEqualList() { + val left = PythonTree.ListNode(mapOf( + 0 to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + val right = PythonTree.ListNode(mapOf( + 0 to PythonTree.PrimitiveNode(pythonIntClassId, "0"), + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + + assertFalse(comparePythonTree(left, right)) + } + + @Test + fun testNotEqualTuple() { + val left = PythonTree.TupleNode(mapOf( + 0 to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + val right = PythonTree.TupleNode(mapOf( + 0 to PythonTree.PrimitiveNode(pythonIntClassId, "0"), + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + + assertFalse(comparePythonTree(left, right)) + } + + @Test + fun testNotEqualDict() { + val left = PythonTree.DictNode(mapOf( + PythonTree.PrimitiveNode(pythonStrClassId, "'c'") to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + PythonTree.PrimitiveNode(pythonStrClassId, "'b'") to PythonTree.PrimitiveNode(pythonIntClassId, "0"), + ).toMutableMap()) + val right = PythonTree.DictNode(mapOf( + PythonTree.PrimitiveNode(pythonStrClassId, "'a'") to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + PythonTree.PrimitiveNode(pythonStrClassId, "'b'") to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + + assertFalse(comparePythonTree(left, right)) + } + + @Test + fun testNotEqualSet() { + val left = PythonTree.SetNode(setOf( + PythonTree.PrimitiveNode(pythonIntClassId, "1"), + PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableSet()) + val right = PythonTree.SetNode(setOf( + PythonTree.PrimitiveNode(pythonIntClassId, "0"), + PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableSet()) + + assertFalse(comparePythonTree(left, right)) + } + + @Test + fun testNotEqualListDiffSize() { + val left = PythonTree.ListNode(mapOf( + 0 to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + val right = PythonTree.ListNode(mapOf( + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + + assertFalse(comparePythonTree(left, right)) + } + + @Test + fun testNotEqualTupleDiffSize() { + val left = PythonTree.TupleNode(mapOf( + 0 to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + val right = PythonTree.TupleNode(mapOf( + 1 to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + + assertFalse(comparePythonTree(left, right)) + } + + @Test + fun testNotEqualDictDiffSize() { + val left = PythonTree.DictNode(mapOf( + PythonTree.PrimitiveNode(pythonStrClassId, "'c'") to PythonTree.PrimitiveNode(pythonIntClassId, "1"), + PythonTree.PrimitiveNode(pythonStrClassId, "'b'") to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + val right = PythonTree.DictNode(mapOf( + PythonTree.PrimitiveNode(pythonStrClassId, "'b'") to PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableMap()) + + assertFalse(comparePythonTree(left, right)) + } + + @Test + fun testNotEqualSetDiffSize() { + val left = PythonTree.SetNode(setOf( + PythonTree.PrimitiveNode(pythonIntClassId, "1"), + PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableSet()) + val right = PythonTree.SetNode(setOf( + PythonTree.PrimitiveNode(pythonIntClassId, "2"), + ).toMutableSet()) + + assertFalse(comparePythonTree(left, right)) + } + + @Test + fun testNotEqualRecursiveReduce() { + val left = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val leftChild1 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val leftChild2 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + left.state["children"] = PythonTree.ListNode( + mapOf(0 to leftChild1).toMutableMap() + ) + leftChild1.state["children"] = PythonTree.ListNode( + mapOf(0 to leftChild2).toMutableMap() + ) + leftChild2.state["children"] = PythonTree.ListNode( + mapOf(0 to left).toMutableMap() + ) + + val right = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val rightChild1 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + val rightChild2 = PythonTree.ReduceNode( + PythonClassId("my_module", "MyClass"), + PythonClassId("my_module.MyClass"), + listOf(), + ) + right.state["children"] = PythonTree.ListNode(mapOf( + 0 to rightChild1, + 1 to rightChild2, + ).toMutableMap()) + rightChild1.state["children"] = PythonTree.ListNode(mapOf( + 0 to rightChild2, + ).toMutableMap()) + + assertFalse(comparePythonTree(left, right)) + } +} \ No newline at end of file diff --git a/utbot-rd/build.gradle b/utbot-rd/build.gradle index ea439435b7..f09e7cdf81 100644 --- a/utbot-rd/build.gradle +++ b/utbot-rd/build.gradle @@ -1,10 +1,16 @@ plugins { - id 'com.jetbrains.rdgen' version "2022.3.1" + id 'com.jetbrains.rdgen' version "2023.2.0" } import com.jetbrains.rd.generator.gradle.RdGenExtension import com.jetbrains.rd.generator.gradle.RdGenTask +if (includeRiderInBuild.toBoolean()) { + def utbotRider = project.rootProject.childProjects["utbot-rider"] + evaluationDependsOn(utbotRider.path) + tasks.getByName("classes").dependsOn(utbotRider.tasks.getByName("addRiderModelsToUtbotModels")) +} + compileKotlin { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8 @@ -26,6 +32,8 @@ configurations { lifetimedProcessMockCompileClasspath.extendsFrom configurations.compileClasspath processWithRdServerMockCompileClasspath.extendsFrom configurations.compileClasspath rdgenModelsCompileClasspath.extendsFrom configurations.compileClasspath + if (includeRiderInBuild.toBoolean()) + riderRdgenModelsCompileClasspath.extendsFrom configurations.rdgenModelsCompileClasspath } sourceSets { @@ -44,18 +52,31 @@ sourceSets { srcDirs = ["src/main/rdgen"] } } + if (includeRiderInBuild.toBoolean()) { + riderRdgenModels { + kotlin { + srcDirs = ["src/main/riderRdgenModels"] + } + } + } } +def riderModelJar = new File(project.buildDir, "libs/rider-model.jar") + dependencies { implementation project(':utbot-core') - implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: '2022.3.1' - implementation group: 'com.jetbrains.rd', name: 'rd-core', version: '2022.3.1' + implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: rdVersion + implementation group: 'com.jetbrains.rd', name: 'rd-core', version: rdVersion implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion processWithRdServerMockImplementation project(':utbot-rd') - rdgenModelsCompileClasspath group: 'com.jetbrains.rd', name: 'rd-gen', version: '2022.3.1' + rdgenModelsCompileClasspath group: 'com.jetbrains.rd', name: 'rd-gen', version: rdVersion + + if (includeRiderInBuild.toBoolean()) { + riderRdgenModelsCompileClasspath files(riderModelJar) + } } task lifetimedProcessMockJar(type: Jar) { @@ -99,7 +120,51 @@ test { systemProperty("PROCESS_WITH_RD_SERVER_MOCK", processWithRdServerMockJar.archiveFile.get().getAsFile().canonicalPath) } -task generateChildProcessProtocolModels(type: RdGenTask) { +if (includeRiderInBuild.toBoolean()) { + def currentProjectDir = project.projectDir + def riderProject = project.rootProject.childProjects["utbot-rider"] + def riderTask = riderProject.tasks.getByName("addRiderModelsToUtbotModels") + // !!!! IMPORTANT !!!! + // after generation you should MANUALLY correct kotlin generated code as it is incorrectly references some rider model + // mandatory steps: + // 1. In UtBotRiderModel.Generated.kt change package to `package org.utbot.rider.generated` + // 2. then import all unreferenced classes + // otherwise you will have broken plugin initialization and ClassNotFoundException + task generateRiderModels(type: RdGenTask) { + dependsOn(riderTask) + + def riderProjectDir = riderProject.projectDir + def generatedOutputDir = new File(riderProjectDir, "src/main/kotlin/org/utbot/rider/generated") + def hashDir = generatedOutputDir + def sourcesDir = new File(currentProjectDir, "src/main/riderRdgenModels/org/utbot/rider/rd/models") + def rdParams = extensions.getByName("params") as RdGenExtension + + group = "rdgen" + rdParams.verbose = true + rdParams.sources(sourcesDir) + rdParams.hashFolder = hashDir.canonicalPath + rdParams.packages = "org.utbot.rider.rd.models" + rdParams.classpath(riderModelJar) + + rdParams.generator { + language = "kotlin" + transform = "symmetric" + root = "com.jetbrains.rider.model.nova.ide.IdeRoot" + directory = generatedOutputDir.canonicalPath + namespace = "org.utbot.rider.generated" + } + + rdParams.generator { + language = "csharp" + transform = "symmetric" + root = "com.jetbrains.rider.model.nova.ide.IdeRoot" + namespace = "UtBot" + directory = new File(riderProjectDir, "src/dotnet/UtBot/UtBot/Generated") + } + } +} + +task generateInstrumentedProcessModels(type: RdGenTask) { def currentProjectDir = project.projectDir def instrumentationProjectDir = project.rootProject.childProjects["utbot-instrumentation"].projectDir def generatedOutputDir = new File(instrumentationProjectDir, "src/main/kotlin/org/utbot/instrumentation/rd/generated") @@ -117,14 +182,14 @@ task generateChildProcessProtocolModels(type: RdGenTask) { rdParams.generator { language = "kotlin" transform = "symmetric" - root = "org.utbot.rd.models.ChildProcessProtocolRoot" + root = "org.utbot.rd.models.InstrumentedProcessRoot" directory = generatedOutputDir.canonicalPath - namespace = "org.utbot.instrumentation.rd.generated" + namespace = "org.utbot.instrumentation.process.generated" } } -task generateEngineProcessProtocolModels(type: RdGenTask) { +task generateEngineProcessModels(type: RdGenTask) { def currentProjectDir = project.projectDir def ideaPluginProjectDir = project.rootProject.childProjects["utbot-framework"].projectDir def generatedOutputDir = new File(ideaPluginProjectDir, "src/main/kotlin/org/utbot/framework/process/generated") @@ -142,25 +207,60 @@ task generateEngineProcessProtocolModels(type: RdGenTask) { rdParams.generator { language = "kotlin" transform = "symmetric" - root = "org.utbot.rd.models.EngineProcessProtocolRoot" + root = "org.utbot.rd.models.EngineProcessRoot" directory = generatedOutputDir.canonicalPath namespace = "org.utbot.framework.process.generated" } +} + +task generateCommonModels(type: RdGenTask) { + def currentProjectDir = project.projectDir + def ideaPluginProjectDir = project.rootProject.childProjects["utbot-rd"].projectDir + def generatedOutputDir = new File(ideaPluginProjectDir, "src/main/kotlin/org/utbot/rd/generated") + def hashDir = generatedOutputDir + def sourcesDir = new File(currentProjectDir, "src/main/rdgen/org/utbot/rd/models") + def rdParams = extensions.getByName("params") as RdGenExtension + + group = "rdgen" + rdParams.verbose = true + rdParams.sources(sourcesDir) + rdParams.hashFolder = hashDir.canonicalPath + // where to search roots + rdParams.packages = "org.utbot.rd.models" + rdParams.generator { language = "kotlin" transform = "symmetric" - root = "org.utbot.rd.models.SettingsProtocolRoot" + root = "org.utbot.rd.models.SynchronizationRoot" directory = generatedOutputDir.canonicalPath - namespace = "org.utbot.framework.process.generated" + namespace = "org.utbot.rd.generated" + } + + rdParams.generator { + language = "kotlin" + transform = "symmetric" + root = "org.utbot.rd.models.SettingsRoot" + + directory = generatedOutputDir.canonicalPath + namespace = "org.utbot.rd.generated" + } + + rdParams.generator { + language = "kotlin" + transform = "symmetric" + root = "org.utbot.rd.models.LoggerRoot" + + directory = generatedOutputDir.canonicalPath + namespace = "org.utbot.rd.generated" } } -task generateSynchronizationModels(type: RdGenTask) { +task generateSpringModels(type: RdGenTask) { def currentProjectDir = project.projectDir - def ideaPluginProjectDir = project.rootProject.childProjects["utbot-rd"].projectDir - def generatedOutputDir = new File(ideaPluginProjectDir, "src/main/kotlin/org/utbot/rd/generated") + def ideaPluginProjectDir = project.rootProject.childProjects["utbot-spring-analyzer"].projectDir + def generatedOutputDir = new File(ideaPluginProjectDir, "src/main/kotlin/org/utbot/spring/generated") def hashDir = generatedOutputDir def sourcesDir = new File(currentProjectDir, "src/main/rdgen/org/utbot/rd/models") def rdParams = extensions.getByName("params") as RdGenExtension @@ -175,9 +275,33 @@ task generateSynchronizationModels(type: RdGenTask) { rdParams.generator { language = "kotlin" transform = "symmetric" - root = "org.utbot.rd.models.SynchronizationModelRoot" + root = "org.utbot.rd.models.SpringAnalyzerRoot" directory = generatedOutputDir.canonicalPath - namespace = "org.utbot.rd.generated" + namespace = "org.utbot.spring.generated" + } +} + +task generateCSharpModels(type: RdGenTask) { + def currentProjectDir = project.projectDir + def riderPluginProjectDir = project.rootProject.projectDir.toPath().resolve("utbot-rider").toFile() + def generatedOutputDir = new File (riderPluginProjectDir, "src/dotnet/UtBot/UtBot.Rd/Generated") + def hashDir = generatedOutputDir + def sourcesDir = new File(currentProjectDir, "src/main/rdgen/org/utbot/rd/models") + def rdParams = extensions.getByName("params") as RdGenExtension + + group = "rdgen" + rdParams.verbose = true + rdParams.sources(sourcesDir) + rdParams.hashFolder = hashDir.canonicalPath + rdParams.packages = "org.utbot.rd.models" + + rdParams.generator { + language = "csharp" + transform = "symmetric" + root = "org.utbot.rd.models.CSharpRoot" + + directory = generatedOutputDir.canonicalPath + namespace = "UtBot.Rd.Generated" } } \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/ClientProcessUtil.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/ClientProcessUtil.kt index 187b475ebc..133437c11c 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/ClientProcessUtil.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/ClientProcessUtil.kt @@ -3,20 +3,26 @@ package org.utbot.rd import com.jetbrains.rd.framework.* import com.jetbrains.rd.framework.impl.RdCall import com.jetbrains.rd.framework.util.launch +import com.jetbrains.rd.util.LogLevel import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.info import com.jetbrains.rd.util.lifetime.Lifetime import com.jetbrains.rd.util.lifetime.LifetimeDefinition import com.jetbrains.rd.util.lifetime.isAlive import com.jetbrains.rd.util.lifetime.plusAssign +import com.jetbrains.rd.util.reactive.adviseEternal import com.jetbrains.rd.util.threading.SingleThreadScheduler import com.jetbrains.rd.util.trace +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.withTimeoutOrNull import org.utbot.common.* import org.utbot.rd.generated.synchronizationModel +import org.utbot.rd.loggers.withLevel import java.io.File +import java.io.OutputStream +import java.io.PrintStream import kotlin.time.Duration const val rdProcessDirName = "rdProcessSync" @@ -29,7 +35,7 @@ internal fun childCreatedFileName(port: Int): String { return "$port.created" } -internal fun signalChildReady(port: Int) { +internal fun signalProcessReady(port: Int) { processSyncDirectory.mkdirs() val signalFile = File(processSyncDirectory, childCreatedFileName(port)) @@ -55,7 +61,10 @@ fun findRdPort(args: Array): Int { ?: throw IllegalArgumentException("No port provided") } -class CallsSynchronizer(private val ldef: LifetimeDefinition, val timeout: Duration) { +/** + * Traces when process is idle for too much time, then terminates it. + */ +class IdleWatchdog(private val ldef: LifetimeDefinition, val timeout: Duration) { private enum class State { STARTED, ENDED @@ -63,19 +72,46 @@ class CallsSynchronizer(private val ldef: LifetimeDefinition, val timeout: Durat private val synchronizer: Channel = Channel(1) - fun measureExecutionForTermination(block: () -> T): T = runBlocking { + init { + ldef.onTermination { synchronizer.close(CancellationException("Client terminated")) } + } + + var suspendTimeout = false + + /** + * Execute block indicating that during this activity process should not die. + * After block ended - idle timer restarts + */ + fun wrapActive(block: () -> T): T { try { - synchronizer.send(State.STARTED) - return@runBlocking block() + synchronizer.trySendBlocking(State.STARTED) + return block() } finally { - synchronizer.send(State.ENDED) + synchronizer.trySendBlocking(State.ENDED) } } - fun measureExecutionForTermination(call: RdCall, block: (T) -> R) { + /** + * Adds callback to RdCall with indicating that during this activity process should not die. + * After block ended - idle timer restarts + */ + fun wrapActiveCall(call: RdCall, block: (T) -> R) { call.set { it -> - runBlocking { - measureExecutionForTermination { + wrapActive { + block(it) + } + } + } + + /** + * Adds callback to RdCall with indicating that during this activity process should not die. + * After block ended - idle timer restarts. + * Also additonally logs + */ + fun measureTimeForActiveCall(call: RdCall, requestName: String, level: LogLevel = LogLevel.Debug, block: (T) -> R) { + call.set { it -> + logger.withLevel(level).measureTime({ requestName }) { + wrapActive { block(it) } } @@ -91,7 +127,7 @@ class CallsSynchronizer(private val ldef: LifetimeDefinition, val timeout: Durat synchronizer.receive() } if (current == null) { - if (lastState == State.ENDED) { + if (lastState == State.ENDED && !suspendTimeout) { // process is waiting for command more than expected, better die logger.info { "terminating lifetime by timeout" } stopProtocol() @@ -112,8 +148,29 @@ class CallsSynchronizer(private val ldef: LifetimeDefinition, val timeout: Durat class ClientProtocolBuilder { private var timeout = Duration.INFINITE - suspend fun start(port: Int, parent: Lifetime? = null, bindables: Protocol.(CallsSynchronizer) -> Unit) { - UtRdCoroutineScope.current // coroutine scope initialization + private fun silentlyCloseStandardStreams() { + // we should change out/err streams as not to spend time on user output + // and also because rd default logging system writes some initial values to stdout, polluting it as well + val tmpStream = PrintStream(object : OutputStream() { + override fun write(b: Int) {} + }) + val prevOut = System.out + val prevError = System.err + System.setOut(tmpStream) + System.setErr(tmpStream) + // stdin/stderr should be closed as not to leave hanging descriptors + // and we cannot log any exceptions here as rd remote logging is still not configured + // so we pass any exceptions + silent { prevOut.close() } + silent { prevError.close() } + } + + suspend fun start(args: Array, parent: Lifetime? = null, block: Protocol.(IdleWatchdog) -> Unit) { + silentlyCloseStandardStreams() + + val port = findRdPort(args) + + UtRdCoroutineScope.initialize() val pid = currentProcessPid.toInt() val ldef = parent?.createNested() ?: LifetimeDefinition() ldef.terminateOnException { _ -> @@ -139,21 +196,24 @@ class ClientProtocolBuilder { SocketWire.Client(ldef, rdClientProtocolScheduler, port), ldef ) - val synchronizer = CallsSynchronizer(ldef, timeout) + val watchdog = IdleWatchdog(ldef, timeout) - synchronizer.setupTimeout() + watchdog.setupTimeout() rdClientProtocolScheduler.pump(ldef) { - clientProtocol.synchronizationModel - clientProtocol.bindables(synchronizer) + clientProtocol.synchronizationModel.suspendTimeoutTimer.set { param -> + watchdog.suspendTimeout = param + } + clientProtocol.synchronizationModel.stopProcess.adviseEternal { _ -> watchdog.stopProtocol() } + clientProtocol.block(watchdog) } - signalChildReady(port) + signalProcessReady(port) logger.info { "signalled" } clientProtocol.synchronizationModel.synchronizationSignal.let { sync -> val answerFromMainProcess = sync.adviseForConditionAsync(ldef) { if (it == "main") { logger.trace { "received from main" } - synchronizer.measureExecutionForTermination { + watchdog.wrapActive { sync.fire("child") } true diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/LifetimedProcess.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/LifetimedProcess.kt index fb1241a617..9d0b38b94d 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/LifetimedProcess.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/LifetimedProcess.kt @@ -5,6 +5,7 @@ import com.jetbrains.rd.util.lifetime.LifetimeDefinition import com.jetbrains.rd.util.lifetime.throwIfNotAlive import kotlinx.coroutines.delay import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread /** * Creates LifetimedProcess. @@ -66,11 +67,14 @@ class LifetimedProcessIml(override val process: Process, lifetime: Lifetime? = n init { ldef = lifetime?.createNested() ?: LifetimeDefinition() + allProcessList.add(this) ldef.onTermination { process.destroy() if (!process.waitFor(processKillTimeoutMillis, TimeUnit.MILLISECONDS)) process.destroyForcibly() + + allProcessList.remove(this) } UtRdCoroutineScope.current.launch(ldef) { while (process.isAlive) { @@ -84,4 +88,16 @@ class LifetimedProcessIml(override val process: Process, lifetime: Lifetime? = n override fun terminate() { ldef.terminate() } + + companion object { + private val allProcessList = mutableListOf() + + init { + Runtime.getRuntime().addShutdownHook(thread(start = false) { + for (proc in allProcessList.reversed()) { + proc.terminate() + } + }) + } + } } \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/ProcessWithRdServer.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/ProcessWithRdServer.kt index 31834ca95b..a6321f1e18 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/ProcessWithRdServer.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/ProcessWithRdServer.kt @@ -11,6 +11,7 @@ import com.jetbrains.rd.util.lifetime.throwIfNotAlive import com.jetbrains.rd.util.trace import kotlinx.coroutines.delay import org.utbot.common.getPid +import org.utbot.common.silent import org.utbot.rd.generated.synchronizationModel import java.io.File import java.nio.file.Files @@ -79,8 +80,7 @@ interface ProcessWithRdServer : LifetimedProcess { val port: Int get() = protocol.wire.serverPort - suspend fun initModels(bindables: Protocol.() -> Unit): ProcessWithRdServer - suspend fun awaitSignal(): ProcessWithRdServer + suspend fun awaitProcessReady(): ProcessWithRdServer } private val logger = getLogger() @@ -91,6 +91,13 @@ class ProcessWithRdServerImpl private constructor( ) : ProcessWithRdServer, LifetimedProcess by child { override val protocol = serverFactory(lifetime) + override fun terminate() { + silent { + protocol.synchronizationModel.stopProcess.fire(Unit) + } + child.terminate() + } + companion object { suspend operator fun invoke( child: LifetimedProcess, serverFactory: (Lifetime) -> Protocol @@ -99,15 +106,7 @@ class ProcessWithRdServerImpl private constructor( } } - override suspend fun initModels(bindables: Protocol.() -> Unit): ProcessWithRdServer { - protocol.scheduler.pump(lifetime) { - protocol.bindables() - } - - return this - } - - override suspend fun awaitSignal(): ProcessWithRdServer { + override suspend fun awaitProcessReady(): ProcessWithRdServer { protocol.scheduler.pump(lifetime) { protocol.synchronizationModel } diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/RdSettingsContainer.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/RdSettingsContainer.kt new file mode 100644 index 0000000000..420a24374a --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/RdSettingsContainer.kt @@ -0,0 +1,56 @@ +package org.utbot.rd + +import mu.KLogger +import org.utbot.common.SettingsContainer +import org.utbot.common.SettingsContainerFactory +import org.utbot.rd.generated.SettingForArgument +import org.utbot.rd.generated.SettingsModel +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class RdSettingsContainerFactory(private val settingsModel: SettingsModel) : SettingsContainerFactory { + override fun createSettingsContainer( + logger: KLogger, + defaultKeyForSettingsPath: String, + defaultSettingsPath: String? + ): SettingsContainer { + return RdSettingsContainer(logger, defaultKeyForSettingsPath, settingsModel) + } +} + +class RdSettingsContainer(val logger: KLogger, val key: String, val settingsModel: SettingsModel) : SettingsContainer { + + override fun settingFor( + defaultValue: T, + range : Triple>?, + converter: (String) -> T + ): PropertyDelegateProvider> { + return PropertyDelegateProvider { _, _ -> + object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + val params = SettingForArgument(key, property.name) + return settingsModel.settingFor.startBlocking(params).value?.let { + try { + return range?.run { + // Coerce parsed value into the specified range + minOf( + range.second, + maxOf(converter(it), range.first, range.third), + range.third + ) + } ?: converter(it) + } catch (e: Exception) { + logger.warn("Cannot parse value for $key, default value [$defaultValue] will be used instead") { e } + defaultValue + } + } ?: defaultValue + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + throw IllegalStateException("Setting properties allowed only from plugin process") + } + } + } + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdCoroutineScope.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdCoroutineScope.kt index f9a2a5a2ae..5d46179e04 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdCoroutineScope.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdCoroutineScope.kt @@ -3,12 +3,16 @@ package org.utbot.rd import com.jetbrains.rd.framework.util.RdCoroutineScope import com.jetbrains.rd.framework.util.asCoroutineDispatcher import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.threading.SingleThreadScheduler -private val coroutineDispatcher = UtSingleThreadScheduler("UtCoroutineScheduler").asCoroutineDispatcher +private val coroutineDispatcher = SingleThreadScheduler(Lifetime.Eternal, "UtCoroutineScheduler").asCoroutineDispatcher class UtRdCoroutineScope(lifetime: Lifetime) : RdCoroutineScope(lifetime) { companion object { val current = UtRdCoroutineScope(Lifetime.Eternal) + fun initialize() { + // only to load and initialize class + } } init { diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdUtil.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdUtil.kt index a7f9f25d25..4dabe27c7b 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdUtil.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/UtRdUtil.kt @@ -3,6 +3,7 @@ package org.utbot.rd import com.jetbrains.rd.framework.* import com.jetbrains.rd.framework.util.NetUtils import com.jetbrains.rd.framework.util.synchronizeWith +import com.jetbrains.rd.util.* import com.jetbrains.rd.util.lifetime.Lifetime import com.jetbrains.rd.util.lifetime.LifetimeDefinition import com.jetbrains.rd.util.lifetime.throwIfNotAlive @@ -11,8 +12,30 @@ import com.jetbrains.rd.util.reactive.ISource import com.jetbrains.rd.util.threading.SingleThreadScheduler import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred +import kotlinx.coroutines.runBlocking +import org.utbot.common.LoggerWithLogMethod -// useful when initializing something +suspend fun ProcessWithRdServer.onScheduler(block: () -> T): T { + val deffered = CompletableDeferred() + protocol.scheduler.invokeOrQueue { deffered.complete(block()) } + return deffered.await() +} + +fun ProcessWithRdServer.onSchedulerBlocking(block: () -> T): T = runBlocking { onScheduler(block) } + +fun IRdCall.startBlocking(req: TReq): TRes { + val call = this + // We do not use RdCall.sync because it requires timeouts for execution, after which request will be stopped. + // Some requests, for example test generation, might be either really long, or have their own timeouts. + // To honour their timeout logic we do not use RdCall.sync. + return runBlocking { call.startSuspending(req) } +} + +/** + * Terminates lifetime if exception occurs. + * Useful when initializing classes, for ex. if an exception occurs while some parts already bound to lifetime, + * and you need to terminate those parts + */ inline fun LifetimeDefinition.terminateOnException(block: (Lifetime) -> T): T { try { return block(this) @@ -22,17 +45,20 @@ inline fun LifetimeDefinition.terminateOnException(block: (Lifetime) -> T): } } -// suspends until provided lifetime is terminated or coroutine cancelled +/** + * Suspend until provided lifetime terminates or coroutine cancelles + */ suspend fun Lifetime.awaitTermination() { val deferred = CompletableDeferred() this.onTermination { deferred.complete(Unit) } deferred.await() } -// function will return when block completed -// if coroutine was cancelled - CancellationException will be thrown -// if lifetime was terminated before block completed - CancellationException will be thrown -// lambda receives lifetime that indicates whether it's operation is still required +/** + * Executes block on IScheduler and suspends until completed + * @param lifetime indicates whether it's operation is still required + * @throws CancellationException if coroutine was cancelled or lifetime was terminated before block completed +*/ suspend fun IScheduler.pump(lifetime: Lifetime, block: (Lifetime) -> T): T { val ldef = lifetime.createNested() val deferred = CompletableDeferred() @@ -46,11 +72,17 @@ suspend fun IScheduler.pump(lifetime: Lifetime, block: (Lifetime) -> T): T { return deferred.await() } -// deferred will be completed if condition was met -// if condition no more needed - cancel deferred -// if lifetime was terminated before condition was met - deferred will be canceled -// if you need timeout - wrap returned deferred it in withTimeout -suspend fun ISource.adviseForConditionAsync(lifetime: Lifetime, condition: (T) -> Boolean): Deferred { +suspend fun IScheduler.pump(block: (Lifetime) -> T): T = this.pump(Lifetime.Eternal, block) + +/** + * Asynchronously checks the condition. + * The condition can be satisfied or canceled. + * As soon as the condition is checked, the lifetime is terminated. + * In order to cancel the calculation, use the function return value of Deferred type. + * + * @see kotlinx.coroutines.withTimeout coroutine builder in case you need cancel the calculation by timeout. + */ +fun ISource.adviseForConditionAsync(lifetime: Lifetime, condition: (T) -> Boolean): Deferred { val ldef = lifetime.createNested() val deferred = CompletableDeferred() @@ -65,7 +97,7 @@ suspend fun ISource.adviseForConditionAsync(lifetime: Lifetime, condition return deferred } -suspend fun ISource.adviseForConditionAsync(lifetime: Lifetime): Deferred { +fun ISource.adviseForConditionAsync(lifetime: Lifetime): Deferred { return this.adviseForConditionAsync(lifetime) { it } } diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/UtSingleThreadScheduler.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/UtSingleThreadScheduler.kt deleted file mode 100644 index f01e2ba29f..0000000000 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/UtSingleThreadScheduler.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.utbot.rd - -import com.jetbrains.rd.util.error -import com.jetbrains.rd.util.getLogger -import com.jetbrains.rd.util.threading.SingleThreadSchedulerBase - -private val logger = getLogger() - -class UtSingleThreadScheduler(name: String) : SingleThreadSchedulerBase(name) { - override fun onException(ex: Throwable) { - logger.error { "exception on scheduler $name: $ex |> ${ex.stackTraceToString()}" } - } -} \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/exceptions/InstantProcessDeathException.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/exceptions/InstantProcessDeathException.kt new file mode 100644 index 0000000000..015abbf0b0 --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/exceptions/InstantProcessDeathException.kt @@ -0,0 +1,16 @@ +package org.utbot.rd.exceptions + +/** + * This exception is designed to be thrown any time you start child process with rd, + * but it dies before rd initiated, implicating that the problem probably in CLI arguments + */ +abstract class InstantProcessDeathException(private val debugPort: Int, private val isProcessDebug: Boolean) : Exception() { + override val message: String? + get() { + var text = "Process died before any request was executed, check process log file." + if (isProcessDebug) { + text += " Process requested debug on port - $debugPort, check if port is open." + } + return text + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/generated/LoggerModel.Generated.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/LoggerModel.Generated.kt new file mode 100644 index 0000000000..d1dbc4deeb --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/LoggerModel.Generated.kt @@ -0,0 +1,185 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.rd.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [LoggerModel.kt:8] + */ +class LoggerModel private constructor( + private val _initRemoteLogging: RdSignal, + private val _log: RdSignal, + private val _getCategoryMinimalLogLevel: RdOptionalProperty +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + serializers.register(LogArguments) + } + + + @JvmStatic + @JvmName("internalCreateModel") + @Deprecated("Use create instead", ReplaceWith("create(lifetime, protocol)")) + internal fun createModel(lifetime: Lifetime, protocol: IProtocol): LoggerModel { + @Suppress("DEPRECATION") + return create(lifetime, protocol) + } + + @JvmStatic + @Deprecated("Use protocol.loggerModel or revise the extension scope instead", ReplaceWith("protocol.loggerModel")) + fun create(lifetime: Lifetime, protocol: IProtocol): LoggerModel { + LoggerRoot.register(protocol.serializers) + + return LoggerModel() + } + + + const val serializationHash = -4262122198555578601L + + } + override val serializersOwner: ISerializersOwner get() = LoggerModel + override val serializationHash: Long get() = LoggerModel.serializationHash + + //fields + val initRemoteLogging: IAsyncSignal get() = _initRemoteLogging + val log: IAsyncSignal get() = _log + + /** + * Property value - integer for com.jetbrains.rd.util.LogLevel. + */ + val getCategoryMinimalLogLevel: IOptProperty get() = _getCategoryMinimalLogLevel + //methods + //initializer + init { + _getCategoryMinimalLogLevel.optimizeNested = true + } + + init { + _initRemoteLogging.async = true + _log.async = true + _getCategoryMinimalLogLevel.async = true + } + + init { + bindableChildren.add("initRemoteLogging" to _initRemoteLogging) + bindableChildren.add("log" to _log) + bindableChildren.add("getCategoryMinimalLogLevel" to _getCategoryMinimalLogLevel) + } + + //secondary constructor + private constructor( + ) : this( + RdSignal(FrameworkMarshallers.Void), + RdSignal(LogArguments), + RdOptionalProperty(FrameworkMarshallers.Int) + ) + + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("LoggerModel (") + printer.indent { + print("initRemoteLogging = "); _initRemoteLogging.print(printer); println() + print("log = "); _log.print(printer); println() + print("getCategoryMinimalLogLevel = "); _getCategoryMinimalLogLevel.print(printer); println() + } + printer.print(")") + } + //deepClone + override fun deepClone(): LoggerModel { + return LoggerModel( + _initRemoteLogging.deepClonePolymorphic(), + _log.deepClonePolymorphic(), + _getCategoryMinimalLogLevel.deepClonePolymorphic() + ) + } + //contexts +} +val IProtocol.loggerModel get() = getOrCreateExtension(LoggerModel::class) { @Suppress("DEPRECATION") LoggerModel.create(lifetime, this) } + + + +/** + * @property logLevelOrdinal Integer value for com.jetbrains.rd.util.LogLevel + * #### Generated from [LoggerModel.kt:9] + */ +data class LogArguments ( + val category: String, + val logLevelOrdinal: Int, + val message: String +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = LogArguments::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): LogArguments { + val category = buffer.readString() + val logLevelOrdinal = buffer.readInt() + val message = buffer.readString() + return LogArguments(category, logLevelOrdinal, message) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: LogArguments) { + buffer.writeString(value.category) + buffer.writeInt(value.logLevelOrdinal) + buffer.writeString(value.message) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as LogArguments + + if (category != other.category) return false + if (logLevelOrdinal != other.logLevelOrdinal) return false + if (message != other.message) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + category.hashCode() + __r = __r*31 + logLevelOrdinal.hashCode() + __r = __r*31 + message.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("LogArguments (") + printer.indent { + print("category = "); category.print(printer); println() + print("logLevelOrdinal = "); logLevelOrdinal.print(printer); println() + print("message = "); message.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/generated/LoggerRoot.Generated.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/LoggerRoot.Generated.kt new file mode 100644 index 0000000000..ed678d075d --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/LoggerRoot.Generated.kt @@ -0,0 +1,59 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.rd.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [LoggerModel.kt:7] + */ +class LoggerRoot private constructor( +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + LoggerRoot.register(serializers) + LoggerModel.register(serializers) + } + + + + + + const val serializationHash = -3743703762234585836L + + } + override val serializersOwner: ISerializersOwner get() = LoggerRoot + override val serializationHash: Long get() = LoggerRoot.serializationHash + + //fields + //methods + //initializer + //secondary constructor + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("LoggerRoot (") + printer.print(")") + } + //deepClone + override fun deepClone(): LoggerRoot { + return LoggerRoot( + ) + } + //contexts +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/SettingsModel.Generated.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SettingsModel.Generated.kt similarity index 93% rename from utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/SettingsModel.Generated.kt rename to utbot-rd/src/main/kotlin/org/utbot/rd/generated/SettingsModel.Generated.kt index 76a3814baa..13dffb1dca 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/process/generated/SettingsModel.Generated.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SettingsModel.Generated.kt @@ -1,5 +1,5 @@ @file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") -package org.utbot.framework.process.generated +package org.utbot.rd.generated import com.jetbrains.rd.framework.* import com.jetbrains.rd.framework.base.* @@ -9,13 +9,14 @@ import com.jetbrains.rd.util.lifetime.* import com.jetbrains.rd.util.reactive.* import com.jetbrains.rd.util.string.* import com.jetbrains.rd.util.* +import kotlin.time.Duration import kotlin.reflect.KClass import kotlin.jvm.JvmStatic /** - * #### Generated from [SettingsModel.kt:7] + * #### Generated from [SettingsModel.kt:9] */ class SettingsModel private constructor( private val _settingFor: RdCall @@ -41,12 +42,9 @@ class SettingsModel private constructor( @JvmStatic @Deprecated("Use protocol.settingsModel or revise the extension scope instead", ReplaceWith("protocol.settingsModel")) fun create(lifetime: Lifetime, protocol: IProtocol): SettingsModel { - SettingsProtocolRoot.register(protocol.serializers) + SettingsRoot.register(protocol.serializers) - return SettingsModel().apply { - identify(protocol.identity, RdId.Null.mix("SettingsModel")) - bind(lifetime, protocol, "SettingsModel") - } + return SettingsModel() } @@ -97,7 +95,7 @@ val IProtocol.settingsModel get() = getOrCreateExtension(SettingsModel::class) { /** - * #### Generated from [SettingsModel.kt:8] + * #### Generated from [SettingsModel.kt:10] */ data class SettingForArgument ( val key: String, @@ -160,7 +158,7 @@ data class SettingForArgument ( /** - * #### Generated from [SettingsModel.kt:12] + * #### Generated from [SettingsModel.kt:14] */ data class SettingForResult ( val value: String? diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SettingsRoot.Generated.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SettingsRoot.Generated.kt new file mode 100644 index 0000000000..4c17349868 --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SettingsRoot.Generated.kt @@ -0,0 +1,59 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.rd.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [SettingsModel.kt:7] + */ +class SettingsRoot private constructor( +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + SettingsRoot.register(serializers) + SettingsModel.register(serializers) + } + + + + + + const val serializationHash = -414203168893339225L + + } + override val serializersOwner: ISerializersOwner get() = SettingsRoot + override val serializationHash: Long get() = SettingsRoot.serializationHash + + //fields + //methods + //initializer + //secondary constructor + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("SettingsRoot (") + printer.print(")") + } + //deepClone + override fun deepClone(): SettingsRoot { + return SettingsRoot( + ) + } + //contexts +} diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationModel.Generated.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationModel.Generated.kt index dafb2c3887..a42b46fb13 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationModel.Generated.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationModel.Generated.kt @@ -9,16 +9,19 @@ import com.jetbrains.rd.util.lifetime.* import com.jetbrains.rd.util.reactive.* import com.jetbrains.rd.util.string.* import com.jetbrains.rd.util.* +import kotlin.time.Duration import kotlin.reflect.KClass import kotlin.jvm.JvmStatic /** - * #### Generated from [SynchronizationModel.kt:7] + * #### Generated from [SynchronizationModel.kt:8] */ class SynchronizationModel private constructor( - private val _synchronizationSignal: RdSignal + private val _suspendTimeoutTimer: RdCall, + private val _synchronizationSignal: RdSignal, + private val _stopProcess: RdSignal ) : RdExtBase() { //companion @@ -39,37 +42,46 @@ class SynchronizationModel private constructor( @JvmStatic @Deprecated("Use protocol.synchronizationModel or revise the extension scope instead", ReplaceWith("protocol.synchronizationModel")) fun create(lifetime: Lifetime, protocol: IProtocol): SynchronizationModel { - SynchronizationModelRoot.register(protocol.serializers) + SynchronizationRoot.register(protocol.serializers) - return SynchronizationModel().apply { - identify(protocol.identity, RdId.Null.mix("SynchronizationModel")) - bind(lifetime, protocol, "SynchronizationModel") - } + return SynchronizationModel() } - const val serializationHash = -6677090974058917499L + const val serializationHash = -8851121542976813112L } override val serializersOwner: ISerializersOwner get() = SynchronizationModel override val serializationHash: Long get() = SynchronizationModel.serializationHash //fields + val suspendTimeoutTimer: RdCall get() = _suspendTimeoutTimer val synchronizationSignal: IAsyncSignal get() = _synchronizationSignal + + /** + * This command tells the instrumented process to stop + */ + val stopProcess: IAsyncSignal get() = _stopProcess //methods //initializer init { + _suspendTimeoutTimer.async = true _synchronizationSignal.async = true + _stopProcess.async = true } init { + bindableChildren.add("suspendTimeoutTimer" to _suspendTimeoutTimer) bindableChildren.add("synchronizationSignal" to _synchronizationSignal) + bindableChildren.add("stopProcess" to _stopProcess) } //secondary constructor private constructor( ) : this( - RdSignal(FrameworkMarshallers.String) + RdCall(FrameworkMarshallers.Bool, FrameworkMarshallers.Void), + RdSignal(FrameworkMarshallers.String), + RdSignal(FrameworkMarshallers.Void) ) //equals trait @@ -78,14 +90,18 @@ class SynchronizationModel private constructor( override fun print(printer: PrettyPrinter) { printer.println("SynchronizationModel (") printer.indent { + print("suspendTimeoutTimer = "); _suspendTimeoutTimer.print(printer); println() print("synchronizationSignal = "); _synchronizationSignal.print(printer); println() + print("stopProcess = "); _stopProcess.print(printer); println() } printer.print(")") } //deepClone override fun deepClone(): SynchronizationModel { return SynchronizationModel( - _synchronizationSignal.deepClonePolymorphic() + _suspendTimeoutTimer.deepClonePolymorphic(), + _synchronizationSignal.deepClonePolymorphic(), + _stopProcess.deepClonePolymorphic() ) } //contexts diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationModelRoot.Generated.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationModelRoot.Generated.kt deleted file mode 100644 index 32b75c35ed..0000000000 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationModelRoot.Generated.kt +++ /dev/null @@ -1,58 +0,0 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") -package org.utbot.rd.generated - -import com.jetbrains.rd.framework.* -import com.jetbrains.rd.framework.base.* -import com.jetbrains.rd.framework.impl.* - -import com.jetbrains.rd.util.lifetime.* -import com.jetbrains.rd.util.reactive.* -import com.jetbrains.rd.util.string.* -import com.jetbrains.rd.util.* -import kotlin.reflect.KClass -import kotlin.jvm.JvmStatic - - - -/** - * #### Generated from [SynchronizationModel.kt:5] - */ -class SynchronizationModelRoot private constructor( -) : RdExtBase() { - //companion - - companion object : ISerializersOwner { - - override fun registerSerializersCore(serializers: ISerializers) { - SynchronizationModelRoot.register(serializers) - SynchronizationModel.register(serializers) - } - - - - - - const val serializationHash = -1304011640135373779L - - } - override val serializersOwner: ISerializersOwner get() = SynchronizationModelRoot - override val serializationHash: Long get() = SynchronizationModelRoot.serializationHash - - //fields - //methods - //initializer - //secondary constructor - //equals trait - //hash code trait - //pretty print - override fun print(printer: PrettyPrinter) { - printer.println("SynchronizationModelRoot (") - printer.print(")") - } - //deepClone - override fun deepClone(): SynchronizationModelRoot { - return SynchronizationModelRoot( - ) - } - //contexts -} diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationRoot.Generated.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationRoot.Generated.kt new file mode 100644 index 0000000000..faf8b2a2ce --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/generated/SynchronizationRoot.Generated.kt @@ -0,0 +1,59 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.rd.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [SynchronizationModel.kt:6] + */ +class SynchronizationRoot private constructor( +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + SynchronizationRoot.register(serializers) + SynchronizationModel.register(serializers) + } + + + + + + const val serializationHash = -8945393054954668256L + + } + override val serializersOwner: ISerializersOwner get() = SynchronizationRoot + override val serializationHash: Long get() = SynchronizationRoot.serializationHash + + //fields + //methods + //initializer + //secondary constructor + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("SynchronizationRoot (") + printer.print(")") + } + //deepClone + override fun deepClone(): SynchronizationRoot { + return SynchronizationRoot( + ) + } + //contexts +} diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdConsoleLogger.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdConsoleLogger.kt index 6a18fe1544..b1e5dfd01e 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdConsoleLogger.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdConsoleLogger.kt @@ -1,33 +1,31 @@ package org.utbot.rd.loggers -import com.jetbrains.rd.util.LogLevel -import com.jetbrains.rd.util.Logger -import com.jetbrains.rd.util.defaultLogFormat -import org.utbot.common.dateFormatter +import com.jetbrains.rd.util.* +import org.utbot.common.timeFormatter import java.io.PrintStream import java.time.LocalDateTime class UtRdConsoleLogger( private val loggersLevel: LogLevel, private val streamToWrite: PrintStream, - private val category: String = "" + private val category: String ) : Logger { override fun isEnabled(level: LogLevel): Boolean { return level >= loggersLevel } + private fun format(category: String, level: LogLevel, message: Any?, throwable: Throwable?) : String { + val throwableToPrint = if (level < LogLevel.Error) throwable else throwable ?: Exception("No exception was actually thrown, this exception is used purely to log trace") + val rdCategory = if (category.isNotEmpty()) "RdCategory: ${category.substringAfterLast('.').padEnd(25)} | " else "" + return "${LocalDateTime.now().format(timeFormatter)} | ${level.toString().uppercase().padEnd(5)} | $rdCategory${message?.toString()?:""} ${throwableToPrint?.getThrowableText()?.let { "| $it" }?:""}" + } + override fun log(level: LogLevel, message: Any?, throwable: Throwable?) { if (!isEnabled(level)) return - val msg = LocalDateTime.now().format(dateFormatter) + " | ${ - defaultLogFormat( - category, - level, - message, - throwable - ) - }" + val msg = format(category, level, message, throwable) + streamToWrite.println(msg) } } \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdConsoleLoggerFactory.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdConsoleLoggerFactory.kt index c34e6a3788..ad2d02a8a1 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdConsoleLoggerFactory.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdConsoleLoggerFactory.kt @@ -5,6 +5,10 @@ import com.jetbrains.rd.util.LogLevel import com.jetbrains.rd.util.Logger import java.io.PrintStream +/** + * Creates loggers with predefined log level, that writes to provided stream. + * Create logger category is added to message. + */ class UtRdConsoleLoggerFactory( private val loggersLevel: LogLevel, private val streamToWrite: PrintStream diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdKLogger.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdKLogger.kt index c72de72d44..4016eb2b36 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdKLogger.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdKLogger.kt @@ -1,29 +1,47 @@ package org.utbot.rd.loggers -import com.jetbrains.rd.util.LogLevel -import com.jetbrains.rd.util.Logger -import com.jetbrains.rd.util.defaultLogFormat +import com.jetbrains.rd.util.* import mu.KLogger -import org.utbot.common.dateFormatter -import java.time.LocalDateTime -class UtRdKLogger(private val realLogger: KLogger, private val category: String): Logger { - override fun isEnabled(level: LogLevel): Boolean { - return when (level) { - LogLevel.Trace -> realLogger.isTraceEnabled - LogLevel.Debug -> realLogger.isDebugEnabled - LogLevel.Info -> realLogger.isInfoEnabled - LogLevel.Warn -> realLogger.isWarnEnabled - LogLevel.Error -> realLogger.isErrorEnabled - LogLevel.Fatal -> realLogger.isErrorEnabled +/** + * Adapter from RD Logger to KLogger + */ +class UtRdKLogger( + private val realLogger: KLogger, + val category: String, + private val logTraceOnError: Boolean = true +) : Logger { + val logLevel: LogLevel + get() { + return when { + realLogger.isTraceEnabled -> LogLevel.Trace + realLogger.isDebugEnabled -> LogLevel.Debug + realLogger.isInfoEnabled -> LogLevel.Info + realLogger.isWarnEnabled -> LogLevel.Warn + realLogger.isErrorEnabled -> LogLevel.Error + else -> LogLevel.Fatal + } } + + override fun isEnabled(level: LogLevel): Boolean { + return level >= logLevel + } + + private fun format(level: LogLevel, message: Any?, throwable: Throwable?): String { + val throwableToPrint = + if (logTraceOnError && level >= LogLevel.Error) + throwable ?: Exception("No exception was actually thrown, this exception is used purely to log trace") + else + throwable + val rdCategory = if (category.isNotEmpty()) "RdCategory: ${category.substringAfterLast('.').padEnd(25)} | " else "" + return "$rdCategory${message?.toString() ?: ""} ${throwableToPrint?.getThrowableText()?.let { "| $it" } ?: ""}" } override fun log(level: LogLevel, message: Any?, throwable: Throwable?) { - if (!isEnabled(level)) + if (!isEnabled(level) || (message == null && throwable == null)) return - val msg = LocalDateTime.now().format(dateFormatter) + " | ${defaultLogFormat(category, level, message, throwable)}" + val msg = format(level, message, throwable) when (level) { LogLevel.Trace -> realLogger.trace(msg) diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdKLoggerFactory.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdKLoggerFactory.kt index 4c1f8910e8..7982c963f3 100644 --- a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdKLoggerFactory.kt +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdKLoggerFactory.kt @@ -4,6 +4,11 @@ import com.jetbrains.rd.util.ILoggerFactory import com.jetbrains.rd.util.Logger import mu.KLogger +/** + * Creates RD loggers that writes to provided KLogger. + * + * Created logger category is added to message. + */ class UtRdKLoggerFactory(private val realLogger: KLogger) : ILoggerFactory { override fun getLogger(category: String): Logger { return UtRdKLogger(realLogger, category) diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdLogUtil.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdLogUtil.kt new file mode 100644 index 0000000000..bc32202782 --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdLogUtil.kt @@ -0,0 +1,45 @@ +package org.utbot.rd.loggers + +import com.jetbrains.rd.util.* +import com.jetbrains.rd.util.lifetime.Lifetime +import mu.KLogger +import org.utbot.common.LoggerWithLogMethod +import org.utbot.rd.generated.LoggerModel + +fun Logger.withLevel(logLevel: LogLevel): LoggerWithLogMethod = LoggerWithLogMethod { + this.log(logLevel, it) +} + +fun Logger.info(): LoggerWithLogMethod = LoggerWithLogMethod { + this.info(it) +} +fun Logger.debug(): LoggerWithLogMethod = LoggerWithLogMethod { + this.debug(it) +} +fun Logger.trace(): LoggerWithLogMethod = LoggerWithLogMethod { + this.trace(it) +} + +fun overrideDefaultRdLoggerFactoryWithKLogger(logger: KLogger) { + if (Statics().get() !is UtRdKLoggerFactory) { + Logger.set(Lifetime.Eternal, UtRdKLoggerFactory(logger)) + } +} + +fun LoggerModel.setup(logger: KLogger, processLifetime: Lifetime) { + // we do not log trace on error here because it's a job of clint-side process logger + // server-side process logger can only log trace of where it was set up from which isn't particularly useful + val rdLogger = UtRdKLogger(logger, "", logTraceOnError = false) + // currently we do not specify log level for different categories + // though it is possible with some additional map on categories -> consider performance + // this logLevel is obtained from KotlinLogger + getCategoryMinimalLogLevel.set(rdLogger.logLevel.ordinal) + + log.advise(processLifetime) { + val logLevel = UtRdRemoteLogger.logLevelValues[it.logLevelOrdinal] + // assume throwable already in message + rdLogger.log(logLevel, it.message, null) + } + + initRemoteLogging.fire(Unit) +} diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdRemoteLogger.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdRemoteLogger.kt new file mode 100644 index 0000000000..648f32ab35 --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdRemoteLogger.kt @@ -0,0 +1,63 @@ +package org.utbot.rd.loggers + +import com.jetbrains.rd.util.LogLevel +import com.jetbrains.rd.util.Logger +import com.jetbrains.rd.util.collections.CountingSet +import com.jetbrains.rd.util.getThrowableText +import com.jetbrains.rd.util.reactive.valueOrThrow +import com.jetbrains.rd.util.threadLocalWithInitial +import org.utbot.rd.generated.LogArguments +import org.utbot.rd.generated.LoggerModel +import org.utbot.rd.startBlocking + +class UtRdRemoteLogger( + private val loggerModel: LoggerModel, + private val category: String +) : Logger { + private val logLevel: LogLevel by lazy { logLevelValues[loggerModel.getCategoryMinimalLogLevel.valueOrThrow] } + + companion object { + val logLevelValues = LogLevel.values() + private val threadLocalExecutingBackingFiled: ThreadLocal> = + threadLocalWithInitial { CountingSet() } + + internal val threadLocalExecuting get() = threadLocalExecutingBackingFiled.get() + } + + override fun isEnabled(level: LogLevel): Boolean { + // On every protocol sends/receives event RD to its own loggers. + // They will be redirected here, and then sent via RD to another process, + // producing new log event again thus causing infinite recursion. + // The solution is to prohibit writing any logs from inside logger. + // This is implemented via thread local counter per logger, + // which will be incremented when this logger fires event to another process, + // and deny all following log events until previous log event is delivered. + if (threadLocalExecuting[this] > 0) + return false + + return level >= logLevel + } + + private fun format(message: Any?, throwable: Throwable?): String { + val rdCategory = if (category.isNotEmpty()) "RdCategory: ${category.substringAfterLast('.').padEnd(25)} | " else "" + return "$rdCategory${message?.toString() ?: ""}${ + throwable?.getThrowableText()?.let { "${message?.let { " | " } ?: ""}$it" } ?: "" + }" + } + + override fun log(level: LogLevel, message: Any?, throwable: Throwable?) { + if (!isEnabled(level) || message == null && throwable == null) + return + + threadLocalExecuting.add(this, +1) + try { + val renderedMsg = format(message, throwable) + val args = LogArguments(category, level.ordinal, renderedMsg) + + loggerModel.log.fire(args) + } finally { + threadLocalExecuting.add(this, -1) + } + } + +} \ No newline at end of file diff --git a/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdRemoteLoggerFactory.kt b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdRemoteLoggerFactory.kt new file mode 100644 index 0000000000..e2024376a3 --- /dev/null +++ b/utbot-rd/src/main/kotlin/org/utbot/rd/loggers/UtRdRemoteLoggerFactory.kt @@ -0,0 +1,17 @@ +package org.utbot.rd.loggers + +import com.jetbrains.rd.util.ILoggerFactory +import com.jetbrains.rd.util.Logger +import org.utbot.rd.generated.LoggerModel + +/** + * Creates loggers that are mapped to the remote counter-part. + * Category is added to message +*/ +class UtRdRemoteLoggerFactory( + private val loggerModel: LoggerModel +) : ILoggerFactory { + override fun getLogger(category: String): Logger { + return UtRdRemoteLogger(loggerModel, category) + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/CSharpModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/CSharpModel.kt new file mode 100644 index 0000000000..cc75a8ff12 --- /dev/null +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/CSharpModel.kt @@ -0,0 +1,44 @@ +@file:Suppress("unused") +package org.utbot.rd.models + +import com.jetbrains.rd.generator.nova.* + +object CSharpRoot: Root() + +object VSharpModel: Ext(CSharpRoot) { + val methodDescriptor = structdef { + field("methodName", PredefinedType.string) + field("typeName", PredefinedType.string) + field("hasNoOverloads", PredefinedType.bool) + field("parameters", immutableList(PredefinedType.string)) + } + + val mapEntry = structdef { + field("key", PredefinedType.string) + field("value", PredefinedType.string) + } + + val generateArguments = structdef { + field("assemblyPath", PredefinedType.string) + field("projectCsprojPath", PredefinedType.string) + field("solutionFilePath", PredefinedType.string) + field("methods", immutableList(methodDescriptor)) + field("generationTimeoutInSeconds", PredefinedType.int) + field("targetFramework", PredefinedType.string.nullable) + field("assembliesFullNameToTheirPath", immutableList(mapEntry)) + } + + val generateResults = structdef { + field("generatedProjectPath", PredefinedType.string.nullable) + field("generatedFilesPaths", immutableList(PredefinedType.string)) + field("exceptionMessage", PredefinedType.string.nullable) + field("testsCount", PredefinedType.int) + field("errorsCount", PredefinedType.int) + } + + init { + call("generate", generateArguments, generateResults).async + signal("ping", PredefinedType.string).async + signal("log", PredefinedType.string).async + } +} diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/ChildProcessModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/ChildProcessModel.kt deleted file mode 100644 index 3a18853a39..0000000000 --- a/utbot-rd/src/main/rdgen/org/utbot/rd/models/ChildProcessModel.kt +++ /dev/null @@ -1,85 +0,0 @@ -package org.utbot.rd.models - -import com.jetbrains.rd.generator.nova.* - -object ChildProcessProtocolRoot : Root() - -object ChildProcessModel : Ext(ChildProcessProtocolRoot) { - val AddPathsParams = structdef { - field("pathsToUserClasses", PredefinedType.string) - field("pathsToDependencyClasses", PredefinedType.string) - } - - val SetInstrumentationParams = structdef { - field("instrumentation", array(PredefinedType.byte)) - } - - val InvokeMethodCommandParams = structdef { - field("classname", PredefinedType.string) - field("signature", PredefinedType.string) - field("arguments", array(PredefinedType.byte)) - field("parameters", array(PredefinedType.byte)) - } - - val InvokeMethodCommandResult = structdef { - field("result", array(PredefinedType.byte)) - } - - val CollectCoverageParams = structdef { - field("clazz", array(PredefinedType.byte)) - } - - val CollectCoverageResult = structdef { - field("coverageInfo", array(PredefinedType.byte)) - } - - val ComputeStaticFieldParams = structdef { - field("fieldId", array(PredefinedType.byte)) - } - - val ComputeStaticFieldResult = structdef { - field("result", array(PredefinedType.byte)) - } - - init { - call("AddPaths", AddPathsParams, PredefinedType.void).apply { - async - documentation = - "The main process tells where the child process should search for the classes" - } - call("Warmup", PredefinedType.void, PredefinedType.void).apply { - async - documentation = - "Load classes from classpath and instrument them" - } - call("SetInstrumentation", SetInstrumentationParams, PredefinedType.void).apply { - async - documentation = - "The main process sends [instrumentation] to the child process" - } - call("InvokeMethodCommand", InvokeMethodCommandParams, InvokeMethodCommandResult).apply { - async - documentation = - "The main process requests the child process to execute a method with the given [signature],\n" + - "which declaring class's name is [className].\n" + - "@property parameters are the parameters needed for an execution, e.g. static environment" - } - call("StopProcess", PredefinedType.void, PredefinedType.void).apply { - async - documentation = - "This command tells the child process to stop" - } - call("CollectCoverage", CollectCoverageParams, CollectCoverageResult).apply { - async - documentation = - "This command is sent to the child process from the [ConcreteExecutor] if user wants to collect coverage for the\n" + - "[clazz]" - } - call("ComputeStaticField", ComputeStaticFieldParams, ComputeStaticFieldResult).apply { - async - documentation = - "This command is sent to the child process from the [ConcreteExecutor] if user wants to get value of static field\n" + - "[fieldId]" - } - } -} \ No newline at end of file diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/EngineProcessModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/EngineProcessModel.kt index d54c9d31ee..bc8ffcabfb 100644 --- a/utbot-rd/src/main/rdgen/org/utbot/rd/models/EngineProcessModel.kt +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/EngineProcessModel.kt @@ -1,39 +1,52 @@ +@file:Suppress("unused") package org.utbot.rd.models import com.jetbrains.rd.generator.nova.* -object EngineProcessProtocolRoot : Root() +object EngineProcessRoot : Root() -object RdSourceFindingStrategy : Ext(EngineProcessProtocolRoot) { - val sourceStrategeMethodArgs = structdef { +object RdInstrumenterAdapter: Ext(EngineProcessRoot) { + val computeSourceFileByClassArguments = structdef { + field("canonicalClassName", PredefinedType.string) + } + init { + call("computeSourceFileByClass", computeSourceFileByClassArguments, PredefinedType.string.nullable).async + } +} + +object RdSourceFindingStrategy : Ext(EngineProcessRoot) { + val sourceStrategyMethodArgs = structdef { + field("testSetId", PredefinedType.long) field("classFqn", PredefinedType.string) field("extension", PredefinedType.string.nullable) } init { - call("testsRelativePath", PredefinedType.void, PredefinedType.string).async - call("getSourceRelativePath", sourceStrategeMethodArgs, PredefinedType.string).async - call("getSourceFile", sourceStrategeMethodArgs, PredefinedType.string.nullable).async + call("testsRelativePath", PredefinedType.long, PredefinedType.string).async + call("getSourceRelativePath", sourceStrategyMethodArgs, PredefinedType.string).async + call("getSourceFile", sourceStrategyMethodArgs, PredefinedType.string.nullable).async } } -object EngineProcessModel : Ext(EngineProcessProtocolRoot) { +object EngineProcessModel : Ext(EngineProcessRoot) { val jdkInfo = structdef { field("path", PredefinedType.string) field("version", PredefinedType.int) } - val testGeneratorParams = structdef { field("buildDir", array(PredefinedType.string)) field("classpath", PredefinedType.string.nullable) field("dependencyPaths", PredefinedType.string) field("jdkInfo", jdkInfo) + field("applicationContext", array(PredefinedType.byte)) + } + val testClassNameParams = structdef { + field("classUnderTest", array(PredefinedType.byte)) + } + val testClassNameResult = structdef { + field("testClassName", PredefinedType.string) } val generateParams = structdef { - // mocks - field("mockInstalled", PredefinedType.bool) - field("staticsMockingIsConfigureda", PredefinedType.bool) - field("conflictTriggers", array(PredefinedType.byte)) // generate field("methods", array(PredefinedType.byte)) field("mockStrategy", PredefinedType.string) @@ -46,6 +59,8 @@ object EngineProcessModel : Ext(EngineProcessProtocolRoot) { field("fuzzingValue", PredefinedType.double) // method filters field("searchDirectory", PredefinedType.string) + // taint analysis + field("taintConfigPath", PredefinedType.string.nullable) } val generateResult = structdef { field("notEmptyCases", PredefinedType.int) @@ -54,6 +69,7 @@ object EngineProcessModel : Ext(EngineProcessProtocolRoot) { val renderParams = structdef { field("testSetsId", PredefinedType.long) field("classUnderTest", array(PredefinedType.byte)) + field("projectType", PredefinedType.string) field("paramNames", array(PredefinedType.byte)) field("generateUtilClassFile", PredefinedType.bool) field("testFramework", PredefinedType.string) @@ -70,18 +86,19 @@ object EngineProcessModel : Ext(EngineProcessProtocolRoot) { } val renderResult = structdef { field("generatedCode", PredefinedType.string) - field("utilClassKind", array(PredefinedType.byte)) + field("utilClassKind", PredefinedType.string.nullable) } val setupContextParams = structdef { field("classpathForUrlsClassloader", immutableList(PredefinedType.string)) } - val signature = structdef { + val methodDescription = structdef { field("name", PredefinedType.string) + field("containingClass", PredefinedType.string.nullable) field("parametersTypes", immutableList(PredefinedType.string.nullable)) } val findMethodsInClassMatchingSelectedArguments = structdef { field("classId", array(PredefinedType.byte)) - field("signatures", immutableList(signature)) + field("methodDescriptions", immutableList(methodDescription)) } val findMethodsInClassMatchingSelectedResult = structdef { field("executableIds", array(PredefinedType.byte)) @@ -112,17 +129,22 @@ object EngineProcessModel : Ext(EngineProcessProtocolRoot) { field("statistics", PredefinedType.string.nullable) field("hasWarnings", PredefinedType.bool) } + val performParams = structdef { + field("engineProcessTask", array(PredefinedType.byte)) + } + init { call("setupUtContext", setupContextParams, PredefinedType.void).async call("createTestGenerator", testGeneratorParams, PredefinedType.void).async call("isCancelled", PredefinedType.void, PredefinedType.bool).async + call("findTestClassName", testClassNameParams, testClassNameResult).async call("generate", generateParams, generateResult).async call("render", renderParams, renderResult).async - call("stopProcess", PredefinedType.void, PredefinedType.void).async call("obtainClassId", PredefinedType.string, array(PredefinedType.byte)).async call("findMethodsInClassMatchingSelected", findMethodsInClassMatchingSelectedArguments, findMethodsInClassMatchingSelectedResult).async call("findMethodParamNames", findMethodParamNamesArguments, findMethodParamNamesResult).async - call("writeSarifReport", writeSarifReportArguments, PredefinedType.void).async + call("writeSarifReport", writeSarifReportArguments, PredefinedType.string).async call("generateTestReport", generateTestReportArgs, generateTestReportResult).async + call("perform", performParams, array(PredefinedType.byte)).async } } \ No newline at end of file diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt new file mode 100644 index 0000000000..f9d3d76f50 --- /dev/null +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt @@ -0,0 +1,103 @@ +@file:Suppress("unused") +package org.utbot.rd.models + +import com.jetbrains.rd.generator.nova.* + +object InstrumentedProcessRoot : Root() + +object InstrumentedProcessModel : Ext(InstrumentedProcessRoot) { + val AddPathsParams = structdef { + field("pathsToUserClasses", PredefinedType.string) + } + + val SetInstrumentationParams = structdef { + field("instrumentation", array(PredefinedType.byte)) + field("useBytecodeTransformation", PredefinedType.bool) + } + + val InvokeMethodCommandParams = structdef { + field("classname", PredefinedType.string) + field("signature", PredefinedType.string) + field("arguments", array(PredefinedType.byte)) + field("parameters", array(PredefinedType.byte)) + } + + val InvokeMethodCommandResult = structdef { + field("result", array(PredefinedType.byte)) + } + + val CollectCoverageParams = structdef { + field("clazz", array(PredefinedType.byte)) + } + + val CollectCoverageResult = structdef { + field("coverageInfo", array(PredefinedType.byte)) + } + + val ComputeStaticFieldParams = structdef { + field("fieldId", array(PredefinedType.byte)) + } + + val ComputeStaticFieldResult = structdef { + field("result", array(PredefinedType.byte)) + } + + val GetSpringRepositoriesParams = structdef { + field("classId", array(PredefinedType.byte)) + } + + val GetSpringRepositoriesResult = structdef { + field("springRepositoryIds", array(PredefinedType.byte)) + } + + val TryLoadingSpringContextResult = structdef { + field("springContextLoadingResult", array(PredefinedType.byte)) + } + + init { + call("AddPaths", AddPathsParams, PredefinedType.void).apply { + async + documentation = + "The main process tells where the instrumented process should search for the classes" + } + call("Warmup", PredefinedType.void, PredefinedType.void).apply { + async + documentation = + "Load classes from classpath and instrument them" + } + call("SetInstrumentation", SetInstrumentationParams, PredefinedType.void).apply { + async + documentation = + "The main process sends [instrumentation] to the instrumented process" + } + call("InvokeMethodCommand", InvokeMethodCommandParams, InvokeMethodCommandResult).apply { + async + documentation = + "The main process requests the instrumented process to execute a method with the given [signature],\n" + + "which declaring class's name is [className].\n" + + "@property parameters are the parameters needed for an execution, e.g. static environment" + } + call("CollectCoverage", CollectCoverageParams, CollectCoverageResult).apply { + async + documentation = + "This command is sent to the instrumented process from the [ConcreteExecutor] if user wants to collect coverage for the\n" + + "[clazz]" + } + call("ComputeStaticField", ComputeStaticFieldParams, ComputeStaticFieldResult).apply { + async + documentation = + "This command is sent to the instrumented process from the [ConcreteExecutor] if user wants to get value of static field\n" + + "[fieldId]" + } + call("getRelevantSpringRepositories", GetSpringRepositoriesParams, GetSpringRepositoriesResult).apply { + async + documentation = "Gets a list of [SpringRepositoryId]s that class specified by the [ClassId]" + + " (possibly indirectly) depends on (requires Spring instrumentation)" + } + call("tryLoadingSpringContext", PredefinedType.void, TryLoadingSpringContextResult).apply { + async + documentation = "This command is sent to the instrumented process from the [ConcreteExecutor]\n" + + "if the user wants to determine whether or not Spring application context can load" + } + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/LoggerModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/LoggerModel.kt new file mode 100644 index 0000000000..cdcd148727 --- /dev/null +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/LoggerModel.kt @@ -0,0 +1,23 @@ +@file:Suppress("unused") + +package org.utbot.rd.models + +import com.jetbrains.rd.generator.nova.* + +object LoggerRoot : Root() +object LoggerModel : Ext(LoggerRoot) { + val logArguments = structdef { + field("category", PredefinedType.string) + field("logLevelOrdinal", PredefinedType.int).doc("Integer value for com.jetbrains.rd.util.LogLevel") + field("message", PredefinedType.string) + } + + init { + signal("initRemoteLogging", PredefinedType.void).async + signal("log", logArguments).async + property( + "getCategoryMinimalLogLevel", + PredefinedType.int + ).async.doc("Property value - integer for com.jetbrains.rd.util.LogLevel.") + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/SettingsModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/SettingsModel.kt index 605b573c78..7cef4ed4b4 100644 --- a/utbot-rd/src/main/rdgen/org/utbot/rd/models/SettingsModel.kt +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/SettingsModel.kt @@ -1,10 +1,12 @@ +@file:Suppress("unused") + package org.utbot.rd.models import com.jetbrains.rd.generator.nova.* -object SettingsProtocolRoot: Root() +object SettingsRoot: Root() -object SettingsModel : Ext(SettingsProtocolRoot) { +object SettingsModel : Ext(SettingsRoot) { val settingForArgument = structdef { field("key", PredefinedType.string) field("propertyName", PredefinedType.string) diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/SpringAnalyzerModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/SpringAnalyzerModel.kt new file mode 100644 index 0000000000..e5474cae67 --- /dev/null +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/SpringAnalyzerModel.kt @@ -0,0 +1,32 @@ +@file:Suppress("unused") +package org.utbot.rd.models + +import com.jetbrains.rd.generator.nova.* + +object SpringAnalyzerRoot : Root() + +object SpringAnalyzerProcessModel : Ext(SpringAnalyzerRoot) { + val springAnalyzerParams = structdef { + field("springSettings", array(PredefinedType.byte)) + } + + val beanAdditionalData = structdef { + field("factoryMethodName", PredefinedType.string) + field("parameterTypes", immutableList(PredefinedType.string)) + field("configClassFqn", PredefinedType.string) + } + + val beanDefinitionData = structdef { + field("beanName", PredefinedType.string) + field("beanTypeFqn", PredefinedType.string) + field("additionalData", beanAdditionalData.nullable) + } + + val springAnalyzerResult = structdef { + field("beanDefinitions", array(beanDefinitionData)) + } + + init { + call("analyze", springAnalyzerParams, springAnalyzerResult).async + } +} diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/SynchronizationModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/SynchronizationModel.kt index 4484671419..f0d86d23fa 100644 --- a/utbot-rd/src/main/rdgen/org/utbot/rd/models/SynchronizationModel.kt +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/SynchronizationModel.kt @@ -1,11 +1,18 @@ +@file:Suppress("unused") package org.utbot.rd.models import com.jetbrains.rd.generator.nova.* -object SynchronizationModelRoot: Root() +object SynchronizationRoot: Root() -object SynchronizationModel: Ext(SynchronizationModelRoot) { +object SynchronizationModel: Ext(SynchronizationRoot) { init { + call("suspendTimeoutTimer", PredefinedType.bool, PredefinedType.void).async signal("synchronizationSignal", PredefinedType.string).async + signal("StopProcess", PredefinedType.void).apply { + async + documentation = + "This command tells the instrumented process to stop" + } } } \ No newline at end of file diff --git a/utbot-rd/src/main/riderRdgenModels/org/utbot/rider/rd/models/UtBotRiderModel.kt b/utbot-rd/src/main/riderRdgenModels/org/utbot/rider/rd/models/UtBotRiderModel.kt new file mode 100644 index 0000000000..17b6e52ccb --- /dev/null +++ b/utbot-rd/src/main/riderRdgenModels/org/utbot/rider/rd/models/UtBotRiderModel.kt @@ -0,0 +1,25 @@ +@file:Suppress("unused") + +package org.utbot.rider.rd.models + +import com.jetbrains.rd.generator.nova.* +import com.jetbrains.rider.model.nova.ide.SolutionModel + +object UtBotRiderModel : Ext(SolutionModel.Solution) { + val startPublishArgs = structdef { + field("fileName", PredefinedType.string) + field("arguments", PredefinedType.string) + field("workingDirectory", PredefinedType.string) + } + + init { + signal("startPublish", startPublishArgs).async + signal("logPublishOutput", PredefinedType.string).async + signal("logPublishError", PredefinedType.string).async + signal("stopPublish", PredefinedType.int).async + + signal("startVSharp", PredefinedType.void).async + signal("logVSharp", PredefinedType.string).async + signal("stopVSharp", PredefinedType.int).async + } +} \ No newline at end of file diff --git a/utbot-rider/.gitignore b/utbot-rider/.gitignore new file mode 100644 index 0000000000..347c41ffef --- /dev/null +++ b/utbot-rider/.gitignore @@ -0,0 +1,233 @@ +bin/ +obj/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# Rider +.idea +*.iml + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +## TODO: Comment the next line if you want to checkin your +## web deploy settings but do note that will include unencrypted +## passwords +#*.pubxml + +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml + +# Custom files +VSharp.Test/Tests/VSharp.CSharpUtils/VSharp.CSharpUtils.dll + +*DS_Store + +# CLR interaction files +VSharp.ClrInteraction/sdk/* +VSharp.ClrInteraction/packs/* +VSharp.ClrInteraction/shared/* +VSharp.ClrInteraction/host/* +**/cmake-build-debug/** + +# Rendered tests +VSharp.Test/GeneratedTests/** diff --git a/utbot-rider/build.gradle.kts b/utbot-rider/build.gradle.kts new file mode 100644 index 0000000000..e88eef51bf --- /dev/null +++ b/utbot-rider/build.gradle.kts @@ -0,0 +1,86 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +val semVer: String? by rootProject +val rdVersion: String? by rootProject + +plugins { + id("org.jetbrains.intellij") version "1.13.1" +} + +intellij { + type.set("RD") + version.set("2023.1") +} + +tasks { + register("addRiderModelsToUtbotModels") { + val rdLibDirectory = File(project.tasks.setupDependencies.get().idea.get().classes, "lib/rd/rider-model.jar") + from(rdLibDirectory) + val utbotRd = project.rootProject.childProjects["utbot-rd"]!! + val targetDir = utbotRd.buildDir.resolve("libs") + into(targetDir) + } + val dotNetSdkCmdPath = projectDir.resolve("dotnet-sdk.cmd").toString() + + compileKotlin { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + runIde { + jvmArgs("-Xmx2048m") + } + + patchPluginXml { + sinceBuild.set("231") + version.set(semVer) + } + + buildSearchableOptions { + enabled = false + } + + val chmodDotnet = create("chmodDotnet") { + group = "build" + doLast { + exec { + commandLine = listOf( + "chmod", + "+x", + dotNetSdkCmdPath + ) + } + } + } + + val publishDotNet = create("publishDotNet") { + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + dependsOn(chmodDotnet) + } + group = "build" + doLast { + exec { + commandLine = listOf( + dotNetSdkCmdPath, + "publish", + projectDir.resolve("src/dotnet/UtBot/UtBot.sln").toString() + ) + } + } + } + + prepareSandbox { + dependsOn(publishDotNet) + from("src/dotnet/UtBot/UtBot/bin/Debug/net6.0/publish") { + into("${intellij.pluginName.get()}/dotnet") } + } + +} diff --git a/utbot-rider/dotnet-sdk.cmd b/utbot-rider/dotnet-sdk.cmd new file mode 100644 index 0000000000..9a08efc811 --- /dev/null +++ b/utbot-rider/dotnet-sdk.cmd @@ -0,0 +1,266 @@ +:<<"::CMDLITERAL" +@ECHO OFF +GOTO :CMDSCRIPT +::CMDLITERAL + +set -eu + +SCRIPT_VERSION=dotnet-cmd-v2 +COMPANY_DIR="UtBot" +TARGET_DIR="${TEMPDIR:-$HOME/.local/share}/$COMPANY_DIR/dotnet-cmd" +KEEP_ROSETTA2=false +export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true +export DOTNET_CLI_TELEMETRY_OPTOUT=true + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +retry_on_error () { + local n="$1" + shift + + for i in $(seq 2 "$n"); do + "$@" 2>&1 && return || echo "WARNING: Command '$1' returned non-zero exit status $?, try again" + done + "$@" +} + +is_linux_musl () { + (ldd --version 2>&1 || true) | grep -q musl +} + +case $(uname) in +Darwin) + DOTNET_ARCH=$(uname -m) + if ! $KEEP_ROSETTA2 && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = "1" ]; then + DOTNET_ARCH=arm64 + fi + case $DOTNET_ARCH in + x86_64) + DOTNET_HASH_URL=cf3e1c73-a9a9-4e08-8607-8f9edae5f3f2/40a021a98a6b6e430a1f170037735f6f + DOTNET_FILE_NAME=dotnet-sdk-6.0.301-osx-x64 + ;; + arm64) + DOTNET_HASH_URL=3859fff3-f8a9-4e05-87cd-bd6db75833f5/64ec1099d45f85d14099da3c1f92a5c3 + DOTNET_FILE_NAME=dotnet-sdk-6.0.301-osx-arm64 + ;; + *) echo "Unknown architecture $DOTNET_ARCH" >&2; exit 1;; + esac;; +Linux) + DOTNET_ARCH=$(linux$(getconf LONG_BIT) uname -m) + case $DOTNET_ARCH in + x86_64) + if is_linux_musl; then + DOTNET_HASH_URL=206aebda-126f-484f-af02-051a17c1ec54/2ec559cb69cec83ffa64dba5441a1b2d + DOTNET_FILE_NAME=dotnet-sdk-6.0.301-linux-musl-x64 + else + DOTNET_HASH_URL=77d472e5-194c-421e-992d-e4ca1d08e6cc/56c61ac303ddf1b12026151f4f000a2b + DOTNET_FILE_NAME=dotnet-sdk-6.0.301-linux-x64 + fi + ;; + aarch64) + if is_linux_musl; then + DOTNET_HASH_URL=4bd2399a-e0e9-43a6-9767-ac15dd430b1c/3dd4307a1ce811e31943d80eee638bc1 + DOTNET_FILE_NAME=dotnet-sdk-6.0.301-linux-musl-arm64 + else + DOTNET_HASH_URL=06c4ee8e-bf2c-4e46-ab1c-e14dd72311c1/f7bc6c9677eaccadd1d0e76c55d361ea + DOTNET_FILE_NAME=dotnet-sdk-6.0.301-linux-arm64 + fi + ;; + armv7l | armv8l) + if is_linux_musl; then + DOTNET_HASH_URL=952c468c-ac70-46b0-9274-4cb9c270950c/f0cd4c8392158547c2fa38674bfd56fd + DOTNET_FILE_NAME=dotnet-sdk-6.0.301-linux-musl-arm + else + DOTNET_HASH_URL=a218e3b9-941b-43be-bfb1-615862777457/80954de34ab68729981ed372a8d25b46 + DOTNET_FILE_NAME=dotnet-sdk-6.0.301-linux-arm + fi + ;; + *) echo "Unknown architecture $DOTNET_ARCH" >&2; exit 1;; + esac;; +*) echo "Unknown platform: $(uname)" >&2; exit 1;; +esac + +DOTNET_URL=https://cache-redirector.jetbrains.com/download.visualstudio.microsoft.com/download/pr/$DOTNET_HASH_URL/$DOTNET_FILE_NAME.tar.gz +DOTNET_TARGET_DIR=$TARGET_DIR/$DOTNET_FILE_NAME-$SCRIPT_VERSION +DOTNET_TEMP_FILE=$TARGET_DIR/dotnet-sdk-temp.tar.gz + +if grep -q -x "$DOTNET_URL" "$DOTNET_TARGET_DIR/.flag" 2>/dev/null; then + # Everything is up-to-date in $DOTNET_TARGET_DIR, do nothing + true +else +while true; do # Note(k15tfu): for goto + mkdir -p "$TARGET_DIR" + + LOCK_FILE="$TARGET_DIR/.dotnet-cmd-lock.pid" + TMP_LOCK_FILE="$TARGET_DIR/.tmp.$$.pid" + echo $$ >"$TMP_LOCK_FILE" + + while ! ln "$TMP_LOCK_FILE" "$LOCK_FILE" 2>/dev/null; do + LOCK_OWNER=$(cat "$LOCK_FILE" 2>/dev/null || true) + while [ -n "$LOCK_OWNER" ] && ps -p $LOCK_OWNER >/dev/null; do + warn "Waiting for the process $LOCK_OWNER to finish bootstrap dotnet.cmd" + sleep 1 + LOCK_OWNER=$(cat "$LOCK_FILE" 2>/dev/null || true) + + # Hurry up, bootstrap is ready.. + if grep -q -x "$DOTNET_URL" "$DOTNET_TARGET_DIR/.flag" 2>/dev/null; then + break 3 # Note(k15tfu): goto out of the outer if-else block. + fi + done + + if [ -n "$LOCK_OWNER" ] && grep -q -x $LOCK_OWNER "$LOCK_FILE" 2>/dev/null; then + die "ERROR: The lock file $LOCK_FILE still exists on disk after the owner process $LOCK_OWNER exited" + fi + done + + trap "rm -f \"$LOCK_FILE\"" EXIT + rm "$TMP_LOCK_FILE" + + if ! grep -q -x "$DOTNET_URL" "$DOTNET_TARGET_DIR/.flag" 2>/dev/null; then + warn "Downloading $DOTNET_URL to $DOTNET_TEMP_FILE" + + rm -f "$DOTNET_TEMP_FILE" + if command -v curl >/dev/null 2>&1; then + if [ -t 1 ]; then CURL_PROGRESS="--progress-bar"; else CURL_PROGRESS="--silent --show-error"; fi + retry_on_error 5 curl -L $CURL_PROGRESS --output "${DOTNET_TEMP_FILE}" "$DOTNET_URL" + elif command -v wget >/dev/null 2>&1; then + if [ -t 1 ]; then WGET_PROGRESS=""; else WGET_PROGRESS="-nv"; fi + retry_on_error 5 wget $WGET_PROGRESS -O "${DOTNET_TEMP_FILE}" "$DOTNET_URL" + else + die "ERROR: Please install wget or curl" + fi + + warn "Extracting $DOTNET_TEMP_FILE to $DOTNET_TARGET_DIR" + rm -rf "$DOTNET_TARGET_DIR" + mkdir -p "$DOTNET_TARGET_DIR" + + tar -x -f "$DOTNET_TEMP_FILE" -C "$DOTNET_TARGET_DIR" + rm -f "$DOTNET_TEMP_FILE" + + echo "$DOTNET_URL" >"$DOTNET_TARGET_DIR/.flag" + fi + + rm "$LOCK_FILE" + break +done +fi + +if [ ! -x "$DOTNET_TARGET_DIR/dotnet" ]; then + die "Unable to find dotnet under $DOTNET_TARGET_DIR" +fi + +exec "$DOTNET_TARGET_DIR/dotnet" "$@" + +:CMDSCRIPT + +setlocal +set SCRIPT_VERSION=v2 +set COMPANY_NAME=UtBot +set TARGET_DIR=%LOCALAPPDATA%\%COMPANY_NAME%\dotnet-cmd\ + +for /f "tokens=3 delims= " %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "PROCESSOR_ARCHITECTURE"') do set ARCH=%%a + +if "%ARCH%"=="ARM64" ( + set DOTNET_HASH_URL=656c8345-6661-409e-871d-00ca93cec542/cae3dcdc5c668c0e0abcf12d838348f1 + set DOTNET_FILE_NAME=dotnet-sdk-6.0.301-win-arm64 +) else ( + +if "%ARCH%"=="AMD64" ( + set DOTNET_HASH_URL=333eba0c-3242-48f3-a923-fdac5f219f77/342a4595101e3b4616360a7666459236 + set DOTNET_FILE_NAME=dotnet-sdk-6.0.301-win-x64 +) else ( + +if "%ARCH%"=="x86" ( + set DOTNET_HASH_URL=0a9cabcb-cb52-4f5e-bb79-1298f9ff9e22/c306c5cc940a9bb9a39ffe6619a255e6 + set DOTNET_FILE_NAME=dotnet-sdk-6.0.301-win-x86 +) else ( + +echo Unknown Windows architecture +goto fail + +))) + +set DOTNET_URL=https://cache-redirector.jetbrains.com/download.visualstudio.microsoft.com/download/pr/%DOTNET_HASH_URL%/%DOTNET_FILE_NAME%.zip +set DOTNET_TARGET_DIR=%TARGET_DIR%%DOTNET_FILE_NAME%-%SCRIPT_VERSION%\ +set DOTNET_TEMP_FILE=%TARGET_DIR%dotnet-sdk-temp.zip +set DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true +set DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true +set DOTNET_CLI_TELEMETRY_OPTOUT=true + +set POWERSHELL=%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe + +if not exist "%DOTNET_TARGET_DIR%.flag" goto downloadAndExtractDotNet + +set /p CURRENT_FLAG=<"%DOTNET_TARGET_DIR%.flag" +if "%CURRENT_FLAG%" == "%DOTNET_URL%" goto continueWithDotNet + +:downloadAndExtractDotNet + +set DOWNLOAD_AND_EXTRACT_DOTNET_PS1= ^ +Set-StrictMode -Version 3.0; ^ +$ErrorActionPreference = 'Stop'; ^ + ^ +$createdNew = $false; ^ +$lock = New-Object System.Threading.Mutex($true, 'Global\dotnet-cmd-lock', [ref]$createdNew); ^ +if (-not $createdNew) { ^ + Write-Host 'Waiting for the other process to finish bootstrap dotnet.cmd'; ^ + [void]$lock.WaitOne(); ^ +} ^ + ^ +try { ^ + if ((Get-Content '%DOTNET_TARGET_DIR%.flag' -ErrorAction Ignore) -ne '%DOTNET_URL%') { ^ + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; ^ + Write-Host 'Downloading %DOTNET_URL% to %DOTNET_TEMP_FILE%'; ^ + [void](New-Item '%TARGET_DIR%' -ItemType Directory -Force); ^ + (New-Object Net.WebClient).DownloadFile('%DOTNET_URL%', '%DOTNET_TEMP_FILE%'); ^ + ^ + Write-Host 'Extracting %DOTNET_TEMP_FILE% to %DOTNET_TARGET_DIR%'; ^ + if (Test-Path '%DOTNET_TARGET_DIR%') { ^ + Remove-Item '%DOTNET_TARGET_DIR%' -Recurse; ^ + } ^ + Add-Type -A 'System.IO.Compression.FileSystem'; ^ + [IO.Compression.ZipFile]::ExtractToDirectory('%DOTNET_TEMP_FILE%', '%DOTNET_TARGET_DIR%'); ^ + Remove-Item '%DOTNET_TEMP_FILE%'; ^ + ^ + Set-Content '%DOTNET_TARGET_DIR%.flag' -Value '%DOTNET_URL%'; ^ + } ^ +} ^ +finally { ^ + $lock.ReleaseMutex(); ^ +} + +"%POWERSHELL%" -nologo -noprofile -Command %DOWNLOAD_AND_EXTRACT_DOTNET_PS1% +if errorlevel 1 goto fail + +:continueWithDotNet + +if not exist "%DOTNET_TARGET_DIR%\dotnet.exe" ( + echo Unable to find dotnet.exe under %DOTNET_TARGET_DIR% + goto fail +) + +REM Prevent globally installed .NET Core from leaking into this runtime's lookup +SET DOTNET_MULTILEVEL_LOOKUP=0 +SET DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false + +for /f "tokens=2 delims=:." %%c in ('chcp') do set /a PREV_CODE_PAGE=%%c +call "%DOTNET_TARGET_DIR%\dotnet.exe" %* +set /a DOTNET_EXIT_CODE=%ERRORLEVEL% +chcp %PREV_CODE_PAGE% >nul + +exit /B %DOTNET_EXIT_CODE% +endlocal + +:fail +echo "FAIL" +exit /b 1 \ No newline at end of file diff --git a/utbot-rider/src/dotnet/UtBot/UtBot.Rd/Generated/CSharpRoot.Generated.cs b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/Generated/CSharpRoot.Generated.cs new file mode 100644 index 0000000000..390b86080f --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/Generated/CSharpRoot.Generated.cs @@ -0,0 +1,94 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a RdGen v1.11. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using JetBrains.Annotations; + +using JetBrains.Core; +using JetBrains.Diagnostics; +using JetBrains.Collections; +using JetBrains.Collections.Viewable; +using JetBrains.Lifetimes; +using JetBrains.Serialization; +using JetBrains.Rd; +using JetBrains.Rd.Base; +using JetBrains.Rd.Impl; +using JetBrains.Rd.Tasks; +using JetBrains.Rd.Util; +using JetBrains.Rd.Text; + + +// ReSharper disable RedundantEmptyObjectCreationArgumentList +// ReSharper disable InconsistentNaming +// ReSharper disable RedundantOverflowCheckingContext + + +namespace UtBot.Rd.Generated +{ + + + /// + ///

    Generated from: CSharpModel.kt:6

    + ///
    + public class CSharpRoot : RdExtBase + { + //fields + //public fields + + //private fields + //primary constructor + private CSharpRoot( + ) + { + } + //secondary constructor + //deconstruct trait + //statics + + + + protected override long SerializationHash => -3743965577225156277L; + + protected override Action Register => RegisterDeclaredTypesSerializers; + public static void RegisterDeclaredTypesSerializers(ISerializers serializers) + { + + serializers.RegisterToplevelOnce(typeof(CSharpRoot), CSharpRoot.RegisterDeclaredTypesSerializers); + serializers.RegisterToplevelOnce(typeof(VSharpModel), VSharpModel.RegisterDeclaredTypesSerializers); + } + + public CSharpRoot(Lifetime lifetime, IProtocol protocol) : this() + { + Identify(protocol.Identities, RdId.Root.Mix("CSharpRoot")); + Bind(lifetime, protocol, "CSharpRoot"); + } + + //constants + + //custom body + //methods + //equals trait + //hash code trait + //pretty print + public override void Print(PrettyPrinter printer) + { + printer.Println("CSharpRoot ("); + printer.Print(")"); + } + //toString + public override string ToString() + { + var printer = new SingleLinePrettyPrinter(); + Print(printer); + return printer.ToString(); + } + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot.Rd/Generated/VSharpModel.Generated.cs b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/Generated/VSharpModel.Generated.cs new file mode 100644 index 0000000000..1fb98174ac --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/Generated/VSharpModel.Generated.cs @@ -0,0 +1,598 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a RdGen v1.11. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using JetBrains.Annotations; + +using JetBrains.Core; +using JetBrains.Diagnostics; +using JetBrains.Collections; +using JetBrains.Collections.Viewable; +using JetBrains.Lifetimes; +using JetBrains.Serialization; +using JetBrains.Rd; +using JetBrains.Rd.Base; +using JetBrains.Rd.Impl; +using JetBrains.Rd.Tasks; +using JetBrains.Rd.Util; +using JetBrains.Rd.Text; + + +// ReSharper disable RedundantEmptyObjectCreationArgumentList +// ReSharper disable InconsistentNaming +// ReSharper disable RedundantOverflowCheckingContext + + +namespace UtBot.Rd.Generated +{ + + + /// + ///

    Generated from: CSharpModel.kt:8

    + ///
    + public class VSharpModel : RdExtBase + { + //fields + //public fields + [NotNull] public RdCall Generate => _Generate; + [NotNull] public ISignal Ping => _Ping; + [NotNull] public ISignal Log => _Log; + + //private fields + [NotNull] private readonly RdCall _Generate; + [NotNull] private readonly RdSignal _Ping; + [NotNull] private readonly RdSignal _Log; + + //primary constructor + private VSharpModel( + [NotNull] RdCall generate, + [NotNull] RdSignal ping, + [NotNull] RdSignal log + ) + { + if (generate == null) throw new ArgumentNullException("generate"); + if (ping == null) throw new ArgumentNullException("ping"); + if (log == null) throw new ArgumentNullException("log"); + + _Generate = generate; + _Ping = ping; + _Log = log; + _Generate.Async = true; + _Ping.Async = true; + _Log.Async = true; + BindableChildren.Add(new KeyValuePair("generate", _Generate)); + BindableChildren.Add(new KeyValuePair("ping", _Ping)); + BindableChildren.Add(new KeyValuePair("log", _Log)); + } + //secondary constructor + private VSharpModel ( + ) : this ( + new RdCall(GenerateArguments.Read, GenerateArguments.Write, GenerateResults.Read, GenerateResults.Write), + new RdSignal(JetBrains.Rd.Impl.Serializers.ReadString, JetBrains.Rd.Impl.Serializers.WriteString), + new RdSignal(JetBrains.Rd.Impl.Serializers.ReadString, JetBrains.Rd.Impl.Serializers.WriteString) + ) {} + //deconstruct trait + //statics + + + + protected override long SerializationHash => 9120359939503061610L; + + protected override Action Register => RegisterDeclaredTypesSerializers; + public static void RegisterDeclaredTypesSerializers(ISerializers serializers) + { + + serializers.RegisterToplevelOnce(typeof(CSharpRoot), CSharpRoot.RegisterDeclaredTypesSerializers); + } + + public VSharpModel(Lifetime lifetime, IProtocol protocol) : this() + { + Identify(protocol.Identities, RdId.Root.Mix("VSharpModel")); + Bind(lifetime, protocol, "VSharpModel"); + } + + //constants + + //custom body + //methods + //equals trait + //hash code trait + //pretty print + public override void Print(PrettyPrinter printer) + { + printer.Println("VSharpModel ("); + using (printer.IndentCookie()) { + printer.Print("generate = "); _Generate.PrintEx(printer); printer.Println(); + printer.Print("ping = "); _Ping.PrintEx(printer); printer.Println(); + printer.Print("log = "); _Log.PrintEx(printer); printer.Println(); + } + printer.Print(")"); + } + //toString + public override string ToString() + { + var printer = new SingleLinePrettyPrinter(); + Print(printer); + return printer.ToString(); + } + } + + + /// + ///

    Generated from: CSharpModel.kt:21

    + ///
    + public sealed class GenerateArguments : IPrintable, IEquatable + { + //fields + //public fields + [NotNull] public string AssemblyPath {get; private set;} + [NotNull] public string ProjectCsprojPath {get; private set;} + [NotNull] public string SolutionFilePath {get; private set;} + [NotNull] public List Methods {get; private set;} + public int GenerationTimeoutInSeconds {get; private set;} + [CanBeNull] public string TargetFramework {get; private set;} + [NotNull] public List AssembliesFullNameToTheirPath {get; private set;} + + //private fields + //primary constructor + public GenerateArguments( + [NotNull] string assemblyPath, + [NotNull] string projectCsprojPath, + [NotNull] string solutionFilePath, + [NotNull] List methods, + int generationTimeoutInSeconds, + [CanBeNull] string targetFramework, + [NotNull] List assembliesFullNameToTheirPath + ) + { + if (assemblyPath == null) throw new ArgumentNullException("assemblyPath"); + if (projectCsprojPath == null) throw new ArgumentNullException("projectCsprojPath"); + if (solutionFilePath == null) throw new ArgumentNullException("solutionFilePath"); + if (methods == null) throw new ArgumentNullException("methods"); + if (assembliesFullNameToTheirPath == null) throw new ArgumentNullException("assembliesFullNameToTheirPath"); + + AssemblyPath = assemblyPath; + ProjectCsprojPath = projectCsprojPath; + SolutionFilePath = solutionFilePath; + Methods = methods; + GenerationTimeoutInSeconds = generationTimeoutInSeconds; + TargetFramework = targetFramework; + AssembliesFullNameToTheirPath = assembliesFullNameToTheirPath; + } + //secondary constructor + //deconstruct trait + public void Deconstruct([NotNull] out string assemblyPath, [NotNull] out string projectCsprojPath, [NotNull] out string solutionFilePath, [NotNull] out List methods, out int generationTimeoutInSeconds, [CanBeNull] out string targetFramework, [NotNull] out List assembliesFullNameToTheirPath) + { + assemblyPath = AssemblyPath; + projectCsprojPath = ProjectCsprojPath; + solutionFilePath = SolutionFilePath; + methods = Methods; + generationTimeoutInSeconds = GenerationTimeoutInSeconds; + targetFramework = TargetFramework; + assembliesFullNameToTheirPath = AssembliesFullNameToTheirPath; + } + //statics + + public static CtxReadDelegate Read = (ctx, reader) => + { + var assemblyPath = reader.ReadString(); + var projectCsprojPath = reader.ReadString(); + var solutionFilePath = reader.ReadString(); + var methods = ReadMethodDescriptorList(ctx, reader); + var generationTimeoutInSeconds = reader.ReadInt(); + var targetFramework = ReadStringNullable(ctx, reader); + var assembliesFullNameToTheirPath = ReadMapEntryList(ctx, reader); + var _result = new GenerateArguments(assemblyPath, projectCsprojPath, solutionFilePath, methods, generationTimeoutInSeconds, targetFramework, assembliesFullNameToTheirPath); + return _result; + }; + public static CtxReadDelegate> ReadMethodDescriptorList = MethodDescriptor.Read.List(); + public static CtxReadDelegate ReadStringNullable = JetBrains.Rd.Impl.Serializers.ReadString.NullableClass(); + public static CtxReadDelegate> ReadMapEntryList = MapEntry.Read.List(); + + public static CtxWriteDelegate Write = (ctx, writer, value) => + { + writer.Write(value.AssemblyPath); + writer.Write(value.ProjectCsprojPath); + writer.Write(value.SolutionFilePath); + WriteMethodDescriptorList(ctx, writer, value.Methods); + writer.Write(value.GenerationTimeoutInSeconds); + WriteStringNullable(ctx, writer, value.TargetFramework); + WriteMapEntryList(ctx, writer, value.AssembliesFullNameToTheirPath); + }; + public static CtxWriteDelegate> WriteMethodDescriptorList = MethodDescriptor.Write.List(); + public static CtxWriteDelegate WriteStringNullable = JetBrains.Rd.Impl.Serializers.WriteString.NullableClass(); + public static CtxWriteDelegate> WriteMapEntryList = MapEntry.Write.List(); + + //constants + + //custom body + //methods + //equals trait + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((GenerateArguments) obj); + } + public bool Equals(GenerateArguments other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return AssemblyPath == other.AssemblyPath && ProjectCsprojPath == other.ProjectCsprojPath && SolutionFilePath == other.SolutionFilePath && Methods.SequenceEqual(other.Methods) && GenerationTimeoutInSeconds == other.GenerationTimeoutInSeconds && Equals(TargetFramework, other.TargetFramework) && AssembliesFullNameToTheirPath.SequenceEqual(other.AssembliesFullNameToTheirPath); + } + //hash code trait + public override int GetHashCode() + { + unchecked { + var hash = 0; + hash = hash * 31 + AssemblyPath.GetHashCode(); + hash = hash * 31 + ProjectCsprojPath.GetHashCode(); + hash = hash * 31 + SolutionFilePath.GetHashCode(); + hash = hash * 31 + Methods.ContentHashCode(); + hash = hash * 31 + GenerationTimeoutInSeconds.GetHashCode(); + hash = hash * 31 + (TargetFramework != null ? TargetFramework.GetHashCode() : 0); + hash = hash * 31 + AssembliesFullNameToTheirPath.ContentHashCode(); + return hash; + } + } + //pretty print + public void Print(PrettyPrinter printer) + { + printer.Println("GenerateArguments ("); + using (printer.IndentCookie()) { + printer.Print("assemblyPath = "); AssemblyPath.PrintEx(printer); printer.Println(); + printer.Print("projectCsprojPath = "); ProjectCsprojPath.PrintEx(printer); printer.Println(); + printer.Print("solutionFilePath = "); SolutionFilePath.PrintEx(printer); printer.Println(); + printer.Print("methods = "); Methods.PrintEx(printer); printer.Println(); + printer.Print("generationTimeoutInSeconds = "); GenerationTimeoutInSeconds.PrintEx(printer); printer.Println(); + printer.Print("targetFramework = "); TargetFramework.PrintEx(printer); printer.Println(); + printer.Print("assembliesFullNameToTheirPath = "); AssembliesFullNameToTheirPath.PrintEx(printer); printer.Println(); + } + printer.Print(")"); + } + //toString + public override string ToString() + { + var printer = new SingleLinePrettyPrinter(); + Print(printer); + return printer.ToString(); + } + } + + + /// + ///

    Generated from: CSharpModel.kt:31

    + ///
    + public sealed class GenerateResults : IPrintable, IEquatable + { + //fields + //public fields + [CanBeNull] public string GeneratedProjectPath {get; private set;} + [NotNull] public List GeneratedFilesPaths {get; private set;} + [CanBeNull] public string ExceptionMessage {get; private set;} + public int TestsCount {get; private set;} + public int ErrorsCount {get; private set;} + + //private fields + //primary constructor + public GenerateResults( + [CanBeNull] string generatedProjectPath, + [NotNull] List generatedFilesPaths, + [CanBeNull] string exceptionMessage, + int testsCount, + int errorsCount + ) + { + if (generatedFilesPaths == null) throw new ArgumentNullException("generatedFilesPaths"); + + GeneratedProjectPath = generatedProjectPath; + GeneratedFilesPaths = generatedFilesPaths; + ExceptionMessage = exceptionMessage; + TestsCount = testsCount; + ErrorsCount = errorsCount; + } + //secondary constructor + //deconstruct trait + public void Deconstruct([CanBeNull] out string generatedProjectPath, [NotNull] out List generatedFilesPaths, [CanBeNull] out string exceptionMessage, out int testsCount, out int errorsCount) + { + generatedProjectPath = GeneratedProjectPath; + generatedFilesPaths = GeneratedFilesPaths; + exceptionMessage = ExceptionMessage; + testsCount = TestsCount; + errorsCount = ErrorsCount; + } + //statics + + public static CtxReadDelegate Read = (ctx, reader) => + { + var generatedProjectPath = ReadStringNullable(ctx, reader); + var generatedFilesPaths = ReadStringList(ctx, reader); + var exceptionMessage = ReadStringNullable(ctx, reader); + var testsCount = reader.ReadInt(); + var errorsCount = reader.ReadInt(); + var _result = new GenerateResults(generatedProjectPath, generatedFilesPaths, exceptionMessage, testsCount, errorsCount); + return _result; + }; + public static CtxReadDelegate ReadStringNullable = JetBrains.Rd.Impl.Serializers.ReadString.NullableClass(); + public static CtxReadDelegate> ReadStringList = JetBrains.Rd.Impl.Serializers.ReadString.List(); + + public static CtxWriteDelegate Write = (ctx, writer, value) => + { + WriteStringNullable(ctx, writer, value.GeneratedProjectPath); + WriteStringList(ctx, writer, value.GeneratedFilesPaths); + WriteStringNullable(ctx, writer, value.ExceptionMessage); + writer.Write(value.TestsCount); + writer.Write(value.ErrorsCount); + }; + public static CtxWriteDelegate WriteStringNullable = JetBrains.Rd.Impl.Serializers.WriteString.NullableClass(); + public static CtxWriteDelegate> WriteStringList = JetBrains.Rd.Impl.Serializers.WriteString.List(); + + //constants + + //custom body + //methods + //equals trait + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((GenerateResults) obj); + } + public bool Equals(GenerateResults other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(GeneratedProjectPath, other.GeneratedProjectPath) && GeneratedFilesPaths.SequenceEqual(other.GeneratedFilesPaths) && Equals(ExceptionMessage, other.ExceptionMessage) && TestsCount == other.TestsCount && ErrorsCount == other.ErrorsCount; + } + //hash code trait + public override int GetHashCode() + { + unchecked { + var hash = 0; + hash = hash * 31 + (GeneratedProjectPath != null ? GeneratedProjectPath.GetHashCode() : 0); + hash = hash * 31 + GeneratedFilesPaths.ContentHashCode(); + hash = hash * 31 + (ExceptionMessage != null ? ExceptionMessage.GetHashCode() : 0); + hash = hash * 31 + TestsCount.GetHashCode(); + hash = hash * 31 + ErrorsCount.GetHashCode(); + return hash; + } + } + //pretty print + public void Print(PrettyPrinter printer) + { + printer.Println("GenerateResults ("); + using (printer.IndentCookie()) { + printer.Print("generatedProjectPath = "); GeneratedProjectPath.PrintEx(printer); printer.Println(); + printer.Print("generatedFilesPaths = "); GeneratedFilesPaths.PrintEx(printer); printer.Println(); + printer.Print("exceptionMessage = "); ExceptionMessage.PrintEx(printer); printer.Println(); + printer.Print("testsCount = "); TestsCount.PrintEx(printer); printer.Println(); + printer.Print("errorsCount = "); ErrorsCount.PrintEx(printer); printer.Println(); + } + printer.Print(")"); + } + //toString + public override string ToString() + { + var printer = new SingleLinePrettyPrinter(); + Print(printer); + return printer.ToString(); + } + } + + + /// + ///

    Generated from: CSharpModel.kt:16

    + ///
    + public sealed class MapEntry : IPrintable, IEquatable + { + //fields + //public fields + [NotNull] public string Key {get; private set;} + [NotNull] public string Value {get; private set;} + + //private fields + //primary constructor + public MapEntry( + [NotNull] string key, + [NotNull] string value + ) + { + if (key == null) throw new ArgumentNullException("key"); + if (value == null) throw new ArgumentNullException("value"); + + Key = key; + Value = value; + } + //secondary constructor + //deconstruct trait + public void Deconstruct([NotNull] out string key, [NotNull] out string value) + { + key = Key; + value = Value; + } + //statics + + public static CtxReadDelegate Read = (ctx, reader) => + { + var key = reader.ReadString(); + var value = reader.ReadString(); + var _result = new MapEntry(key, value); + return _result; + }; + + public static CtxWriteDelegate Write = (ctx, writer, value) => + { + writer.Write(value.Key); + writer.Write(value.Value); + }; + + //constants + + //custom body + //methods + //equals trait + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((MapEntry) obj); + } + public bool Equals(MapEntry other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Key == other.Key && Value == other.Value; + } + //hash code trait + public override int GetHashCode() + { + unchecked { + var hash = 0; + hash = hash * 31 + Key.GetHashCode(); + hash = hash * 31 + Value.GetHashCode(); + return hash; + } + } + //pretty print + public void Print(PrettyPrinter printer) + { + printer.Println("MapEntry ("); + using (printer.IndentCookie()) { + printer.Print("key = "); Key.PrintEx(printer); printer.Println(); + printer.Print("value = "); Value.PrintEx(printer); printer.Println(); + } + printer.Print(")"); + } + //toString + public override string ToString() + { + var printer = new SingleLinePrettyPrinter(); + Print(printer); + return printer.ToString(); + } + } + + + /// + ///

    Generated from: CSharpModel.kt:9

    + ///
    + public sealed class MethodDescriptor : IPrintable, IEquatable + { + //fields + //public fields + [NotNull] public string MethodName {get; private set;} + [NotNull] public string TypeName {get; private set;} + public bool HasNoOverloads {get; private set;} + [NotNull] public List Parameters {get; private set;} + + //private fields + //primary constructor + public MethodDescriptor( + [NotNull] string methodName, + [NotNull] string typeName, + bool hasNoOverloads, + [NotNull] List parameters + ) + { + if (methodName == null) throw new ArgumentNullException("methodName"); + if (typeName == null) throw new ArgumentNullException("typeName"); + if (parameters == null) throw new ArgumentNullException("parameters"); + + MethodName = methodName; + TypeName = typeName; + HasNoOverloads = hasNoOverloads; + Parameters = parameters; + } + //secondary constructor + //deconstruct trait + public void Deconstruct([NotNull] out string methodName, [NotNull] out string typeName, out bool hasNoOverloads, [NotNull] out List parameters) + { + methodName = MethodName; + typeName = TypeName; + hasNoOverloads = HasNoOverloads; + parameters = Parameters; + } + //statics + + public static CtxReadDelegate Read = (ctx, reader) => + { + var methodName = reader.ReadString(); + var typeName = reader.ReadString(); + var hasNoOverloads = reader.ReadBool(); + var parameters = ReadStringList(ctx, reader); + var _result = new MethodDescriptor(methodName, typeName, hasNoOverloads, parameters); + return _result; + }; + public static CtxReadDelegate> ReadStringList = JetBrains.Rd.Impl.Serializers.ReadString.List(); + + public static CtxWriteDelegate Write = (ctx, writer, value) => + { + writer.Write(value.MethodName); + writer.Write(value.TypeName); + writer.Write(value.HasNoOverloads); + WriteStringList(ctx, writer, value.Parameters); + }; + public static CtxWriteDelegate> WriteStringList = JetBrains.Rd.Impl.Serializers.WriteString.List(); + + //constants + + //custom body + //methods + //equals trait + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((MethodDescriptor) obj); + } + public bool Equals(MethodDescriptor other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return MethodName == other.MethodName && TypeName == other.TypeName && HasNoOverloads == other.HasNoOverloads && Parameters.SequenceEqual(other.Parameters); + } + //hash code trait + public override int GetHashCode() + { + unchecked { + var hash = 0; + hash = hash * 31 + MethodName.GetHashCode(); + hash = hash * 31 + TypeName.GetHashCode(); + hash = hash * 31 + HasNoOverloads.GetHashCode(); + hash = hash * 31 + Parameters.ContentHashCode(); + return hash; + } + } + //pretty print + public void Print(PrettyPrinter printer) + { + printer.Println("MethodDescriptor ("); + using (printer.IndentCookie()) { + printer.Print("methodName = "); MethodName.PrintEx(printer); printer.Println(); + printer.Print("typeName = "); TypeName.PrintEx(printer); printer.Println(); + printer.Print("hasNoOverloads = "); HasNoOverloads.PrintEx(printer); printer.Println(); + printer.Print("parameters = "); Parameters.PrintEx(printer); printer.Println(); + } + printer.Print(")"); + } + //toString + public override string ToString() + { + var printer = new SingleLinePrettyPrinter(); + Print(printer); + return printer.ToString(); + } + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot.Rd/RdUtil.cs b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/RdUtil.cs new file mode 100644 index 0000000000..d069e66524 --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/RdUtil.cs @@ -0,0 +1,7 @@ +namespace UtBot.Rd; + +public static class RdUtil +{ + public static readonly string MainProcessName = "UtBot"; + public static readonly string DefaultTestProjectTarget = "net6.0"; +} \ No newline at end of file diff --git a/utbot-rider/src/dotnet/UtBot/UtBot.Rd/TypeDescriptor.cs b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/TypeDescriptor.cs new file mode 100644 index 0000000000..7f370c5cde --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/TypeDescriptor.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace UtBot.Rd; + +// Type descriptors are separately serialized to json because +// they are recursive (have type descriptor Parameters) +public class TypeDescriptor +{ + public int? ArrayRank { get; set; } + public string Name { get; set; } + public int? MethodParameterPosition { get; set; } + public int? TypeParameterPosition { get; set; } + public List Parameters { get; set; } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot.Rd/UtBot.Rd.csproj b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/UtBot.Rd.csproj new file mode 100644 index 0000000000..a037083c98 --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot.Rd/UtBot.Rd.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + disable + UtBot.Rd + + + + + + + + diff --git a/utbot-rider/src/dotnet/UtBot/UtBot.VSharp/UtBot.VSharp.csproj b/utbot-rider/src/dotnet/UtBot/UtBot.VSharp/UtBot.VSharp.csproj new file mode 100644 index 0000000000..a8cab38432 --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot.VSharp/UtBot.VSharp.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + disable + UtBot.VSharp + UtBot.VSharp.VSharpMain + + + + + + + + + + diff --git a/utbot-rider/src/dotnet/UtBot/UtBot.VSharp/VSharpMain.cs b/utbot-rider/src/dotnet/UtBot/UtBot.VSharp/VSharpMain.cs new file mode 100644 index 0000000000..662f2e9a1a --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot.VSharp/VSharpMain.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using JetBrains.Collections.Viewable; +using JetBrains.Lifetimes; +using JetBrains.Rd; +using JetBrains.Rd.Impl; +using JetBrains.Rd.Tasks; +using UtBot.Rd; +using UtBot.Rd.Generated; +using VSharp; +using VSharp.TestRenderer; + +namespace UtBot.VSharp; + +public static class VSharpMain +{ + public static readonly string VSharpProcessName = "VSharp"; + + private class SignalWriter : TextWriter + { + private readonly ISignal _signal; + public SignalWriter(ISignal signal) + { + _signal = signal; + } + + public override void Write(string value) + { + _signal.Fire(value); + } + + public override void WriteLine(string value) + { + Write(value); + } + + public override Encoding Encoding => Encoding.Default; + } + + private static GenerateResults GenerateImpl(GenerateArguments arguments, IReadOnlyDictionary assembliesFullNameToTheirPath) + { + var (assemblyPath, projectCsprojPath, solutionFilePath, + methodDescriptors, generationTimeout, targetFramework, _) = arguments; + + string AssemblyResolveFunc(string fullAssemblyName) + { + return !assembliesFullNameToTheirPath.TryGetValue(fullAssemblyName, out var found) ? null : found; + } + + var assemblyLoadContext = new AssemblyLoadContext(VSharpProcessName); + assemblyLoadContext.Resolving += (context, assemblyName) => + { + var found = AssemblyResolveFunc(assemblyName.FullName); + if (found == null) + { + return null; + } + return context.LoadFromAssemblyPath(found); + }; + + using (new DependencyResolver(AssemblyResolveFunc)) + { + var assembly = assemblyLoadContext.LoadFromAssemblyPath(assemblyPath); + var methods = methodDescriptors.Select(d => d.ToMethodInfo(assembly)).ToList(); + var declaringType = methods.Select(m => m.DeclaringType).Distinct().SingleOrDefault(); + var stat = TestGenerator.Cover(methods, generationTimeout, verbosity: Verbosity.Info); + + var testedProject = new FileInfo(projectCsprojPath); + var solution = new FileInfo(solutionFilePath); + + var (generatedProject, renderedFiles) = + Renderer.Render(stat.Results(), testedProject, declaringType, solution, targetFramework); + + return new GenerateResults( + generatedProject.FullName, + renderedFiles, + null, + (int)stat.TestsCount, + (int)stat.ErrorsCount); + } + } + + private static bool MatchesType(TypeDescriptor typeDescriptor, Type typ) + { + if (typ.IsGenericMethodParameter) + { + return typ.GenericParameterPosition == typeDescriptor.MethodParameterPosition; + } + + if (typ.IsGenericTypeParameter) + { + return typ.GenericParameterPosition == typeDescriptor.TypeParameterPosition; + } + + if (typ.IsArray) + { + return typ.GetArrayRank() == typeDescriptor.ArrayRank && + MatchesType(typeDescriptor.Parameters[0], typ.GetElementType()); + } + + var name = typ.IsGenericType ? typ.GetGenericTypeDefinition().FullName : typ.FullName; + + if (name != typeDescriptor.Name) + { + Logger.printLogString(Logger.Error, $"{typ.FullName} != {typeDescriptor.Name}"); + return false; + } + + var genericArguments = typ.GetGenericArguments(); + + if (genericArguments.Length != typeDescriptor.Parameters.Count) + { + return false; + } + + for (var i = 0; i < genericArguments.Length; ++i) + { + if (!MatchesType(typeDescriptor.Parameters[i], genericArguments[i])) + { + return false; + } + } + + return true; + } + + private static bool MatchesMethod(MethodDescriptor descriptor, MethodInfo methodInfo) + { + var targetParameters = descriptor.Parameters.Select(p => JsonSerializer.Deserialize(p)).ToArray(); + + if (methodInfo.Name != descriptor.MethodName) + { + return false; + } + + var parameters = methodInfo.GetParameters(); + + if (parameters.Length != targetParameters.Length) + { + return false; + } + + for (var i = 0; i < parameters.Length; ++i) + { + if (!MatchesType(targetParameters[i], parameters[i].ParameterType)) + { + return false; + } + } + + return true; + } + + private static MethodInfo ToMethodInfo(this MethodDescriptor descriptor, Assembly assembly) + { + var type = assembly.GetType(descriptor.TypeName, throwOnError: false); + + if (type?.FullName != descriptor.TypeName) + throw new InvalidDataException($"Cannot find type {descriptor.TypeName}, found: {type?.Name}"); + + MethodInfo methodInfo; + var bindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public; + + if (descriptor.HasNoOverloads) + { + methodInfo = type.GetMethod(descriptor.MethodName, bindingFlags); + } + else + { + methodInfo = type.GetMethods() + .FirstOrDefault(m => MatchesMethod(descriptor, m)); + } + + if (methodInfo?.Name != descriptor.MethodName) + throw new InvalidDataException( + $"Cannot find method ${descriptor.MethodName} for type ${descriptor.TypeName}"); + + return methodInfo; + } + + public static void Main(string[] args) + { + using var blockingQueue = new BlockingCollection(1); + var port = int.Parse(args[0]); + var ldef = new LifetimeDefinition(); + SingleThreadScheduler.RunOnSeparateThread(ldef.Lifetime, VSharpProcessName, scheduler => + { + var wire = new SocketWire.Client(ldef.Lifetime, scheduler, port); + var serializers = new Serializers(); + var identities = new Identities(IdKind.Client); + var protocol = new Protocol(VSharpProcessName, serializers, identities, scheduler, wire, ldef.Lifetime); + scheduler.Queue(() => + { + var vSharpModel = new VSharpModel(ldef.Lifetime, protocol); + // Configuring V# logger: messages will be send via RD to UTBot plugin process + Logger.configureWriter(new SignalWriter(vSharpModel.Log)); + vSharpModel.Generate.Set((_, arguments) => + { + try + { + var assemblyToPath = arguments.AssembliesFullNameToTheirPath.ToDictionary(it => it.Key, it => it.Value); + return GenerateImpl(arguments, assemblyToPath); + } + catch (Exception e) + { + return new GenerateResults(null, new(), e.ToString(), 0, 0); + } + finally + { + var pathToCsProject = arguments.ProjectCsprojPath; + var csProjFile = new FileInfo(pathToCsProject); + if (csProjFile.Exists) + { + var csProjDir = csProjFile.Directory; + var vSharpDirs = csProjDir!.GetDirectories(); + foreach(var dir in vSharpDirs) + { + if (Regex.IsMatch(dir.Name, @"^VSharp\.tests\..+$")) + { + dir.Delete(recursive: true); + } + } + } + scheduler.Queue(() => { blockingQueue.Add("End"); }); + } + }); + vSharpModel.Ping.Advise(ldef.Lifetime, s => + { + if (s == RdUtil.MainProcessName) + { + vSharpModel.Ping.Fire(VSharpProcessName); + } + }); + }); + }); + blockingQueue.Take(); + ldef.Terminate(); + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot.sln b/utbot-rider/src/dotnet/UtBot/UtBot.sln new file mode 100644 index 0000000000..c9234f0b98 --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtBot", "UtBot\UtBot.csproj", "{573A2CF1-56F0-4350-A018-E25A52A86E63}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtBot.VSharp", "UtBot.VSharp\UtBot.VSharp.csproj", "{C076EF37-4DCA-4852-9D5D-04B18E662DBF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtBot.Rd", "UtBot.Rd\UtBot.Rd.csproj", "{B8B9FF73-A630-4476-82BD-C0BBD8C8A9E5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {573A2CF1-56F0-4350-A018-E25A52A86E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {573A2CF1-56F0-4350-A018-E25A52A86E63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {573A2CF1-56F0-4350-A018-E25A52A86E63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {573A2CF1-56F0-4350-A018-E25A52A86E63}.Release|Any CPU.Build.0 = Release|Any CPU + {C076EF37-4DCA-4852-9D5D-04B18E662DBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C076EF37-4DCA-4852-9D5D-04B18E662DBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C076EF37-4DCA-4852-9D5D-04B18E662DBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C076EF37-4DCA-4852-9D5D-04B18E662DBF}.Release|Any CPU.Build.0 = Release|Any CPU + {B8B9FF73-A630-4476-82BD-C0BBD8C8A9E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8B9FF73-A630-4476-82BD-C0BBD8C8A9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8B9FF73-A630-4476-82BD-C0BBD8C8A9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8B9FF73-A630-4476-82BD-C0BBD8C8A9E5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestAction.cs b/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestAction.cs new file mode 100644 index 0000000000..6c9d8c290f --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestAction.cs @@ -0,0 +1,12 @@ +using JetBrains.Application.UI.ActionsRevised.Menu; +using JetBrains.ReSharper.Feature.Services.Generate.Actions; +using JetBrains.ReSharper.Resources.Resources.Icons; +using JetBrains.UI.RichText; + +namespace UtBot; + +[Action("Generate.UnitTest", "Generate Unit Test", Icon = typeof(PsiFeaturesUnsortedThemedIcons.FuncZoneGenerate))] +internal class GenerateUnitTestAction : GenerateActionBase +{ + protected override RichText Caption => "Generate Unit Test"; +} \ No newline at end of file diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestElementProvider.cs b/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestElementProvider.cs new file mode 100644 index 0000000000..c9ca0341e7 --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestElementProvider.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JetBrains.ReSharper.Feature.Services.CSharp.Generate; +using JetBrains.ReSharper.Feature.Services.Generate; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.CSharp; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.Psi.Tree; + +namespace UtBot; + +internal class TimeoutGeneratorOption : IGeneratorOption +{ + public static readonly string Id = "TimeoutGeneratorOption"; + private const string InitialValue = "10"; + + public IReadOnlyList GetPossibleValues() => new string[] { }; + + public bool IsValidValue(string value) => + int.TryParse(value, out var intValue) && intValue > 0; + + public string ID => Id; + + public string Title => "Timeout (s) for all selected methods"; + + public GeneratorOptionKind Kind => GeneratorOptionKind.Text; + + public bool Persist => false; + + public string Value { get; set; } = InitialValue; + + public bool OverridesGlobalOption { get; set; } = false; + + public bool HasDependentOptions => false; +} + +[GeneratorElementProvider(GenerateUnitTestWorkflow.Kind, typeof(CSharpLanguage))] +internal class GenerateUnitTestElementProvider : GeneratorProviderBase +{ + public override void Populate(CSharpGeneratorContext context) + { + context.Options.Add(new TimeoutGeneratorOption()); + + var memberSource = context.ExternalElementsSource?.GetTypeElement() ?? context.ClassDeclaration.DeclaredElement; + if (memberSource == null) return; + + var substitution = context.ExternalElementsSource?.GetSubstitution() ?? memberSource.IdSubstitution; + + var usageContext = (ITreeNode)context.ClassDeclaration.Body ?? context.ClassDeclaration; + + foreach (var method in memberSource.Methods) + { + if (MethodFilter(method, substitution, usageContext)) + { + var element = new GeneratorDeclaredElement(method, substitution); + context.ProvidedElements.Add(element); + context.InputElements.Add(element); + } + } + } + + protected virtual bool MethodFilter([NotNull] IMethod method, ISubstitution substitution, + [NotNull] ITreeNode context) + { + if (method.IsSynthetic()) return false; + if (method.GetAccessRights() != AccessRights.PUBLIC) return false; + return true; + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestWorkflow.cs b/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestWorkflow.cs new file mode 100644 index 0000000000..0daa5f1237 --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestWorkflow.cs @@ -0,0 +1,23 @@ +using JetBrains.ReSharper.Feature.Services.Generate.Actions; +using JetBrains.ReSharper.Feature.Services.Generate.Workflows; +using JetBrains.ReSharper.Psi.Resources; + +namespace UtBot; + +public class GenerateUnitTestWorkflow : GenerateCodeWorkflowBase +{ + public const string Kind = "UnitTest"; + + public GenerateUnitTestWorkflow() : base( + Kind, + PsiSymbolsThemedIcons.SymbolUnitTest.Id, + "Tests with UnitTestBot", + GenerateActionGroup.CLR_LANGUAGE, + "Generate tests with UnitTestBot", + "Select methods for generation", + "Generate.UnitTest") + { + } + + public override double Order => 10; +} \ No newline at end of file diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestWorkflowProvider.cs b/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestWorkflowProvider.cs new file mode 100644 index 0000000000..661a6d8e4f --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/GenerateUnitTestWorkflowProvider.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using JetBrains.Application.DataContext; +using JetBrains.ReSharper.Feature.Services.Generate.Actions; + +namespace UtBot; + +[GenerateProvider] +public class GenerateUnitTestWorkflowProvider : IGenerateWorkflowProvider +{ + public IEnumerable CreateWorkflow(IDataContext dataContext) + { + return new[] { new GenerateUnitTestWorkflow() }; + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/Generated/UtBotRiderModel.Generated.cs b/utbot-rider/src/dotnet/UtBot/UtBot/Generated/UtBotRiderModel.Generated.cs new file mode 100644 index 0000000000..cf5bfd3ebb --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/Generated/UtBotRiderModel.Generated.cs @@ -0,0 +1,268 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a RdGen v1.10. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using JetBrains.Annotations; + +using JetBrains.Core; +using JetBrains.Diagnostics; +using JetBrains.Collections; +using JetBrains.Collections.Viewable; +using JetBrains.Lifetimes; +using JetBrains.Serialization; +using JetBrains.Rd; +using JetBrains.Rd.Base; +using JetBrains.Rd.Impl; +using JetBrains.Rd.Tasks; +using JetBrains.Rd.Util; +using JetBrains.Rd.Text; + + +// ReSharper disable RedundantEmptyObjectCreationArgumentList +// ReSharper disable InconsistentNaming +// ReSharper disable RedundantOverflowCheckingContext + + +namespace JetBrains.Rider.Model +{ + + + /// + ///

    Generated from: UtBotRiderModel.kt:8

    + ///
    + public class UtBotRiderModel : RdExtBase + { + //fields + //public fields + [NotNull] public ISignal StartPublish => _StartPublish; + [NotNull] public ISignal LogPublishOutput => _LogPublishOutput; + [NotNull] public ISignal LogPublishError => _LogPublishError; + [NotNull] public ISignal StopPublish => _StopPublish; + [NotNull] public ISignal StartVSharp => _StartVSharp; + [NotNull] public ISignal LogVSharp => _LogVSharp; + [NotNull] public ISignal StopVSharp => _StopVSharp; + + //private fields + [NotNull] private readonly RdSignal _StartPublish; + [NotNull] private readonly RdSignal _LogPublishOutput; + [NotNull] private readonly RdSignal _LogPublishError; + [NotNull] private readonly RdSignal _StopPublish; + [NotNull] private readonly RdSignal _StartVSharp; + [NotNull] private readonly RdSignal _LogVSharp; + [NotNull] private readonly RdSignal _StopVSharp; + + //primary constructor + private UtBotRiderModel( + [NotNull] RdSignal startPublish, + [NotNull] RdSignal logPublishOutput, + [NotNull] RdSignal logPublishError, + [NotNull] RdSignal stopPublish, + [NotNull] RdSignal startVSharp, + [NotNull] RdSignal logVSharp, + [NotNull] RdSignal stopVSharp + ) + { + if (startPublish == null) throw new ArgumentNullException("startPublish"); + if (logPublishOutput == null) throw new ArgumentNullException("logPublishOutput"); + if (logPublishError == null) throw new ArgumentNullException("logPublishError"); + if (stopPublish == null) throw new ArgumentNullException("stopPublish"); + if (startVSharp == null) throw new ArgumentNullException("startVSharp"); + if (logVSharp == null) throw new ArgumentNullException("logVSharp"); + if (stopVSharp == null) throw new ArgumentNullException("stopVSharp"); + + _StartPublish = startPublish; + _LogPublishOutput = logPublishOutput; + _LogPublishError = logPublishError; + _StopPublish = stopPublish; + _StartVSharp = startVSharp; + _LogVSharp = logVSharp; + _StopVSharp = stopVSharp; + _StartPublish.Async = true; + _LogPublishOutput.Async = true; + _LogPublishError.Async = true; + _StopPublish.Async = true; + _StartVSharp.Async = true; + _LogVSharp.Async = true; + _StopVSharp.Async = true; + BindableChildren.Add(new KeyValuePair("startPublish", _StartPublish)); + BindableChildren.Add(new KeyValuePair("logPublishOutput", _LogPublishOutput)); + BindableChildren.Add(new KeyValuePair("logPublishError", _LogPublishError)); + BindableChildren.Add(new KeyValuePair("stopPublish", _StopPublish)); + BindableChildren.Add(new KeyValuePair("startVSharp", _StartVSharp)); + BindableChildren.Add(new KeyValuePair("logVSharp", _LogVSharp)); + BindableChildren.Add(new KeyValuePair("stopVSharp", _StopVSharp)); + } + //secondary constructor + internal UtBotRiderModel ( + ) : this ( + new RdSignal(StartPublishArgs.Read, StartPublishArgs.Write), + new RdSignal(JetBrains.Rd.Impl.Serializers.ReadString, JetBrains.Rd.Impl.Serializers.WriteString), + new RdSignal(JetBrains.Rd.Impl.Serializers.ReadString, JetBrains.Rd.Impl.Serializers.WriteString), + new RdSignal(JetBrains.Rd.Impl.Serializers.ReadInt, JetBrains.Rd.Impl.Serializers.WriteInt), + new RdSignal(JetBrains.Rd.Impl.Serializers.ReadVoid, JetBrains.Rd.Impl.Serializers.WriteVoid), + new RdSignal(JetBrains.Rd.Impl.Serializers.ReadString, JetBrains.Rd.Impl.Serializers.WriteString), + new RdSignal(JetBrains.Rd.Impl.Serializers.ReadInt, JetBrains.Rd.Impl.Serializers.WriteInt) + ) {} + //deconstruct trait + //statics + + + + protected override long SerializationHash => 6014484928290881L; + + protected override Action Register => RegisterDeclaredTypesSerializers; + public static void RegisterDeclaredTypesSerializers(ISerializers serializers) + { + + serializers.RegisterToplevelOnce(typeof(IdeRoot), IdeRoot.RegisterDeclaredTypesSerializers); + } + + + //constants + + //custom body + //methods + //equals trait + //hash code trait + //pretty print + public override void Print(PrettyPrinter printer) + { + printer.Println("UtBotRiderModel ("); + using (printer.IndentCookie()) { + printer.Print("startPublish = "); _StartPublish.PrintEx(printer); printer.Println(); + printer.Print("logPublishOutput = "); _LogPublishOutput.PrintEx(printer); printer.Println(); + printer.Print("logPublishError = "); _LogPublishError.PrintEx(printer); printer.Println(); + printer.Print("stopPublish = "); _StopPublish.PrintEx(printer); printer.Println(); + printer.Print("startVSharp = "); _StartVSharp.PrintEx(printer); printer.Println(); + printer.Print("logVSharp = "); _LogVSharp.PrintEx(printer); printer.Println(); + printer.Print("stopVSharp = "); _StopVSharp.PrintEx(printer); printer.Println(); + } + printer.Print(")"); + } + //toString + public override string ToString() + { + var printer = new SingleLinePrettyPrinter(); + Print(printer); + return printer.ToString(); + } + } + public static class SolutionUtBotRiderModelEx + { + public static UtBotRiderModel GetUtBotRiderModel(this Solution solution) + { + return solution.GetOrCreateExtension("utBotRiderModel", () => new UtBotRiderModel()); + } + } + + + /// + ///

    Generated from: UtBotRiderModel.kt:9

    + ///
    + public sealed class StartPublishArgs : IPrintable, IEquatable + { + //fields + //public fields + [NotNull] public string FileName {get; private set;} + [NotNull] public string Arguments {get; private set;} + [NotNull] public string WorkingDirectory {get; private set;} + + //private fields + //primary constructor + public StartPublishArgs( + [NotNull] string fileName, + [NotNull] string arguments, + [NotNull] string workingDirectory + ) + { + if (fileName == null) throw new ArgumentNullException("fileName"); + if (arguments == null) throw new ArgumentNullException("arguments"); + if (workingDirectory == null) throw new ArgumentNullException("workingDirectory"); + + FileName = fileName; + Arguments = arguments; + WorkingDirectory = workingDirectory; + } + //secondary constructor + //deconstruct trait + public void Deconstruct([NotNull] out string fileName, [NotNull] out string arguments, [NotNull] out string workingDirectory) + { + fileName = FileName; + arguments = Arguments; + workingDirectory = WorkingDirectory; + } + //statics + + public static CtxReadDelegate Read = (ctx, reader) => + { + var fileName = reader.ReadString(); + var arguments = reader.ReadString(); + var workingDirectory = reader.ReadString(); + var _result = new StartPublishArgs(fileName, arguments, workingDirectory); + return _result; + }; + + public static CtxWriteDelegate Write = (ctx, writer, value) => + { + writer.Write(value.FileName); + writer.Write(value.Arguments); + writer.Write(value.WorkingDirectory); + }; + + //constants + + //custom body + //methods + //equals trait + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((StartPublishArgs) obj); + } + public bool Equals(StartPublishArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return FileName == other.FileName && Arguments == other.Arguments && WorkingDirectory == other.WorkingDirectory; + } + //hash code trait + public override int GetHashCode() + { + unchecked { + var hash = 0; + hash = hash * 31 + FileName.GetHashCode(); + hash = hash * 31 + Arguments.GetHashCode(); + hash = hash * 31 + WorkingDirectory.GetHashCode(); + return hash; + } + } + //pretty print + public void Print(PrettyPrinter printer) + { + printer.Println("StartPublishArgs ("); + using (printer.IndentCookie()) { + printer.Print("fileName = "); FileName.PrintEx(printer); printer.Println(); + printer.Print("arguments = "); Arguments.PrintEx(printer); printer.Println(); + printer.Print("workingDirectory = "); WorkingDirectory.PrintEx(printer); printer.Println(); + } + printer.Print(")"); + } + //toString + public override string ToString() + { + var printer = new SingleLinePrettyPrinter(); + Print(printer); + return printer.ToString(); + } + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/ProcessWithRdServer.cs b/utbot-rider/src/dotnet/UtBot/UtBot/ProcessWithRdServer.cs new file mode 100644 index 0000000000..0d7f3616ff --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/ProcessWithRdServer.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using JetBrains.Annotations; +using JetBrains.Application.Threading; +using JetBrains.Collections.Viewable; +using JetBrains.Lifetimes; +using JetBrains.Rd; +using JetBrains.Rd.Impl; +using JetBrains.Rider.Model; +using JetBrains.Threading; +using JetBrains.Util; +using JetBrains.Util.Logging; +using UtBot.Rd; +using UtBot.Rd.Generated; + +namespace UtBot; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class ProcessWithRdServer +{ + public Lifetime Lifetime => _ldef.Lifetime; + public Protocol Protocol; + [CanBeNull] public VSharpModel VSharpModel { get; private set; } + + private readonly LifetimeDefinition _ldef; + public readonly Process Proc = new(); + + public ProcessWithRdServer(string name, string workingDir, int port, string exePath, IShellLocks shellLocks, UtBotRiderModel riderModel, Lifetime? parent = null, [CanBeNull] ILogger logger = null) + { + logger ??= Logger.GetLogger(); + using var blockingCollection = new BlockingCollection(2); + shellLocks.AssertNonMainThread(); + _ldef = (parent ?? Lifetime.Eternal).CreateNested(); + var pingLdef = _ldef.Lifetime.CreateNested(); + try + { + SingleThreadScheduler.RunOnSeparateThread(Lifetime, name, scheduler => + { + var endPoint = new IPEndPoint(IPAddress.Loopback, port); + var socket = SocketWire.Server.CreateServerSocket(endPoint); + var wire = new SocketWire.Server(Lifetime, scheduler, socket); + var serializers = new Serializers(); + var identities = new Identities(IdKind.Server); + var startInfo = new ProcessStartInfo("dotnet", $"--roll-forward LatestMajor \"{exePath}\" {port}") + { + WorkingDirectory = workingDir + }; + riderModel.StartVSharp.Fire(); + Protocol = new Protocol(name, serializers, identities, scheduler, wire, Lifetime); + scheduler.Queue(() => + { + VSharpModel = new VSharpModel(Lifetime, Protocol); + VSharpModel.Ping.Advise(pingLdef.Lifetime, s => + { + if (s == name) + { + blockingCollection.TryAdd(s); + } + }); + VSharpModel.Log.Advise(Lifetime, s => + { + logger.Info($"V#: {s}"); + riderModel.LogVSharp.Fire(s); + }); + }); + Proc.StartInfo = startInfo; + Lifetime.OnTermination(() => Proc.Kill(entireProcessTree: true)); + if (Proc.Start()) + Proc.Exited += (_, _) => _ldef.Terminate(); + else + _ldef.Terminate(); + }); + + if (Proc?.HasExited == true) return; + + SpinWaitEx.SpinUntil(pingLdef.Lifetime, () => + { + if (Proc?.HasExited == true) + { + VSharpModel = null; + _ldef.Terminate(); + } + + VSharpModel?.Ping.Fire(RdUtil.MainProcessName); + return blockingCollection.TryTake(out _); + }); + pingLdef.Terminate(); + } + catch (Exception) + { + _ldef.Terminate(); + throw; + } + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/UnitTestBuilder.cs b/utbot-rider/src/dotnet/UtBot/UtBot/UnitTestBuilder.cs new file mode 100644 index 0000000000..e0cc0a98e2 --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/UnitTestBuilder.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Timers; +using JetBrains.Application.Notifications; +using JetBrains.Application.Progress; +using JetBrains.Application.Threading; +using JetBrains.Application.Threading.Tasks; +using JetBrains.Application.UI.Controls; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ProjectModel.ProjectsHost; +using JetBrains.Rd.Tasks; +using JetBrains.RdBackend.Common.Features; +using JetBrains.ReSharper.Feature.Services.CSharp.Generate; +using JetBrains.ReSharper.Feature.Services.Generate; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.CSharp; +using JetBrains.ReSharper.Psi.Util; +using JetBrains.Rider.Model; +using JetBrains.Util; +using JetBrains.Util.Threading; +using UtBot.Rd; +using UtBot.Rd.Generated; +using UtBot.Utils; +using UtBot.VSharp; + +namespace UtBot; + +[GeneratorBuilder(GenerateUnitTestWorkflow.Kind, typeof(CSharpLanguage))] +internal sealed class UnitTestBuilder : GeneratorBuilderBase +{ + private const string PublishDirName = "utbot-publish"; + + private readonly IBackgroundProgressIndicatorManager _backgroundProgressIndicatorManager; + private readonly Lifetime _lifetime; + private readonly ILogger _logger; + private readonly IShellLocks _shellLocks; + private readonly UtBotRiderModel _riderModel; + private readonly Notifications _notifications; + + public UnitTestBuilder( + Lifetime lifetime, + ISolution solution, + IShellLocks shellLocks, + IBackgroundProgressIndicatorManager backgroundProgressIndicatorManager, + ILogger logger, + Notifications notifications) + { + _lifetime = lifetime; + _shellLocks = shellLocks; + _backgroundProgressIndicatorManager = backgroundProgressIndicatorManager; + _logger = logger; + _notifications = notifications; + _riderModel = solution.GetProtocolSolution().GetUtBotRiderModel(); + } + + protected override void Process(CSharpGeneratorContext context, IProgressIndicator progress) + { + _notifications.Refresh(); + + var timeoutString = context.GetOption(TimeoutGeneratorOption.Id); + + if (!int.TryParse(timeoutString, out var timeout) || timeout <= 0) + { + _notifications.ShowError("Invalid timeout value. Timeout should be an integer number greater than zero"); + return; + } + + if (context.PsiModule.ContainingProjectModule is not IProject project) return; + + if (!DotNetVersionUtils.CanRunVSharp(project.GetSolution())) + { + _notifications.ShowError($"At least .NET {DotNetVersionUtils.MinCompatibleSdkMajor} SDK is required for UnitTestBot.NET"); + return; + } + + var typeElement = context.ClassDeclaration.DeclaredElement; + if (typeElement == null) return; + if (typeElement is not IClass && typeElement is not IStruct) return; + var testProjectTfm = DotNetVersionUtils.GetTestProjectFramework(project); + + var jsonOptions = new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + var descriptors = new List(); + foreach (var inputElement in context.InputElements.WithProgress(progress, "Generating unit tests") + .OfType>()) + { + var methodName = inputElement.DeclaredElement.ShortName; + var hasNoOverLoads = typeElement.GetAllClassMembers().Count(m => m.Member.ShortName == methodName) == 1; + + var parameterDescriptors = new List(); + + if (!hasNoOverLoads) + { + foreach (var parameter in inputElement.DeclaredElement.Parameters) + { + var typeDescriptor = ToTypeDescriptor(parameter.Type, inputElement.DeclaredElement, typeElement); + parameterDescriptors.Add(JsonSerializer.Serialize(typeDescriptor, jsonOptions)); + } + } + + descriptors.Add(new MethodDescriptor(methodName, typeElement.GetClrName().FullName, hasNoOverLoads, parameterDescriptors)); + } + + var progressLifetimeDef = _lifetime.CreateNested(); + var indicator = + _backgroundProgressIndicatorManager.CreateIndicator(progressLifetimeDef.Lifetime, true, true, + "Generating unit tests"); + + _shellLocks.Tasks.StartNew(_lifetime, Scheduling.FreeThreaded, () => + { + try + { + Generate(indicator, project, descriptors, testProjectTfm, timeout); + } + finally + { + progressLifetimeDef.Terminate(); + } + }); + } + + private TypeDescriptor ToTypeDescriptor(IType typ, IMethod declMethod, ITypeElement declTyp) + { + var par = typ.GetTypeParameterType(); + + if (declMethod.TypeParameters.Contains(par)) + { + return new TypeDescriptor + { + ArrayRank = null, + Name = null, + MethodParameterPosition = declMethod.TypeParameters.IndexOf(par), + TypeParameterPosition = null, + Parameters = new() + }; + } + + if (declTyp.TypeParameters.Contains(par)) + { + return new TypeDescriptor + { + ArrayRank = null, + Name = null, + MethodParameterPosition = null, + TypeParameterPosition = declTyp.TypeParameters.IndexOf(par), + Parameters = new() + }; + } + + if (typ is IArrayType arr) + { + var elementType = ToTypeDescriptor(typ.GetScalarType(), declMethod, declTyp); + + return new TypeDescriptor + { + ArrayRank = arr.Rank, + Name = null, + MethodParameterPosition = null, + TypeParameterPosition = null, + Parameters = new List { elementType } + }; + } + + var subst = typ.GetScalarType().GetSubstitution(); + var pars = typ.GetTypeElement().TypeParameters.Select(p => ToTypeDescriptor(subst[p], declMethod, declTyp)); + + return new TypeDescriptor + { + ArrayRank = null, + Name = typ.GetScalarType()?.GetClrName().FullName, + MethodParameterPosition = null, + TypeParameterPosition = null, + Parameters = pars.ToList() + }; + } + + private void Generate(IBackgroundProgressIndicator progressIndicator, IProject project, + List descriptors, TestProjectTargetFramework testProjectFramework, int timeout) + { + var solution = project.GetSolution(); + var solutionMark = solution.GetSolutionMark(); + if (solutionMark == null) return; + + var solutionFilePath = solutionMark.Location.FullPath; + _logger.Verbose($"Solution path: {solutionFilePath}"); + + project.Locks.AssertNonMainThread(); + + var config = project.ProjectProperties.ActiveConfigurations.Configurations.First(); + var outputDir = project.GetOutputDirectory(config.TargetFrameworkId).Combine(PublishDirName); + progressIndicator.Header.SetValue($"Publishing dependencies to {PublishDirName}..."); + + if (!ProjectPublisher.PublishSync(_logger, progressIndicator, project, config, outputDir, _riderModel)) { + var title = $"Cannot publish project {project.Name}"; + _notifications.ShowError(title); + return; + } + + var assemblyFileName = project.GetOutputFilePath(config.TargetFrameworkId).Name; + var assemblyPath = Directory.GetFiles(outputDir.FullPath, assemblyFileName, SearchOption.AllDirectories).FirstOrDefault(); + if (assemblyPath is null) + { + _notifications.ShowError($"Cannot build project {project.Name}"); + return; + } + + var typeName = descriptors.Select(m => m.TypeName).Distinct().SingleOrDefault(); + + _logger.Verbose($"Start Generation for {typeName}"); + progressIndicator.Lifetime.ThrowIfNotAlive(); + progressIndicator.Header.SetValue(typeName); + + var pluginPath = FileSystemPath.Parse(Assembly.GetExecutingAssembly().Location).Parent; + var vsharpRunner = pluginPath.Combine("UtBot.VSharp.dll"); + + var methodNames = descriptors.Select(m => $"{m.TypeName}.{m.MethodName}").ToArray(); + var intervalS = (double)timeout / methodNames.Length; + var intervalMs = intervalS > 0 ? intervalS * 1000 : 500; + using var methodProgressTimer = new Timer(intervalMs); + var i = 0; + void ChangeMethodName() + { + progressIndicator.Header.SetValue(methodNames[i]); + i = (i + 1) % methodNames.Length; + } + methodProgressTimer.Elapsed += (_, _) => ChangeMethodName(); + _logger.Catch(() => + { + var name = VSharpMain.VSharpProcessName; + var workingDir = project.ProjectFileLocation.Directory.FullPath; + var port = NetworkUtil.GetFreePort(); + var runnerPath = vsharpRunner.FullPath; + var proc = new ProcessWithRdServer(name, workingDir, port, runnerPath, project.Locks, _riderModel, _lifetime, _logger); + var projectCsprojPath = project.ProjectFileLocation.FullPath; + List allAssemblies; + using (_shellLocks.UsingReadLock()) + { + allAssemblies = solution.GetAllAssemblies() + .Where(it => it.Location.AssemblyPhysicalPath is not null) + .Select(it => new MapEntry(it.FullAssemblyName, it.Location.AssemblyPhysicalPath.FullPath)) + .DistinctBy(it => it.Key) + .ToList(); + } + var args = new GenerateArguments(assemblyPath, projectCsprojPath, solutionFilePath, descriptors, + timeout, testProjectFramework.FrameworkMoniker.Name, allAssemblies); + var vSharpTimeout = TimeSpan.FromSeconds(timeout); + var rpcTimeout = new RpcTimeouts(vSharpTimeout + TimeSpan.FromSeconds(1), vSharpTimeout + TimeSpan.FromSeconds(30)); + ChangeMethodName(); + methodProgressTimer.Start(); + var result = proc.VSharpModel?.Generate.Sync(args, rpcTimeout); + methodProgressTimer.Stop(); + proc.Proc.WaitForExit(); + _riderModel.StopVSharp.Fire(proc.Proc.ExitCode); + _logger.Info("Result acquired"); + if (result is { GeneratedProjectPath: not null }) + { + _shellLocks.ExecuteOrQueue(_lifetime, "UnitTestBuilder::Generate", () => + { + if (solution.IsValid()) + { + solution.GetProtocolSolution().GetFileSystemModel().RefreshPaths + .Start(_lifetime, + new RdFsRefreshRequest(new List { result.GeneratedProjectPath }, true)); + + } + }); + + _notifications.ShowInfo( + $"Generated {result.TestsCount} tests and found {result.ErrorsCount} errors for {typeName}"); + + if (testProjectFramework.IsDefault) + { + _notifications.ShowWarning( + $"Generated test project targets {testProjectFramework.FrameworkMoniker}, which is not directly targeted by {project.Name}. " + + "Test project may fail to compile due to reference errors"); + } + } + else + { + var ex = result == null ? "Could not start V#" : result.ExceptionMessage; + _logger.Info($"Could not generate tests for ${typeName}, exception - {ex}"); + + var title = $"Could not generate tests for {typeName}"; + var openExceptionMessageCommand = new UserNotificationCommand( + "Show error info", + () => MessageBox.ShowError(ex ?? "Cannot get error info", title)); + _notifications.ShowError(title, command: openExceptionMessageCommand); + } + }); + + methodProgressTimer.Stop(); + _logger.Verbose($"Generation finished for {typeName}"); + + _shellLocks.ExecuteOrQueue(_lifetime, "UnitTestBuilder::Generate", () => + { + if (project.IsValid()) + solution.GetProtocolSolution().GetFileSystemModel().RefreshPaths + .Start(_lifetime, + new RdFsRefreshRequest(new List { solutionMark.Location.FullPath }, true)); + }); + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/UtBot.csproj b/utbot-rider/src/dotnet/UtBot/UtBot/UtBot.csproj new file mode 100644 index 0000000000..83a35b478b --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/UtBot.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + UtBot + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/Utils/DotNetVersionUtils.cs b/utbot-rider/src/dotnet/UtBot/UtBot/Utils/DotNetVersionUtils.cs new file mode 100644 index 0000000000..48aa3380f9 --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/Utils/DotNetVersionUtils.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using JetBrains.ProjectModel; +using JetBrains.Util; + +namespace UtBot.Utils; + +internal readonly record struct TestProjectTargetFramework(FrameworkMoniker FrameworkMoniker, bool IsDefault); + +internal static class DotNetVersionUtils +{ + public const int MinCompatibleSdkMajor = 7; + private const string NUnitProjectMinTfm = "net6.0"; + + public static bool CanRunVSharp(ISolution solution) => GetCanRunVSharp(solution.SolutionDirectory.FullPath); + + public static TestProjectTargetFramework GetTestProjectFramework(IProject project) + { + var path = project.ProjectFileLocation.Directory.FullPath; + + var nUnitNewInfo = RunProcess(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "new nunit -f", + WorkingDirectory = path + }, returnError: true); + + if (nUnitNewInfo == null) + throw new Exception("Could not get new NUnit info"); + + var matches = DotNetRegex.Any.Matches(nUnitNewInfo); + + if (matches.IsEmpty()) + throw new Exception("Could not parse TFMs from new NUnit info"); + + // Code in Allocator.cs uses methods which were added in .NET 6 + var testProjectRequiredFramework = new FrameworkMoniker(NUnitProjectMinTfm); + + var availableTfms = matches + .Select(m => new FrameworkMoniker(m.Value)) + .Distinct() + .Where(s => s.CompareTo(testProjectRequiredFramework) >= 0) + .OrderBy() + .ToList(); + + var projectTfms = GetTfms(project).ToList(); + + TestProjectTargetFramework framework; + + var exactMatch = availableTfms.FirstOrDefault(t => projectTfms.Contains(t)); + + if (exactMatch is not null) + { + return new(exactMatch, false); + } + + var projectStandards = projectTfms.Where(t => t.Kind is DotNetKind.NetStandard); + var standardMatch = + availableTfms.FirstOrDefault(t => projectStandards.Any(t.ImplementsStandard)); + + if (standardMatch is not null) + { + return new(standardMatch, false); + } + + var defaultTfm = availableTfms.First(); + return new(defaultTfm, true); + } + + private static bool GetCanRunVSharp(string workingDir) + { + var sdksInfo = RunProcess(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "--list-sdks", + WorkingDirectory = workingDir + }); + + if (sdksInfo is null) + { + return false; + } + + var matches = Regex.Matches(sdksInfo, @"(\d+)\.(\d+)\.(\d+)"); + + if (matches.Count < 1) + { + return false; + } + + for (var i = 0; i < matches.Count; ++i) + { + if (!int.TryParse(matches[i].Groups[1].Value, out var majorVersion)) + { + continue; + } + + if (majorVersion >= MinCompatibleSdkMajor) + { + return true; + } + } + + return false; + } + + private static IEnumerable GetTfms(IProject project) => + project.TargetFrameworkIds + .Select(i => new FrameworkMoniker(i.TryGetShortIdentifier())); + + private static string RunProcess(ProcessStartInfo startInfo, bool returnError = false) + { + startInfo.RedirectStandardError = true; + startInfo.RedirectStandardOutput = true; + + var pi = Process.Start(startInfo); + var s = returnError ? pi?.StandardError.ReadToEnd() : pi?.StandardOutput.ReadToEnd(); + pi?.WaitForExit(); + return s; + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/Utils/FrameworkMoniker.cs b/utbot-rider/src/dotnet/UtBot/UtBot/Utils/FrameworkMoniker.cs new file mode 100644 index 0000000000..0d7dce61ae --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/Utils/FrameworkMoniker.cs @@ -0,0 +1,148 @@ +using System; +using System.Text.RegularExpressions; +using JetBrains.Util; + +namespace UtBot.Utils; + +internal static class DotNetRegex +{ + public static readonly Regex ModernNet = new(@"net\d+\.\d+"); + public static readonly Regex NetFramework = new(@"net\d+"); + public static readonly Regex NetCore = new(@"netcoreapp(\d+)\.(\d+)"); + public static readonly Regex NetStandard = new(@"netstandard\d+\.\d+"); + public static readonly Regex Any = new(@"net\d+\.\d+|netcoreapp\d+\.\d+|net\d+"); +} + +internal enum DotNetKind +{ + NetFramework, + NetCore, + Net, + NetStandard, + Unknown +} + +internal class FrameworkMoniker : IComparable +{ + public string Name { get; } + public DotNetKind Kind { get; } + + public FrameworkMoniker(string name) + { + // Checking 'netcoreapp6.0' etc. cases --- look like .NET Core, but in fact .NET + var matches = DotNetRegex.NetCore.Matches(name); + if (matches.IsEmpty()) + { + Name = name; + Kind = GetKind(name); + return; + } + + var major = int.Parse(matches[0].Groups[1].Value); + if (major >= 5) + { + var minor = int.Parse(matches[0].Groups[2].Value); + Name = $"net{major}.{minor}"; + Kind = DotNetKind.Net; + return; + } + + Name = name; + Kind = DotNetKind.NetCore; + } + + private const string NetStandard10 = "netstandard1.0"; + private const string NetStandard11 = "netstandard1.1"; + private const string NetStandard12 = "netstandard1.2"; + private const string NetStandard13 = "netstandard1.3"; + private const string NetStandard14 = "netstandard1.4"; + private const string NetStandard20 = "netstandard2.0"; + private const string NetStandard21 = "netstandard2.1"; + + private static DotNetKind GetKind(string tfm) + { + if (DotNetRegex.ModernNet.IsMatch(tfm)) + { + return DotNetKind.Net; + } + + if (DotNetRegex.NetFramework.IsMatch(tfm)) + { + return DotNetKind.NetFramework; + } + + if (DotNetRegex.NetStandard.IsMatch(tfm)) + { + return DotNetKind.NetStandard; + } + + return DotNetRegex.NetCore.IsMatch(tfm) ? DotNetKind.NetCore : DotNetKind.Unknown; + } + + public int CompareTo(FrameworkMoniker other) => + (Kind, other.Kind) switch + { + (DotNetKind.NetStandard, not DotNetKind.NetStandard) or + (not DotNetKind.NetStandard, DotNetKind.NetStandard) => + throw new InvalidOperationException("Cannot compare .NET Standard and specific .NET"), + + (DotNetKind.NetFramework, DotNetKind.NetCore) or + (DotNetKind.NetFramework, DotNetKind.Net) or + (DotNetKind.NetCore, DotNetKind.Net) => -1, + + (DotNetKind.NetCore, DotNetKind.NetFramework) or + (DotNetKind.Net, DotNetKind.NetFramework) or + (DotNetKind.Net, DotNetKind.NetCore) => 1, + + _ => string.Compare(Name, other.Name, StringComparison.InvariantCulture) + }; + + // https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-1-5#select-net-standard-version + public bool ImplementsStandard(FrameworkMoniker standardFrameworkMoniker) + { + if (standardFrameworkMoniker.Kind is not DotNetKind.NetStandard) + { + throw new ArgumentException("standardTfm is not a .NET Standard TFM"); + } + + var standardName = standardFrameworkMoniker.Name; + + return Kind switch + { + DotNetKind.Net => true, + DotNetKind.NetFramework => + Name switch + { + "net45" => standardName is NetStandard10 or NetStandard11, + "net451" or "net452" => standardName is NetStandard10 or NetStandard11 or NetStandard12, + "net46" => standardName is NetStandard10 or NetStandard11 or NetStandard12 or NetStandard13, + "net461" => standardName is NetStandard10 or NetStandard11 or NetStandard12 or NetStandard13 + or NetStandard14, + _ => standardName is not NetStandard21 + }, + DotNetKind.NetCore => + Name switch + { + "netcoreapp1.0" or "netcoreapp1.1" => standardName is not (NetStandard20 or NetStandard21), + "netcoreapp2.0" or "netcoreapp2.1" or "netcoreapp2.2" => standardName is not NetStandard21, + _ => true + }, + DotNetKind.NetStandard => CompareTo(standardFrameworkMoniker) >= 0, + _ => false + }; + } + + public override bool Equals(object obj) + { + if (obj is not FrameworkMoniker another) + { + return false; + } + + return Name.Equals(another.Name); + } + + public override int GetHashCode() => Name.GetHashCode(); + + public override string ToString() => Name; +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/Utils/Notifications.cs b/utbot-rider/src/dotnet/UtBot/UtBot/Utils/Notifications.cs new file mode 100644 index 0000000000..140f9d5eac --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/Utils/Notifications.cs @@ -0,0 +1,98 @@ +using JetBrains.Annotations; +using JetBrains.Application.Notifications; +using JetBrains.Application.Threading; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.Util; + +namespace UtBot.Utils; + +[SolutionComponent] +internal class Notifications +{ + private Lifetime _lifetime; + private readonly SequentialLifetimes _sequentialLifetimes; + private readonly UserNotifications _userNotifications; + private readonly IShellLocks _shellLocks; + private readonly ILogger _logger; + + private const string Title = "UnitTestBot.NET"; + + public Notifications( + Lifetime lifetime, + UserNotifications userNotifications, + IShellLocks shellLocks, + ILogger logger) + { + _sequentialLifetimes = new SequentialLifetimes(lifetime); + _lifetime = _sequentialLifetimes.Next(); + _userNotifications = userNotifications; + _shellLocks = shellLocks; + _logger = logger; + } + + private void ShowNotification( + NotificationSeverity severity, + string title, + string body, + bool closeAfterExecution = true, + UserNotificationCommand command = null) + { + void NotificationAction() + { + _userNotifications.CreateNotification( + _lifetime, + severity, + title, + body, + closeAfterExecution: closeAfterExecution, + executed: command); + } + + _shellLocks.ExecuteOrQueueEx( + _lifetime, + "UtBot::Notification::Show", + NotificationAction); + } + + public void ShowError( + [NotNull] string body, + bool closeAfterExecution = true, + UserNotificationCommand command = null) + { + ShowNotification( + NotificationSeverity.CRITICAL, + Title, + body, + closeAfterExecution: closeAfterExecution, + command: command); + } + + public void ShowInfo( + [NotNull] string body, + bool closeAfterExecution = true, + UserNotificationCommand command = null) + { + ShowNotification( + NotificationSeverity.INFO, + Title, + body, + closeAfterExecution: closeAfterExecution, + command: command); + } + + public void ShowWarning( + [NotNull] string body, + bool closeAfterExecution = true, + UserNotificationCommand command = null) + { + ShowNotification( + NotificationSeverity.WARNING, + Title, + body, + closeAfterExecution: closeAfterExecution, + command: command); + } + + public void Refresh() => _lifetime = _sequentialLifetimes.Next(); +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/Utils/ProjectPublisher.cs b/utbot-rider/src/dotnet/UtBot/UtBot/Utils/ProjectPublisher.cs new file mode 100644 index 0000000000..8358bc2c02 --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/Utils/ProjectPublisher.cs @@ -0,0 +1,105 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using JetBrains.Application.Threading.Tasks; +using JetBrains.Application.UI.Controls; +using JetBrains.Collections.Viewable; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ProjectModel.Properties; +using JetBrains.Rider.Model; +using JetBrains.Threading; +using JetBrains.Util; +using UtBot.Rd.Generated; + +namespace UtBot.Utils; + +public static class ProjectPublisher +{ + public static bool PublishSync(ILogger logger, IBackgroundProgressIndicator indicator, IProject project, IProjectConfiguration config, VirtualFileSystemPath outputDir, UtBotRiderModel model) + { + var publishLifetimeDef = indicator.Lifetime.CreateNested(); + indicator.Cancel.Advise(Lifetime.Eternal, canceled => {if (canceled) publishLifetimeDef.Terminate();}); + + try + { + Directory.CreateDirectory(outputDir.FullPath); + var projectName = project.ProjectFileLocation.Name; + var architecture = GetArchitecture(); + var tfm = config.TargetFrameworkId.TryGetShortIdentifier(); + var command = config.TargetFrameworkId.IsNetFramework ? "build" : "publish"; + + if (tfm is null) + { + throw new ArgumentException("Cannot get framework moniker from project config"); + } + + var processInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = + $"{command} \"{projectName}\" --sc -c {config.Name} -a {architecture} -o {outputDir.FullPath} -f {tfm}", + WorkingDirectory = project.ProjectFileLocation.Directory.FullPath, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + using var process = new Process(); + process.StartInfo = processInfo; + process.OutputDataReceived += (_, args) => + { + model.LogPublishOutput.Fire(args.Data ?? "\n"); + }; + process.ErrorDataReceived += (_, args) => + { + model.LogPublishError.Fire(args.Data ?? "\n"); + }; + process.Exited += (_, a) => + { + publishLifetimeDef.Terminate(); + }; + model.StartPublish.Fire(new StartPublishArgs(processInfo.FileName, processInfo.Arguments, processInfo.WorkingDirectory)); + if (process.Start()) + { + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + SpinWaitEx.SpinUntil(publishLifetimeDef.Lifetime, () => process.HasExited); + if (process.ExitCode != 0) + { + logger.Warn($"Publish process exited with code {process.ExitCode}: {process.StandardOutput.ReadToEnd()}"); + } + model.StopPublish.Fire(process.ExitCode); + } + else + { + model.StopPublish.Fire(-1); + } + return process.HasExited && process.ExitCode == 0; + } + catch (Exception e) + { + logger.Warn(e, comment: "Could not publish project for VSharp, exception occured"); + return false; + } + finally + { + publishLifetimeDef.Terminate(); + } + } + + private static string GetArchitecture() + { + var arch = RuntimeInformation.OSArchitecture; + return arch switch + { + Architecture.X86 => "x86", + Architecture.X64 => "x64", + Architecture.Arm => "arm", + Architecture.Arm64 => "arm64", + Architecture.Wasm or Architecture.S390x => + throw new InvalidOperationException($"Unsupported architecture: {arch}"), + _ => throw new ArgumentOutOfRangeException() + }; + } +} diff --git a/utbot-rider/src/dotnet/UtBot/UtBot/ZoneMarker.cs b/utbot-rider/src/dotnet/UtBot/UtBot/ZoneMarker.cs new file mode 100644 index 0000000000..927016ff1c --- /dev/null +++ b/utbot-rider/src/dotnet/UtBot/UtBot/ZoneMarker.cs @@ -0,0 +1,22 @@ +using JetBrains.Application.BuildScript.Application.Zones; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi.CSharp; +using JetBrains.ReSharper.UnitTestFramework; +using JetBrains.Rider.Model; + +namespace UtBot; + +[ZoneDefinition(ZoneFlags.AutoEnable)] +public interface IUtBotPluginZone : + IZone, + IRequire, + IRequire, + IRequire, + IRequire +{ +} + +[ZoneMarker] +public class ZoneMarker : IRequire +{ +} \ No newline at end of file diff --git a/utbot-rider/src/main/kotlin/org/utbot/rider/UtBotConsoleViewComponent.kt b/utbot-rider/src/main/kotlin/org/utbot/rider/UtBotConsoleViewComponent.kt new file mode 100644 index 0000000000..25b3c7c61b --- /dev/null +++ b/utbot-rider/src/main/kotlin/org/utbot/rider/UtBotConsoleViewComponent.kt @@ -0,0 +1,118 @@ +package org.utbot.rider + +import com.intellij.execution.executors.DefaultRunExecutor +import com.intellij.execution.filters.TextConsoleBuilderFactory +import com.intellij.execution.ui.ConsoleView +import com.intellij.execution.ui.ConsoleViewContentType +import com.intellij.execution.ui.RunContentDescriptor +import com.intellij.execution.ui.RunContentManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.rd.createLifetime +import com.jetbrains.rd.platform.util.idea.ProtocolSubscribedProjectComponent +import com.jetbrains.rd.platform.util.lifetime +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.reactive.ISignal +import com.jetbrains.rider.projectView.solution +import org.utbot.rider.generated.UtBotRiderModel +import org.utbot.rider.generated.utBotRiderModel +import kotlin.random.Random + +private fun ConsoleView.printYellowLine(any: Any) { + print("$any\n", ConsoleViewContentType.LOG_INFO_OUTPUT) +} + +private fun ConsoleView.printNormalLine(any: Any) { + print("$any\n", ConsoleViewContentType.NORMAL_OUTPUT) +} + +private fun ConsoleView.printErrorLine(any: Any) { + print("$any\n", ConsoleViewContentType.ERROR_OUTPUT) +} + +private fun ISignal.advisePrintExitCode(lifetime: Lifetime, consoleView: ConsoleView) { + advise(lifetime) { exitCode -> + val msg = "Process exited with code: $exitCode" + if (exitCode == 0) + consoleView.printNormalLine(msg) + else + consoleView.printErrorLine(msg) + } +} + +private fun ISignal.advisePrintNormal(lifetime: Lifetime, consoleView: ConsoleView) { + advise(lifetime) { output -> + consoleView.printNormalLine(output) + } +} + +private fun ISignal.advisePrintError(lifetime: Lifetime, consoleView: ConsoleView) { + advise(lifetime) { output -> + consoleView.printErrorLine(output) + } +} + +class UtBotConsoleViewComponent(project: Project) : ProtocolSubscribedProjectComponent(project) { + private var currentExecutionId: Long = -1 + private val model: UtBotRiderModel + + private fun update( + firstLine: String, + displayName: String, + previous: RunContentDescriptor? + ): Triple { + val textConsoleBuilderFactory = TextConsoleBuilderFactory.getInstance() + val consoleView = textConsoleBuilderFactory.createBuilder(project).console + val runContentDescriptor = + RunContentDescriptor(consoleView, null, consoleView.component, displayName).apply { + executionId = currentExecutionId + } + val runContentManager = RunContentManager.getInstance(project) + + runContentManager.showRunContent( + DefaultRunExecutor.getRunExecutorInstance(), + runContentDescriptor, + previous + ) + consoleView.printYellowLine(firstLine) + + return Triple(runContentDescriptor, consoleView, consoleView.createLifetime()) + } + + private fun initPublish() { + var previousPublish: RunContentDescriptor? = null + model.startPublish.advise(project.lifetime) { publishArgs -> + currentExecutionId = Random.nextLong() + val (fileName, arguments, workingDirectory) = publishArgs + val firstLine = "$workingDirectory .> $fileName $arguments" + val (currentPublish, consoleView, consoleLifetime) = update( + firstLine, + "Project publish for UtBot", + previousPublish + ) + previousPublish = currentPublish + model.logPublishOutput.advisePrintNormal(consoleLifetime, consoleView) + model.logPublishError.advisePrintError(consoleLifetime, consoleView) + model.stopPublish.advisePrintExitCode(consoleLifetime, consoleView) + } + } + + init { + model = project.solution.utBotRiderModel + initPublish() + initVSharp() + } + + private fun initVSharp() { + var previousVSharp: RunContentDescriptor? = null + model.startVSharp.advise(project.lifetime) { + val (currentVSharp, consoleView, consoleLifetime) = update( + "Started VSharp Engine", + "Running VSharp Engine", + previousVSharp + ) + previousVSharp = currentVSharp + model.logVSharp.advisePrintNormal(consoleLifetime, consoleView) + model.stopVSharp.advisePrintExitCode(consoleLifetime, consoleView) + } + } +} \ No newline at end of file diff --git a/utbot-rider/src/main/kotlin/org/utbot/rider/generated/UtBotRiderModel.Generated.kt b/utbot-rider/src/main/kotlin/org/utbot/rider/generated/UtBotRiderModel.Generated.kt new file mode 100644 index 0000000000..f1dac3d1f2 --- /dev/null +++ b/utbot-rider/src/main/kotlin/org/utbot/rider/generated/UtBotRiderModel.Generated.kt @@ -0,0 +1,191 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.rider.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* +import com.jetbrains.rd.ide.model.Solution + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [UtBotRiderModel.kt:8] + */ +class UtBotRiderModel private constructor( + private val _startPublish: RdSignal, + private val _logPublishOutput: RdSignal, + private val _logPublishError: RdSignal, + private val _stopPublish: RdSignal, + private val _startVSharp: RdSignal, + private val _logVSharp: RdSignal, + private val _stopVSharp: RdSignal +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + serializers.register(StartPublishArgs) + } + + + + + + const val serializationHash = 6014484928290881L + + } + override val serializersOwner: ISerializersOwner get() = UtBotRiderModel + override val serializationHash: Long get() = UtBotRiderModel.serializationHash + + //fields + val startPublish: IAsyncSignal get() = _startPublish + val logPublishOutput: IAsyncSignal get() = _logPublishOutput + val logPublishError: IAsyncSignal get() = _logPublishError + val stopPublish: IAsyncSignal get() = _stopPublish + val startVSharp: IAsyncSignal get() = _startVSharp + val logVSharp: IAsyncSignal get() = _logVSharp + val stopVSharp: IAsyncSignal get() = _stopVSharp + //methods + //initializer + init { + _startPublish.async = true + _logPublishOutput.async = true + _logPublishError.async = true + _stopPublish.async = true + _startVSharp.async = true + _logVSharp.async = true + _stopVSharp.async = true + } + + init { + bindableChildren.add("startPublish" to _startPublish) + bindableChildren.add("logPublishOutput" to _logPublishOutput) + bindableChildren.add("logPublishError" to _logPublishError) + bindableChildren.add("stopPublish" to _stopPublish) + bindableChildren.add("startVSharp" to _startVSharp) + bindableChildren.add("logVSharp" to _logVSharp) + bindableChildren.add("stopVSharp" to _stopVSharp) + } + + //secondary constructor + internal constructor( + ) : this( + RdSignal(StartPublishArgs), + RdSignal(FrameworkMarshallers.String), + RdSignal(FrameworkMarshallers.String), + RdSignal(FrameworkMarshallers.Int), + RdSignal(FrameworkMarshallers.Void), + RdSignal(FrameworkMarshallers.String), + RdSignal(FrameworkMarshallers.Int) + ) + + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("UtBotRiderModel (") + printer.indent { + print("startPublish = "); _startPublish.print(printer); println() + print("logPublishOutput = "); _logPublishOutput.print(printer); println() + print("logPublishError = "); _logPublishError.print(printer); println() + print("stopPublish = "); _stopPublish.print(printer); println() + print("startVSharp = "); _startVSharp.print(printer); println() + print("logVSharp = "); _logVSharp.print(printer); println() + print("stopVSharp = "); _stopVSharp.print(printer); println() + } + printer.print(")") + } + //deepClone + override fun deepClone(): UtBotRiderModel { + return UtBotRiderModel( + _startPublish.deepClonePolymorphic(), + _logPublishOutput.deepClonePolymorphic(), + _logPublishError.deepClonePolymorphic(), + _stopPublish.deepClonePolymorphic(), + _startVSharp.deepClonePolymorphic(), + _logVSharp.deepClonePolymorphic(), + _stopVSharp.deepClonePolymorphic() + ) + } + //contexts +} +val Solution.utBotRiderModel get() = getOrCreateExtension("utBotRiderModel", ::UtBotRiderModel) + + + +/** + * #### Generated from [UtBotRiderModel.kt:9] + */ +data class StartPublishArgs ( + val fileName: String, + val arguments: String, + val workingDirectory: String +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = StartPublishArgs::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): StartPublishArgs { + val fileName = buffer.readString() + val arguments = buffer.readString() + val workingDirectory = buffer.readString() + return StartPublishArgs(fileName, arguments, workingDirectory) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: StartPublishArgs) { + buffer.writeString(value.fileName) + buffer.writeString(value.arguments) + buffer.writeString(value.workingDirectory) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as StartPublishArgs + + if (fileName != other.fileName) return false + if (arguments != other.arguments) return false + if (workingDirectory != other.workingDirectory) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + fileName.hashCode() + __r = __r*31 + arguments.hashCode() + __r = __r*31 + workingDirectory.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("StartPublishArgs (") + printer.indent { + print("fileName = "); fileName.print(printer); println() + print("arguments = "); arguments.print(printer); println() + print("workingDirectory = "); workingDirectory.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} diff --git a/utbot-rider/src/main/resources/META-INF/plugin.xml b/utbot-rider/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000000..1b60aceb7d --- /dev/null +++ b/utbot-rider/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,43 @@ + + + + org.utbot.rider.plugin.id + UnitTestBot.NET + utbot.org + com.intellij.modules.rider + + + +
  • Generating ready-to-use test cases with method bodies and inputs
  • +
  • Maximizing branch coverage in regression suite while keeping the number of tests at minimum
  • +
  • Capable to find deeply hidden code defects and express them as tests
  • +
  • Applicable to LINQ syntax and complex generics
  • +
  • Supporting .NET Framework, .NET Core, .NET 5, and .NET 6
  • +
  • Powered by V# — the custom symbolic execution engine
  • + +
    + Try UnitTestBot .NET online demo to see how it generates tests for your code in real time. + ]]> + + + + + org.utbot.rider.UtBotConsoleViewComponent + + + + + +
  • Test generation timeout option to control test generation duration
  • +
  • Balloon notifications informing about the test generation process state
  • +
  • Console output for publish and test generation processes
  • +
  • .NET 7 projects support
  • +
  • Bugfixes
  • + + ]]> +
    + diff --git a/utbot-rider/src/main/resources/META-INF/pluginIcon.svg b/utbot-rider/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000000..d24574d6dd --- /dev/null +++ b/utbot-rider/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/utbot-sample/build.gradle b/utbot-sample/build.gradle index ff6f14a543..20c963ec5c 100644 --- a/utbot-sample/build.gradle +++ b/utbot-sample/build.gradle @@ -6,16 +6,10 @@ dependencies { implementation group: 'org.jetbrains', name: 'annotations', version: '16.0.2' implementation group: 'com.github.stephenc.findbugs', name: 'findbugs-annotations', version: '1.3.9-1' implementation 'org.projectlombok:lombok:1.18.20' + testImplementation group: 'org.mockito', name:'mockito-core', version: mockitoVersion annotationProcessor 'org.projectlombok:lombok:1.18.20' implementation(project(":utbot-api")) implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' implementation group: 'javax.validation', name: 'validation-api', version: '2.0.0.Final' - implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' + implementation group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion } - -test { - minHeapSize = "128m" - maxHeapSize = "3072m" - - jvmArgs '-XX:MaxHeapSize=3072m' -} \ No newline at end of file diff --git a/utbot-sample/src/main/java/org/utbot/examples/arrays/CopyOfExample.java b/utbot-sample/src/main/java/org/utbot/examples/arrays/CopyOfExample.java new file mode 100644 index 0000000000..cf1316e294 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/arrays/CopyOfExample.java @@ -0,0 +1,45 @@ +package org.utbot.examples.arrays; + +import org.utbot.api.mock.UtMock; + +import java.util.Arrays; + +public class CopyOfExample { + @SuppressWarnings("IfStatementWithIdenticalBranches") + Integer[] copyOfExample(Integer[] values, int newLength) { + UtMock.assume(values != null); + + if (values.length == 0) { + if (newLength <= 0) { + return Arrays.copyOf(values, newLength); + } else { + return Arrays.copyOf(values, newLength); + } + } else { + if (newLength <= 0) { + return Arrays.copyOf(values, newLength); + } else { + return Arrays.copyOf(values, newLength); + } + } + } + + @SuppressWarnings("IfStatementWithIdenticalBranches") + Integer[] copyOfRangeExample(Integer[] values, int from, int to) { + UtMock.assume(values != null); + + if (from < 0) { + return Arrays.copyOfRange(values, from, to); + } + + if (from > to) { + return Arrays.copyOfRange(values, from, to); + } + + if (from > values.length) { + return Arrays.copyOfRange(values, from, to); + } + + return Arrays.copyOfRange(values, from, to); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/codegen/FileWithTopLevelFunctionsReflectHelper.java b/utbot-sample/src/main/java/org/utbot/examples/codegen/FileWithTopLevelFunctionsReflectHelper.java new file mode 100644 index 0000000000..d5803f72bc --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/codegen/FileWithTopLevelFunctionsReflectHelper.java @@ -0,0 +1,6 @@ +package org.utbot.examples.codegen; + +// We can't access FileWithTopLevelFunctionsKt::class from Kotlin, so we use this class to get reflection from Java +public class FileWithTopLevelFunctionsReflectHelper { + static Class clazz = FileWithTopLevelFunctionsKt.class; +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/codegen/modifiers/ClassWithPrivateMutableFieldOfPrivateType.java b/utbot-sample/src/main/java/org/utbot/examples/codegen/modifiers/ClassWithPrivateMutableFieldOfPrivateType.java new file mode 100644 index 0000000000..938f6f863d --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/codegen/modifiers/ClassWithPrivateMutableFieldOfPrivateType.java @@ -0,0 +1,16 @@ +package org.utbot.examples.codegen.modifiers; + +public class ClassWithPrivateMutableFieldOfPrivateType { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private PrivateClass privateMutableField = null; + + public int changePrivateMutableFieldWithPrivateType() { + privateMutableField = new PrivateClass(); + + return privateMutableField.x; + } + + private static class PrivateClass { + int x = 0; + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/collections/ListIterators.java b/utbot-sample/src/main/java/org/utbot/examples/collections/ListIterators.java index ce55011cea..8bdd6219ed 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/collections/ListIterators.java +++ b/utbot-sample/src/main/java/org/utbot/examples/collections/ListIterators.java @@ -1,11 +1,34 @@ package org.utbot.examples.collections; +import org.utbot.api.mock.UtMock; + import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ListIterator; public class ListIterators { + @SuppressWarnings({"IfStatementWithIdenticalBranches", "RedundantOperationOnEmptyContainer"}) + Iterator returnIterator(List list) { + UtMock.assume(list != null); + + if (list.isEmpty()) { + return list.iterator(); + } else { + return list.iterator(); + } + } + + @SuppressWarnings("IfStatementWithIdenticalBranches") + ListIterator returnListIterator(List list) { + UtMock.assume(list != null); + + if (list.isEmpty()) { + return list.listIterator(); + } else { + return list.listIterator(); + } + } List iterate(List list) { Iterator iterator = list.iterator(); 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 3c66dd0c69..99d5c1c3b3 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 @@ -3,6 +3,7 @@ import org.utbot.api.mock.UtMock; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; @@ -159,4 +160,15 @@ List addAllByIndex(List list, int i) { } return list; } + + @SuppressWarnings("IfStatementWithIdenticalBranches") + List asListExample(String[] values) { + UtMock.assume(values != null); + + if (values.length == 0) { + return Arrays.asList(values); + } else { + return Arrays.asList(values); + } + } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/collections/Maps.java b/utbot-sample/src/main/java/org/utbot/examples/collections/Maps.java index 5bd4f6882d..0be06782f5 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/collections/Maps.java +++ b/utbot-sample/src/main/java/org/utbot/examples/collections/Maps.java @@ -1,5 +1,7 @@ package org.utbot.examples.collections; +import org.utbot.api.mock.UtMock; + import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; @@ -25,6 +27,36 @@ String mapToString(long startTime, int pageSize, int pageNum) { return params.toString(); } + @SuppressWarnings("OverwrittenKey") + Integer mapPutAndGet() { + Map values = new HashMap<>(); + + values.put(1L, 2); + values.put(1L, 3); + + return values.get(1L); + } + + @SuppressWarnings("OverwrittenKey") + Integer putInMapFromParameters(Map values) { + values.put(1L, 2); + values.put(1L, 3); + + return values.get(1L); + } + + @SuppressWarnings("OverwrittenKey") + Integer containsKeyAndPuts(Map values) { + UtMock.assume(!values.containsKey(1L)); + + values.put(1L, 2); + values.put(1L, 3); + + UtMock.assume(values.get(1L).equals(3)); + + return values.get(1L); + } + Map countChars(String s) { Map map = new LinkedHashMap<>(); for (int i = 0; i < s.length(); i++) { @@ -261,4 +293,30 @@ public List mapOperator(Map map) { return new ArrayList<>(map.values()); } } + + public Map createMapWithString() { + Map map = new HashMap<>(); + map.put("tuesday", 354); + map.remove("tuesday"); + + return map; + } + + public Map createMapWithEnum() { + Map map = new HashMap<>(); + map.put(WorkDays.Monday, 112); + map.put(WorkDays.Tuesday, 354); + map.put(WorkDays.Friday, 567); + map.remove(WorkDays.Tuesday); + + return map; + } + + public enum WorkDays { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday + } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/collections/SetIterators.java b/utbot-sample/src/main/java/org/utbot/examples/collections/SetIterators.java index 91ed859aae..0f332e0a85 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/collections/SetIterators.java +++ b/utbot-sample/src/main/java/org/utbot/examples/collections/SetIterators.java @@ -1,9 +1,22 @@ package org.utbot.examples.collections; +import org.utbot.api.mock.UtMock; + import java.util.Iterator; import java.util.Set; public class SetIterators { + @SuppressWarnings({"IfStatementWithIdenticalBranches", "RedundantOperationOnEmptyContainer"}) + Iterator returnIterator(Set set) { + UtMock.assume(set != null); + + if (set.isEmpty()) { + return set.iterator(); + } else { + return set.iterator(); + } + } + int iteratorHasNext(Set s) { Iterator iterator = s.iterator(); if (!iterator.hasNext()) { diff --git a/utbot-sample/src/main/java/org/utbot/examples/controlflow/Conditions.java b/utbot-sample/src/main/java/org/utbot/examples/controlflow/Conditions.java index c5ba5c8aab..d13d9d6282 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/controlflow/Conditions.java +++ b/utbot-sample/src/main/java/org/utbot/examples/controlflow/Conditions.java @@ -1,6 +1,21 @@ package org.utbot.examples.controlflow; public class Conditions { + + /** + * This Doc is here in order to check whether the summaries and display names are rendered correctly. + * Had not in hours of peace, + * It learned to lightly look on life. + * + * @param a some long value + * @param b some int value + * @return the result you won't expect. + */ + public int returnCastFromTernaryOperator(long a, int b) { + a = a % b; + return (int) (a < 0 ? a + b : a); + } + public int simpleCondition(boolean condition) { if (condition) { return 1; @@ -16,4 +31,10 @@ public void emptyBranches(boolean condition) { // do nothing } } + + public int elseIf(int id) throws RuntimeException { + if (id > 0) return 0; + else if (id == 0) throw new RuntimeException("Exception message"); + else return 1; + } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/controlflow/Switch.java b/utbot-sample/src/main/java/org/utbot/examples/controlflow/Switch.java index d4948828a4..63f9f0b8f5 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/controlflow/Switch.java +++ b/utbot-sample/src/main/java/org/utbot/examples/controlflow/Switch.java @@ -46,6 +46,32 @@ public int enumSwitch(RoundingMode m) { return -1; } + public int charToIntSwitch(char c) { + switch (c) { + case 'I': return 1; + case 'V': return 5; + case 'X': return 10; + case 'L': return 50; + case 'C': return 100; + case 'D': return 500; + case 'M': return 1000; + default: throw new IllegalArgumentException("Unrecognized symbol: " + c); + } + } + + public int throwExceptionInSwitchArgument() { + switch (getChar()) { + case 'I': + return 1; + default: + return 100; + } + } + + private char getChar() throws RuntimeException { + throw new RuntimeException("Exception message"); + } + //TODO: String switch // public int stringSwitch(String s) { // switch (s) { @@ -59,4 +85,3 @@ public int enumSwitch(RoundingMode m) { // } // } } - 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 14a0ded3fc..61cc9c4a83 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 @@ -98,9 +98,28 @@ public boolean changingStaticWithEnumInit() { return true; } + public int virtualFunction(StatusEnum parameter) { + int value = parameter.virtualFunction(); + if (value > 0) { + return value; + } + + return Math.abs(value); + } + enum StatusEnum { - READY(0, 10, "200"), - ERROR(-1, -10, null); + READY(0, 10, "200") { + @Override + public int virtualFunction() { + return 0; + } + }, + ERROR(-1, -10, null) { + @Override + int virtualFunction() { + return 1; + } + }; int mutableInt; final int code; @@ -137,6 +156,8 @@ static StatusEnum fromIsReady(boolean isReady) { int publicGetCode() { return this == READY ? 10 : -10; } + + abstract int virtualFunction(); } enum ManyConstantsEnum { @@ -228,7 +249,7 @@ boolean affectSystemStaticAndInitEnumFromItAndGetItFromEnumFun() { enum OuterStaticUsageEnum { A; - int y; + final int y; OuterStaticUsageEnum() { y = staticInt; @@ -237,5 +258,11 @@ enum OuterStaticUsageEnum { int getOuterStatic() { return staticInt; } + + + @Override + public String toString() { + return String.format("%s(y = %d)", name(), y); + } } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/exceptions/ExceptionExamples.java b/utbot-sample/src/main/java/org/utbot/examples/exceptions/ExceptionExamples.java index cf259389fc..efc9e9eca1 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/exceptions/ExceptionExamples.java +++ b/utbot-sample/src/main/java/org/utbot/examples/exceptions/ExceptionExamples.java @@ -86,10 +86,29 @@ public int catchDeepNestedThrow(int i) { } } + public int catchExceptionAfterOtherPossibleException(int i) { + int x = 15; + x /= i + 1; + + try { + x /= i; + } catch (RuntimeException e) { + return 2; + } + return 1; + } + public IllegalArgumentException createException() { return new IllegalArgumentException("Here we are: " + Math.sqrt(10)); } + public int hangForSeconds(int seconds) throws InterruptedException { + for (int i = 0; i < seconds; i++) { + Thread.sleep(1000); + } + return seconds; + } + public int dontCatchDeepNestedThrow(int i) { return callNestedWithThrow(i); } @@ -104,4 +123,8 @@ private int nestedWithThrow(int i) { } return i; } -} \ No newline at end of file + + public int throwExceptionInMethodUnderTest() throws RuntimeException { + throw new RuntimeException("Exception message"); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/lambda/ThrowingWithLambdaExample.java b/utbot-sample/src/main/java/org/utbot/examples/lambda/ThrowingWithLambdaExample.java new file mode 100644 index 0000000000..acbc77ceb3 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/lambda/ThrowingWithLambdaExample.java @@ -0,0 +1,27 @@ +package org.utbot.examples.lambda; + +import org.utbot.api.mock.UtMock; + +public class ThrowingWithLambdaExample { + // This example mostly checks that we can construct non-static lambda even if it's init section was not analyzed + // (e.g., an exception was thrown before it). + boolean anyExample(int[] values, IntPredicate predicate) { + UtMock.assume(predicate != null); + + for (int value : values) { + if (predicate.test(value)) { + return true; + } + } + + return false; + } + + // To make this lambda non-static, we need to make it use `this` instance. + @SuppressWarnings({"unused", "ConstantConditions"}) + IntPredicate nonStaticIntPredicate = x -> this != null && x == 42; + + interface IntPredicate { + boolean test(int value); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/math/OverflowExamples.java b/utbot-sample/src/main/java/org/utbot/examples/math/OverflowExamples.java index b1cc7fd4f7..271139ab2e 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/math/OverflowExamples.java +++ b/utbot-sample/src/main/java/org/utbot/examples/math/OverflowExamples.java @@ -4,6 +4,9 @@ public class OverflowExamples { public byte byteAddOverflow(byte x, byte y) { return (byte) (x + y); } + public byte byteWithIntOverflow(byte x, int y) { + return (byte) (x + y); + } public byte byteMulOverflow(byte x, byte y) { return (byte) (x * y); } diff --git a/utbot-sample/src/main/java/org/utbot/examples/mixed/SerializableExample.java b/utbot-sample/src/main/java/org/utbot/examples/mixed/SerializableExample.java new file mode 100644 index 0000000000..b5456054be --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/mixed/SerializableExample.java @@ -0,0 +1,13 @@ +package org.utbot.examples.mixed; + +import java.io.File; + +public class SerializableExample { + public void example() { + join("string", File.separator, System.currentTimeMillis()); + } + + public static String join(T... elements) { + return null; + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/mock/CommonMocksExample.java b/utbot-sample/src/main/java/org/utbot/examples/mock/CommonMocksExample.java index 873ea187d2..d65209b356 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/mock/CommonMocksExample.java +++ b/utbot-sample/src/main/java/org/utbot/examples/mock/CommonMocksExample.java @@ -1,5 +1,7 @@ package org.utbot.examples.mock; +import org.utbot.api.mock.UtMock; +import org.utbot.examples.mock.others.Random; import org.utbot.examples.mock.service.impl.ExampleClass; import org.utbot.examples.objects.ObjectWithFinalStatic; import org.utbot.examples.objects.RecursiveTypeClass; @@ -35,4 +37,11 @@ public int clinitMockExample() { return -ObjectWithFinalStatic.keyValue; } } + + public int mocksForNullOfDifferentTypes(Integer intValue, Random random) { + UtMock.assume(intValue == null); + UtMock.assume(random == null); + + return 0; + } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/mock/MockStaticFieldExample.java b/utbot-sample/src/main/java/org/utbot/examples/mock/MockStaticFieldExample.java index 916f6a2759..4de83e353f 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/mock/MockStaticFieldExample.java +++ b/utbot-sample/src/main/java/org/utbot/examples/mock/MockStaticFieldExample.java @@ -1,5 +1,6 @@ package org.utbot.examples.mock; +import org.utbot.examples.mock.others.ClassWithStaticField; import org.utbot.examples.mock.others.Generator; public class MockStaticFieldExample { @@ -7,6 +8,9 @@ public class MockStaticFieldExample { private static Generator privateGenerator; public static Generator publicGenerator; + public static final ClassWithStaticField staticFinalField = new ClassWithStaticField(); + public static ClassWithStaticField staticField = new ClassWithStaticField(); + public int calculate(int threshold) { int a = privateGenerator.generateInt(); int b = publicGenerator.generateInt(); @@ -15,4 +19,20 @@ public int calculate(int threshold) { } return a + b + 1; } + + // This test is associated with https://github.com/UnitTestBot/UTBotJava/issues/1533 + public int checkMocksInLeftAndRightAssignPartFinalField() { + staticFinalField.intField = 5; + staticFinalField.anotherIntField = staticFinalField.foo(); + + return staticFinalField.anotherIntField; + } + + // This test is associated with https://github.com/UnitTestBot/UTBotJava/issues/1533 + public int checkMocksInLeftAndRightAssignPart() { + staticField.intField = 5; + staticField.anotherIntField = staticField.foo(); + + return staticField.anotherIntField; + } } \ No newline at end of file diff --git a/utbot-sample/src/main/java/org/utbot/examples/mock/MockStaticMethodExample.java b/utbot-sample/src/main/java/org/utbot/examples/mock/MockStaticMethodExample.java index 9cb42fef6c..63e5902e87 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/mock/MockStaticMethodExample.java +++ b/utbot-sample/src/main/java/org/utbot/examples/mock/MockStaticMethodExample.java @@ -11,4 +11,8 @@ public int useStaticMethod() { return 0; } + + public void mockStaticMethodFromAlwaysMockClass() { + System.out.println("example"); + } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/mock/fields/ClassUsingClassWithRandomField.java b/utbot-sample/src/main/java/org/utbot/examples/mock/fields/ClassUsingClassWithRandomField.java new file mode 100644 index 0000000000..7e611f3b50 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/mock/fields/ClassUsingClassWithRandomField.java @@ -0,0 +1,9 @@ +package org.utbot.examples.mock.fields; + +public class ClassUsingClassWithRandomField { + public int useClassWithRandomField() { + ClassWithRandomField classWithRandomField = new ClassWithRandomField(); + + return classWithRandomField.nextInt(); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/mock/fields/ClassWithRandomField.java b/utbot-sample/src/main/java/org/utbot/examples/mock/fields/ClassWithRandomField.java new file mode 100644 index 0000000000..cfb9ae137d --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/mock/fields/ClassWithRandomField.java @@ -0,0 +1,11 @@ +package org.utbot.examples.mock.fields; + +import java.util.Random; + +public class ClassWithRandomField { + public Random random = new Random(); + + public int nextInt() { + return random.nextInt(); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/mock/others/ClassWithStaticField.java b/utbot-sample/src/main/java/org/utbot/examples/mock/others/ClassWithStaticField.java new file mode 100644 index 0000000000..fb6d8bbb06 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/mock/others/ClassWithStaticField.java @@ -0,0 +1,10 @@ +package org.utbot.examples.mock.others; + +public class ClassWithStaticField { + public int intField; + public int anotherIntField; + + public int foo() { + return 5; + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/nested/DeepNested.java b/utbot-sample/src/main/java/org/utbot/examples/nested/DeepNested.java new file mode 100644 index 0000000000..0c9ba5e61d --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/nested/DeepNested.java @@ -0,0 +1,14 @@ +package org.utbot.examples.nested; + +public class DeepNested { + public class Nested1 { + public class Nested2 { + public int f(int i) { + if (i > 0) { + return 10; + } + return 0; + } + } + } +} \ No newline at end of file diff --git a/utbot-sample/src/main/java/org/utbot/examples/objects/AbstractAnonymousClass.java b/utbot-sample/src/main/java/org/utbot/examples/objects/AbstractAnonymousClass.java index 3a5777f724..d09da1136b 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/objects/AbstractAnonymousClass.java +++ b/utbot-sample/src/main/java/org/utbot/examples/objects/AbstractAnonymousClass.java @@ -4,6 +4,22 @@ abstract class AbstractAnonymousClass { abstract int constValue(); abstract int add(int x); + public int methodWithoutOverrides(int x, int y) { + return y + addFortyTwo(x); + } + + public int methodWithOverride(int x, int y) { + return y + addNumber(x); + } + + public int addFortyTwo(int x) { + return x + 42; + } + + public int addNumber(int x) { + return x + 27; + } + public static AbstractAnonymousClass getInstance(int x) { if (x % 2 == 0) { return new AnonymousClassAlternative(); @@ -19,6 +35,11 @@ int constValue() { int add(int x) { return x + 15; } + + @Override + public int methodWithOverride(int x, int y) { + return x + 37; + } }; } } @@ -33,4 +54,9 @@ int constValue() { int add(int x) { return x + 1; } + + @Override + public int methodWithOverride(int x, int y) { + return x + 17; + } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/objects/ClassForTestClinitSections.java b/utbot-sample/src/main/java/org/utbot/examples/objects/ClassForTestClinitSections.java new file mode 100644 index 0000000000..f037e88283 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/objects/ClassForTestClinitSections.java @@ -0,0 +1,13 @@ +package org.utbot.examples.objects; + +public class ClassForTestClinitSections { + private static int x = 5; + + public int resultDependingOnStaticSection() { + if (x == 5) { + return -1; + } + + return 1; + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/objects/LocalClassExample.java b/utbot-sample/src/main/java/org/utbot/examples/objects/LocalClassExample.java new file mode 100644 index 0000000000..6c6ddcc560 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/objects/LocalClassExample.java @@ -0,0 +1,17 @@ +package org.utbot.examples.objects; + +public class LocalClassExample { + int localClassFieldExample(int y) { + class LocalClass { + final int x; + + public LocalClass(int x) { + this.x = x; + } + } + + LocalClass localClass = new LocalClass(42); + + return localClass.x + y; + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/reflection/NewInstanceExample.java b/utbot-sample/src/main/java/org/utbot/examples/reflection/NewInstanceExample.java new file mode 100644 index 0000000000..21f4c1f6ed --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/reflection/NewInstanceExample.java @@ -0,0 +1,16 @@ +package org.utbot.examples.reflection; + +public class NewInstanceExample { + @SuppressWarnings("deprecation") + int createWithReflectionExample() throws ClassNotFoundException, InstantiationException, IllegalAccessException { + Class cls = Class.forName("org.utbot.examples.reflection.ClassWithDefaultConstructor"); + ClassWithDefaultConstructor classWithDefaultConstructor = (ClassWithDefaultConstructor) cls.newInstance(); + + return classWithDefaultConstructor.x; + } +} + +class ClassWithDefaultConstructor { + + int x; +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/stdlib/JavaIOFileInputStreamCheck.java b/utbot-sample/src/main/java/org/utbot/examples/stdlib/JavaIOFileInputStreamCheck.java new file mode 100644 index 0000000000..968295ef2a --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/stdlib/JavaIOFileInputStreamCheck.java @@ -0,0 +1,12 @@ +package org.utbot.examples.stdlib; + +import java.io.FileInputStream; +import java.io.IOException; + +public class JavaIOFileInputStreamCheck { + public int read(String s) throws IOException { + java.io.FileInputStream fis = new java.io.FileInputStream(s); + byte[] b = new byte[1000]; + return fis.read(b); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/stdlib/StaticsPathDiversion.java b/utbot-sample/src/main/java/org/utbot/examples/stdlib/StaticsPathDiversion.java new file mode 100644 index 0000000000..a83791d1d1 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/stdlib/StaticsPathDiversion.java @@ -0,0 +1,21 @@ +package org.utbot.examples.stdlib; + +import org.utbot.api.mock.UtMock; + +import java.io.File; + +public class StaticsPathDiversion { + @SuppressWarnings({"IfStatementWithIdenticalBranches"}) + // In this test we check that the symbolic engine does not change the static field `File.separator` + public String separatorEquality(String s) { + // Ignore this case to make sure we will have not more than 2 executions even without minimization + UtMock.assume(s != null); + + // We use if-else here instead of a simple return to get executions for both return values + if (File.separator.equals(s)) { + return File.separator; + } else { + return File.separator; + } + } +} 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 index 8760f36f55..b636394152 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/stream/BaseStreamExample.java +++ b/utbot-sample/src/main/java/org/utbot/examples/stream/BaseStreamExample.java @@ -21,16 +21,6 @@ @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; diff --git a/utbot-sample/src/main/java/org/utbot/examples/stream/DoubleStreamExample.java b/utbot-sample/src/main/java/org/utbot/examples/stream/DoubleStreamExample.java index a86d6f953d..dc8801f498 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/stream/DoubleStreamExample.java +++ b/utbot-sample/src/main/java/org/utbot/examples/stream/DoubleStreamExample.java @@ -18,19 +18,6 @@ @SuppressWarnings("IfStatementWithIdenticalBranches") public class DoubleStreamExample { - DoubleStream returningStreamExample(List list) { - UtMock.assume(list != null); - - final ToDoubleFunction shortToDoubleFunction = value -> value == null ? 0 : value.doubleValue(); - final DoubleStream doubles = list.stream().mapToDouble(shortToDoubleFunction); - - if (list.isEmpty()) { - return doubles; - } else { - return doubles; - } - } - DoubleStream returningStreamAsParameterExample(DoubleStream s) { UtMock.assume(s != null); diff --git a/utbot-sample/src/main/java/org/utbot/examples/stream/IntStreamExample.java b/utbot-sample/src/main/java/org/utbot/examples/stream/IntStreamExample.java index 58e57e8843..2e3e33f122 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/stream/IntStreamExample.java +++ b/utbot-sample/src/main/java/org/utbot/examples/stream/IntStreamExample.java @@ -21,19 +21,6 @@ @SuppressWarnings("IfStatementWithIdenticalBranches") public class IntStreamExample { - IntStream returningStreamExample(List list) { - UtMock.assume(list != null); - - final ToIntFunction shortToIntFunction = value -> value == null ? 0 : value.intValue(); - final IntStream ints = list.stream().mapToInt(shortToIntFunction); - - if (list.isEmpty()) { - return ints; - } else { - return ints; - } - } - IntStream returningStreamAsParameterExample(IntStream s) { UtMock.assume(s != null); diff --git a/utbot-sample/src/main/java/org/utbot/examples/stream/LongStreamExample.java b/utbot-sample/src/main/java/org/utbot/examples/stream/LongStreamExample.java index 1a071fa69c..c821714069 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/stream/LongStreamExample.java +++ b/utbot-sample/src/main/java/org/utbot/examples/stream/LongStreamExample.java @@ -20,19 +20,6 @@ @SuppressWarnings("IfStatementWithIdenticalBranches") public class LongStreamExample { - LongStream returningStreamExample(List list) { - UtMock.assume(list != null); - - final ToLongFunction shortToLongFunction = value -> value == null ? 0 : value.longValue(); - final LongStream longs = list.stream().mapToLong(shortToLongFunction); - - if (list.isEmpty()) { - return longs; - } else { - return longs; - } - } - LongStream returningStreamAsParameterExample(LongStream s) { UtMock.assume(s != null); diff --git a/utbot-sample/src/main/java/org/utbot/examples/stream/StreamsAsMethodResultExample.java b/utbot-sample/src/main/java/org/utbot/examples/stream/StreamsAsMethodResultExample.java new file mode 100644 index 0000000000..566a57f091 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/stream/StreamsAsMethodResultExample.java @@ -0,0 +1,83 @@ +package org.utbot.examples.stream; + +import org.utbot.api.mock.*; + +import java.util.List; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +@SuppressWarnings({"IfStatementWithIdenticalBranches", "RedundantOperationOnEmptyContainer"}) +public class StreamsAsMethodResultExample { + Stream returningStreamExample(List list) { + UtMock.assume(list != null); + + if (list.isEmpty()) { + return list.stream(); + } + + return list.stream(); + } + + IntStream returningIntStreamExample(List list) { + UtMock.assume(list != null); + + final int size = list.size(); + + if (size == 0) { + return list.stream().mapToInt(value -> value); + } + + UtMock.assume(size == 1); + + final Integer integer = list.get(0); + + if (integer == null) { + return list.stream().mapToInt(value -> value); + } + + return list.stream().mapToInt(value -> value); + } + + LongStream returningLongStreamExample(List list) { + UtMock.assume(list != null); + + final int size = list.size(); + + if (size == 0) { + return list.stream().mapToLong(value -> value); + } + + UtMock.assume(size == 1); + + final Integer integer = list.get(0); + + if (integer == null) { + return list.stream().mapToLong(value -> value); + } + + return list.stream().mapToLong(value -> value); + } + + DoubleStream returningDoubleStreamExample(List list) { + UtMock.assume(list != null); + + final int size = list.size(); + + if (size == 0) { + return list.stream().mapToDouble(value -> value); + } + + UtMock.assume(size == 1); + + final Integer integer = list.get(0); + + if (integer == null) { + return list.stream().mapToDouble(value -> value); + } + + return list.stream().mapToDouble(value -> value); + } +} + diff --git a/utbot-sample/src/main/java/org/utbot/examples/strings/GenericExamples.java b/utbot-sample/src/main/java/org/utbot/examples/strings/GenericExamples.java new file mode 100644 index 0000000000..55f0be4196 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/strings/GenericExamples.java @@ -0,0 +1,11 @@ +package org.utbot.examples.strings; + +public class GenericExamples { + public boolean containsOk(T obj) { + return obj.toString().contains("ok"); + } + + public boolean containsOkExample() { + return new GenericExamples().containsOk("Elders have spoken"); + } +} 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 73c624ae92..ffdec57810 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,8 @@ package org.utbot.examples.strings; +import org.jetbrains.annotations.NotNull; +import org.utbot.api.mock.UtMock; + import java.util.Arrays; import static java.lang.Boolean.valueOf; @@ -44,6 +47,16 @@ public String intToString(int a, int b) { } } + public String[] intToStringWithConstants() { + return new String[]{ + Integer.toString(Integer.MIN_VALUE), + Integer.toString(Integer.MIN_VALUE + 100), + Integer.toString(0), + Integer.toString(Integer.MAX_VALUE - 100), + Integer.toString(Integer.MAX_VALUE) + }; + } + public String longToString(long a, long b) { if (a > b) { return Long.toString(a); @@ -52,6 +65,16 @@ public String longToString(long a, long b) { } } + public String[] longToStringWithConstants() { + return new String[]{ + Long.toString(Long.MIN_VALUE), + Long.toString(Long.MIN_VALUE + 100L), + Long.toString(0), + Long.toString(Long.MAX_VALUE - 100L), + Long.toString(Long.MAX_VALUE) + }; + } + public String startsWithLiteral(String str) { if (str.startsWith("1234567890")) { str = str.replace("3", "A"); @@ -74,6 +97,16 @@ public String byteToString(byte a, byte b) { } } + public String[] byteToStringWithConstants() { + return new String[]{ + Byte.toString(Byte.MIN_VALUE), + Byte.toString((byte) (Byte.MIN_VALUE + 100)), + Byte.toString((byte) 0), + Byte.toString((byte) (Byte.MAX_VALUE - 100)), + Byte.toString(Byte.MAX_VALUE) + }; + } + public String replace(String a, String b) { return a.replace("abc", b); } @@ -94,6 +127,16 @@ public String shortToString(short a, short b) { } } + public String[] shortToStringWithConstants() { + return new String[]{ + Short.toString(Short.MIN_VALUE), + Short.toString((short) (Short.MIN_VALUE + 100)), + Short.toString((short) 0), + Short.toString((short) (Short.MAX_VALUE - 100)), + Short.toString(Short.MAX_VALUE) + }; + } + public String booleanToString(boolean a, boolean b) { if (a ^ b) { return Boolean.toString(a ^ b); @@ -166,6 +209,11 @@ public String useStringBuffer(String fst, String snd) { return buffer.toString(); } + // This test checks StringBuilder can be correctly constructed + public void stringBuilderAsParameterExample(StringBuilder sb) { + UtMock.assume(sb != null); + } + public String nullableStringBuffer(StringBuffer buffer, int i) { if (i >= 0) { buffer.append("Positive"); @@ -175,6 +223,16 @@ public String nullableStringBuffer(StringBuffer buffer, int i) { return buffer.toString(); } + @SuppressWarnings("RedundantIfStatement") + public boolean isStringBuilderEmpty(@NotNull StringBuilder stringBuilder) { + String content = stringBuilder.toString(); + if (content.length() == 0) { + return true; + } + + return false; + } + public boolean isValidUuid(String uuid) { return isNotBlank(uuid) && uuid .matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); @@ -184,6 +242,50 @@ public boolean isValidUuidShortVersion(String uuid) { return uuid != null && uuid.matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); } + @SuppressWarnings("IfStatementWithIdenticalBranches") + public int splitExample(String s) { + UtMock.assume(s != null); + UtMock.assume(s.length() == 3); + + final char firstChar = s.charAt(0); + final char secondChar = s.charAt(1); + final char thirdChar = s.charAt(2); + + final boolean isFirstWhitespace = Character.isWhitespace(firstChar); + final boolean isSecondWhitespace = Character.isWhitespace(secondChar); + final boolean isThirdWhitespace = Character.isWhitespace(thirdChar); + + if (isFirstWhitespace) { + if (isSecondWhitespace) { + if (isThirdWhitespace) { + return s.split("\\s+").length; + } else { + return s.split("\\s+").length; + } + } else { + if (isThirdWhitespace) { + return s.split("\\s+").length; + } else { + return s.split("\\s+").length; + } + } + } else { + if (isSecondWhitespace) { + if (isThirdWhitespace) { + return s.split("\\s+").length; + } else { + return s.split("\\s+").length; + } + } else { + if (isThirdWhitespace) { + return s.split("\\s+").length; + } else { + return s.split("\\s+").length; + } + } + } + } + public boolean isNotBlank(CharSequence cs) { return !isBlank(cs); } diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintBranching.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintBranching.java new file mode 100644 index 0000000000..4ab1923ce1 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintBranching.java @@ -0,0 +1,33 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintBranchingConfig.yaml + */ +public class TaintBranching { + + public String source() { + return "t"; + } + + public void sink(String s) { + // + } + + public void bad(boolean cond) { + String s = source(); + if (cond) { + sink(s); + } else { + // + } + } + + public void good(boolean cond) { + String s = source(); + if (cond) { + sink("n"); + } else { + // + } + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintCleanerConditions.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintCleanerConditions.java new file mode 100644 index 0000000000..0312f9acb6 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintCleanerConditions.java @@ -0,0 +1,63 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintCleanerConditionsConfig.yaml + */ +public class TaintCleanerConditions { + + public String source() { + return "t"; + } + + public String sourceEmpty() { + return ""; + } + + public void sink(String s) { + // + } + + public void cleanerArgCondition(String s) { + // + } + + public boolean cleanerReturnCondition(String s) { + return s.isEmpty(); + } + + public void badArg() { + String s = source(); + cleanerArgCondition(s); + sink(s); + } + + public void badReturn() { + String s = source(); + boolean isClean = cleanerReturnCondition(s); + sink(s); + } + + public void badThis() { + String s = source(); + boolean res = s.isEmpty(); + sink(s); + } + + public void goodArg() { + String s = sourceEmpty(); + cleanerArgCondition(s); + sink(s); + } + + public void goodReturn() { + String s = sourceEmpty(); + boolean isClean = cleanerReturnCondition(s); + sink(s); + } + + public void goodThis() { + String s = sourceEmpty(); + boolean res = s.isEmpty(); + sink(s); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintCleanerSimple.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintCleanerSimple.java new file mode 100644 index 0000000000..b9f727ea69 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintCleanerSimple.java @@ -0,0 +1,35 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintCleanerSimpleConfig.yaml + */ +public class TaintCleanerSimple { + + public String source() { + return "t"; + } + + public void cleaner(String s) { + // + } + + public void notCleaner(String s) { + // + } + + public void sink(String s) { + // + } + + public void bad() { + String s = source(); + notCleaner(s); + sink(s); + } + + public void good() { + String s = source(); + cleaner(s); + sink(s); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintLongPath.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintLongPath.java new file mode 100644 index 0000000000..1262130534 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintLongPath.java @@ -0,0 +1,33 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintLongPathConfig.yaml + */ +public class TaintLongPath { + + public String source() { + return "t"; + } + + public void sink(String s) { + // + } + + public void bad() { + String s = source(); + bad2(s); + } + + public void bad2(String s) { + bad3(s); + } + + public void bad3(String s) { + sink(s); + } + + public void good() { + String s = source(); + sink("n"); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintOtherClass.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintOtherClass.java new file mode 100644 index 0000000000..a33076f67f --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintOtherClass.java @@ -0,0 +1,37 @@ +package org.utbot.examples.taint; + +public class TaintOtherClass { + + public void bad() { + Inner inner = new Inner(); + String s = inner.source(); + String sp = inner.pass(s); + inner.sink(sp); + } + + public void good() { + Inner inner = new Inner(); + String s = inner.source(); + inner.cleaner(s); + inner.sink(s); + } +} + +class Inner { + + public String source() { + return "t"; + } + + public String pass(String s) { + return s + "p"; + } + + public void cleaner(String s) { + // + } + + public void sink(String s) { + // + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintPassConditions.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintPassConditions.java new file mode 100644 index 0000000000..7f5b38bf44 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintPassConditions.java @@ -0,0 +1,71 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintPassConditionsConfig.yaml + */ +public class TaintPassConditions { + + public String source() { + return "t"; + } + + public String sourceEmpty() { + return ""; + } + + public void sink(String s) { + // + } + + public String passArgCondition(String s, boolean isPass) { + if (isPass) { + return s + "p"; + } else { + return ""; + } + } + + public String passReturnCondition(String s, boolean isPass) { + if (isPass) { + return s + "p"; + } else { + return ""; + } + } + + public void badArg() { + String s = source(); + String sp = passArgCondition(s, true); + sink(sp); + } + + public void badReturn() { + String s = source(); + String sp = passReturnCondition(s, true); + sink(sp); + } + + public void badThis() { + String s = source(); + String sp = s.concat("#"); + sink(sp); + } + + public void goodArg() { + String s = source(); + String sp = passArgCondition(s, false); + sink(sp); + } + + public void goodReturn() { + String s = source(); + String sp = passReturnCondition(s, false); + sink(sp); + } + + public void goodThis() { + String s = sourceEmpty(); + String sp = s.concat("#"); + sink(sp); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintPassSimple.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintPassSimple.java new file mode 100644 index 0000000000..c76bf76b06 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintPassSimple.java @@ -0,0 +1,42 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintPassSimpleConfig.yaml + */ +public class TaintPassSimple { + + public String source() { + return "t"; + } + + public String pass(String s) { + return s + "p"; + } + + public String notPass(String s) { + return s + "p"; + } + + public void sink(String s) { + // + } + + public void bad() { + String s = source(); + String sp = pass(s); + sink(sp); + } + + public void badDoublePass() { + String s = source(); + String sp = pass(s); + String spp = pass(sp); + sink(spp); + } + + public void good() { + String s = source(); + String sp = notPass(s); + sink(sp); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSeveralMarks.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSeveralMarks.java new file mode 100644 index 0000000000..52da4847ed --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSeveralMarks.java @@ -0,0 +1,131 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintSeveralMarksConfig.yaml + */ +public class TaintSeveralMarks { + + public String source1() { + return "1"; + } + + public String source2() { + return "2"; + } + + public String source12() { + return "12"; + } + + public String pass1(String s) { + return s + "1"; + } + + public String pass2(String s) { + return s + "2"; + } + + + public void cleaner1(String s) { + // + } + + public void cleaner2(String s) { + // + } + + public void sink1(String s) { + // + } + + public void sink2(String s) { + // + } + + public void sink13(String s) { + // + } + + public void sink123(String s) { + // + } + + public void sinkAll(String s) { + // + } + + public void bad1() { + String s = source1(); + String sp = pass1(s); + sink1(sp); + } + + public void bad2() { + String s = source2(); + String sp = pass2(s); + sink2(sp); + } + + public void bad13() { + String s = source1(); + sink13(s); + } + + public void bad123() { + String s12 = source12(); + sink123(s12); + } + + public void badSourceAll() { + String s = source12(); + sink1(s); + sink2(s); + sinkAll(s); + } + + public void badSinkAll() { + String s1 = source1(); + String s2 = source2(); + sinkAll(s1); + sinkAll(s2); + } + + public void badWrongCleaner() { + String s = source1(); + cleaner2(s); + sink1(s); + } + + public void good1() { + String s = source1(); + cleaner1(s); + sink1(s); + } + + public void good2() { + String s = source2(); + cleaner2(s); + sink2(s); + } + + public void good13() { + String s = source2(); + sink13(s); + } + + public void goodWrongSource() { + String s = source1(); + sink2(s); + } + + public void goodWrongSink() { + String s = source2(); + sink1(s); + } + + public void goodWrongPass() { + String s = source1(); + String sp = pass2(s); + sink1(sp); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSignature.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSignature.java new file mode 100644 index 0000000000..39f973fdbe --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSignature.java @@ -0,0 +1,83 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintSignatureConfig.yaml + */ +public class TaintSignature { + + // sources + + public String source() { + return "t"; + } + + public String source(boolean cond) { // fake + return "t"; + } + + public String source(int a, int b) { // fake + return "t"; + } + + // passes + + public String pass(String s) { + return s + "p"; + } + + public String pass(String s, int b) { // fake + return s + "p"; + } + + // cleaners + + public void cleaner(String s) { + // + } + + public void cleaner(String s, String t) { // fake + // + } + + // sinks + + public void sink(String s) { + // + } + + public void sink(String s, int a) { // fake + // + } + + public void badFakeCleaner() { + String s = source(); + String sp = pass(s); + cleaner(sp, s); // fake + sink(sp); + } + + public void goodCleaner() { + String s = source(); + String sp = pass(s); + cleaner(sp); + sink(sp); + } + + public void goodFakeSources() { + String s = source(true); + String t = source(1, 2); + sink(s); + sink(t); + } + + public void goodFakePass() { + String s = source(); + String sp = pass(s, 1); // fake + sink(sp); + } + + public void goodFakeSink() { + String s = source(); + sink(s, 1); // fake + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSimple.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSimple.java new file mode 100644 index 0000000000..e02c2ef10c --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSimple.java @@ -0,0 +1,25 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintSimpleConfig.yaml + */ +public class TaintSimple { + + public String source() { + return "t"; + } + + public void sink(String s) { + // + } + + public void bad() { + String s = source(); + sink(s); + } + + public void good() { + String s = source(); + sink("n"); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSinkConditions.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSinkConditions.java new file mode 100644 index 0000000000..786b73f14d --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSinkConditions.java @@ -0,0 +1,39 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintSinkConditionsConfig.yaml + */ +public class TaintSinkConditions { + + public String source() { + return "t"; + } + + public String sourceEmpty() { + return ""; + } + + public void sink(String s, boolean isSink) { + // + } + + public void badArg() { + String s = source(); + sink(s, true); + } + + public void badThis() { + String s = source(); + byte[] res = s.getBytes(); + } + + public void goodArg() { + String s = source(); + sink(s, false); + } + + public void goodThis() { + String s = sourceEmpty(); + byte[] res = s.getBytes(); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSourceConditions.java b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSourceConditions.java new file mode 100644 index 0000000000..0da33cbfa1 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/taint/TaintSourceConditions.java @@ -0,0 +1,59 @@ +package org.utbot.examples.taint; + +/** + * Config: ./utbot-sample/src/main/resources/taint/TaintSourceConditionsConfig.yaml + */ +public class TaintSourceConditions { + + public String sourceArgCondition(boolean isSource) { + if (isSource) { + return "t"; + } else { + return ""; + } + } + + public String sourceReturnCondition(boolean isSource) { + if (isSource) { + return "t"; + } else { + return ""; + } + } + + public void sink(String s) { + // + } + + public void badArg() { + String s = sourceArgCondition(true); + sink(s); + } + + public void badReturn() { + String s = sourceReturnCondition(true); + sink(s); + } + + public void badThis() { + String s = "t"; + String res = s.toLowerCase(); + sink(res); + } + + public void goodArg() { + String s = sourceArgCondition(false); + sink(s); + } + + public void goodReturn() { + String s = sourceArgCondition(false); + sink(s); + } + + public void goodThis() { + String s = ""; + String res = s.toLowerCase(); + sink(res); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/threads/CountDownLatchExamples.java b/utbot-sample/src/main/java/org/utbot/examples/threads/CountDownLatchExamples.java new file mode 100644 index 0000000000..b1ff95af84 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/threads/CountDownLatchExamples.java @@ -0,0 +1,38 @@ +package org.utbot.examples.threads; + +import org.utbot.api.mock.UtMock; + +import java.util.concurrent.CountDownLatch; + +public class CountDownLatchExamples { + long getAndDown(CountDownLatch countDownLatch) { + UtMock.assume(countDownLatch != null); + + final long count = countDownLatch.getCount(); + + if (count < 0) { + // Unreachable + return -1; + } + + countDownLatch.countDown(); + final long nextCount = countDownLatch.getCount(); + + if (nextCount < 0) { + // Unreachable + return -2; + } + + if (count == 0) { + // Could not differs from 0 too + return nextCount; + } + + if (count - nextCount != 1) { + // Unreachable + return -3; + } + + return nextCount; + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/threads/ExecutorServiceExamples.java b/utbot-sample/src/main/java/org/utbot/examples/threads/ExecutorServiceExamples.java new file mode 100644 index 0000000000..c55005343e --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/threads/ExecutorServiceExamples.java @@ -0,0 +1,23 @@ +package org.utbot.examples.threads; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; + +public class ExecutorServiceExamples { + public void throwingInExecute() { + Executors.newSingleThreadExecutor().execute(() -> { + throw new IllegalStateException(); + }); + } + + public int changingCollectionInExecute() { + List list = new ArrayList<>(); + + Executors.newSingleThreadExecutor().execute(() -> { + list.add(42); + }); + + return list.get(0); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/threads/FutureExamples.java b/utbot-sample/src/main/java/org/utbot/examples/threads/FutureExamples.java new file mode 100644 index 0000000000..2b233f239d --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/threads/FutureExamples.java @@ -0,0 +1,43 @@ +package org.utbot.examples.threads; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class FutureExamples { + public void throwingRunnableExample() throws ExecutionException, InterruptedException { + final CompletableFuture future = CompletableFuture.runAsync(() -> { + throw new IllegalStateException(); + }); + + future.get(); + } + + public int resultFromGet() throws ExecutionException, InterruptedException { + final CompletableFuture future = CompletableFuture.supplyAsync(() -> 42); + + return future.get(); + } + + public int changingCollectionInFuture() throws ExecutionException, InterruptedException { + List values = new ArrayList<>(); + + final CompletableFuture future = CompletableFuture.runAsync(() -> values.add(42)); + + future.get(); + + return values.get(0); + } + + // NOTE: this tests looks similar as the test above BUT it is important to check correctness of the wrapper + // for CompletableFuture - an actions is executed regardless of invoking `get` method. + @SuppressWarnings("unused") + public int changingCollectionInFutureWithoutGet() { + List values = new ArrayList<>(); + + final CompletableFuture future = CompletableFuture.runAsync(() -> values.add(42)); + + return values.get(0); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/threads/ThreadExamples.java b/utbot-sample/src/main/java/org/utbot/examples/threads/ThreadExamples.java new file mode 100644 index 0000000000..21cc1bbfe1 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/threads/ThreadExamples.java @@ -0,0 +1,29 @@ +package org.utbot.examples.threads; + +import java.util.ArrayList; +import java.util.List; + +public class ThreadExamples { + public void explicitExceptionInStart() { + new Thread(() -> { + throw new IllegalStateException(); + }).start(); + } + + public int changingCollectionInThread() { + List values = new ArrayList<>(); + + new Thread(() -> values.add(42)).start(); + + return values.get(0); + } + + @SuppressWarnings("unused") + public int changingCollectionInThreadWithoutStart() { + List values = new ArrayList<>(); + + final Thread thread = new Thread(() -> values.add(42)); + + return values.get(0); + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/types/CollectionAsField.java b/utbot-sample/src/main/java/org/utbot/examples/types/CollectionAsField.java new file mode 100644 index 0000000000..157f34f63c --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/types/CollectionAsField.java @@ -0,0 +1,10 @@ +package org.utbot.examples.types; + +import java.util.HashMap; +import java.util.Map; + +public class CollectionAsField { + public static Map staticMap = new HashMap<>(); + public Map nonStaticMap = new HashMap<>(); + public T field; +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/types/Generics.java b/utbot-sample/src/main/java/org/utbot/examples/types/Generics.java new file mode 100644 index 0000000000..5272a4d391 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/types/Generics.java @@ -0,0 +1,57 @@ +package org.utbot.examples.types; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class Generics { + public boolean genericAsField(CollectionAsField object) { + if (object != null && object.field != null) { + return object.field.equals("abc"); + } + + return false; + } + + public String mapAsStaticField() { + CollectionAsField.staticMap.put("key", "value"); + return CollectionAsField.staticMap.get("key"); + } + + public String mapAsParameter(Map map) { + map.put("key", "value"); + return map.get("key"); + } + + public String mapAsNonStaticField(CollectionAsField object) { + object.nonStaticMap.put("key", "value"); + return object.nonStaticMap.get("key"); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public String methodWithRawType(Map map) { + nestedMethodWithGenericInfo(map); + map.put("key", "value"); + + return (String) map.get("key"); + } + + @SuppressWarnings("UnusedReturnValue") + private Map nestedMethodWithGenericInfo(Map map) { + return map; + } + + public int methodWithArrayTypeBoundary() { + return new ArrayTypeParameters().methodWithArrayTypeBoundary(null); + } +} + +class ArrayTypeParameters { + public int methodWithArrayTypeBoundary(List list) { + if (list.isEmpty()) { + return -1; + } + + return 1; + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/types/PathDependentGenericsExample.java b/utbot-sample/src/main/java/org/utbot/examples/types/PathDependentGenericsExample.java new file mode 100644 index 0000000000..e0861705b6 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/types/PathDependentGenericsExample.java @@ -0,0 +1,59 @@ +package org.utbot.examples.types; + +import org.utbot.api.mock.UtMock; + +import java.util.List; + +public class PathDependentGenericsExample { + public int pathDependentGenerics(GenericParent element) { + if (element instanceof ClassWithOneGeneric) { + functionWithOneGeneric((ClassWithOneGeneric) element); + return 1; + } + + if (element instanceof ClassWithTwoGenerics) { + functionWithTwoGenerics((ClassWithTwoGenerics) element); + return 2; + } + + return 3; + } + + public int functionWithSeveralTypeConstraintsForTheSameObject(Object element) { + if (element instanceof List) { + functionWithSeveralGenerics((List) element, (List) element); + + UtMock.assume(!((List) element).isEmpty()); + Object value = ((List) element).get(0); + UtMock.assume(value != null); + + if (value instanceof Number) { + return 1; + } else { + return 2; // unreachable + } + } + + return 3; + } + + private void functionWithSeveralGenerics(List firstValue, List anotherValue) { + } + + private void functionWithOneGeneric(ClassWithOneGeneric value) { + System.out.println(); + } + + private void functionWithTwoGenerics(ClassWithTwoGenerics value) { + System.out.println(); + } +} + +abstract class GenericParent { +} + +class ClassWithOneGeneric extends GenericParent { +} + +class ClassWithTwoGenerics extends GenericParent { +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/unsafe/UnsafeWithField.java b/utbot-sample/src/main/java/org/utbot/examples/unsafe/UnsafeWithField.java index bac70b2dcc..83c3bf3df9 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/unsafe/UnsafeWithField.java +++ b/utbot-sample/src/main/java/org/utbot/examples/unsafe/UnsafeWithField.java @@ -5,7 +5,7 @@ public class UnsafeWithField { Field field; - Field setField(Field f) { + public Field setField(Field f) { field = f; return Field.INTEGER; } diff --git a/utbot-sample/src/main/kotlin/org/utbot/examples/codegen/FileWithTopLevelFunctions.kt b/utbot-sample/src/main/kotlin/org/utbot/examples/codegen/FileWithTopLevelFunctions.kt new file mode 100644 index 0000000000..f9b281a5ce --- /dev/null +++ b/utbot-sample/src/main/kotlin/org/utbot/examples/codegen/FileWithTopLevelFunctions.kt @@ -0,0 +1,15 @@ +package org.utbot.examples.codegen + +class CustomClass + +fun topLevelSum(a: Int, b: Int): Int { + return a + b +} + +fun Int.extensionOnBasicType(other: Int): Int { + return this + other +} + +fun CustomClass.extensionOnCustomClass(other: CustomClass): Boolean { + return this === other +} \ No newline at end of file diff --git a/utbot-sample/src/main/resources/taint/TaintBranchingConfig.yaml b/utbot-sample/src/main/resources/taint/TaintBranchingConfig.yaml new file mode 100644 index 0000000000..70ad529cad --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintBranchingConfig.yaml @@ -0,0 +1,9 @@ +sources: + - org.utbot.examples.taint.TaintBranching.source: + add-to: return + marks: bad + +sinks: + - org.utbot.examples.taint.TaintBranching.sink: + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/taint/TaintCleanerConditionsConfig.yaml b/utbot-sample/src/main/resources/taint/TaintCleanerConditionsConfig.yaml new file mode 100644 index 0000000000..1594025397 --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintCleanerConditionsConfig.yaml @@ -0,0 +1,29 @@ +sources: + - org.utbot.examples.taint.TaintCleanerConditions.source: + add-to: return + marks: bad + - org.utbot.examples.taint.TaintCleanerConditions.sourceEmpty: + add-to: return + marks: bad + +cleaners: + - org.utbot.examples.taint.TaintCleanerConditions.cleanerArgCondition: + remove-from: arg1 + marks: bad + conditions: + arg1: "" + - org.utbot.examples.taint.TaintCleanerConditions.cleanerReturnCondition: + remove-from: arg1 + marks: bad + conditions: + return: true + - java.lang.String.isEmpty: + remove-from: this + marks: bad + conditions: + this: "" + +sinks: + - org.utbot.examples.taint.TaintCleanerConditions.sink: + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/taint/TaintCleanerSimpleConfig.yaml b/utbot-sample/src/main/resources/taint/TaintCleanerSimpleConfig.yaml new file mode 100644 index 0000000000..9761b1ad1f --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintCleanerSimpleConfig.yaml @@ -0,0 +1,14 @@ +sources: + - org.utbot.examples.taint.TaintCleanerSimple.source: + add-to: return + marks: bad + +cleaners: + - org.utbot.examples.taint.TaintCleanerSimple.cleaner: + remove-from: arg1 + marks: bad + +sinks: + - org.utbot.examples.taint.TaintCleanerSimple.sink: + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/taint/TaintLongPathConfig.yaml b/utbot-sample/src/main/resources/taint/TaintLongPathConfig.yaml new file mode 100644 index 0000000000..813971f19b --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintLongPathConfig.yaml @@ -0,0 +1,9 @@ +sources: + - org.utbot.examples.taint.TaintLongPath.source: + add-to: return + marks: bad + +sinks: + - org.utbot.examples.taint.TaintLongPath.sink: + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/taint/TaintOtherClassConfig.yaml b/utbot-sample/src/main/resources/taint/TaintOtherClassConfig.yaml new file mode 100644 index 0000000000..ec02e98452 --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintOtherClassConfig.yaml @@ -0,0 +1,20 @@ +sources: + - org.utbot.examples.taint.Inner.source: + add-to: return + marks: bad + +passes: + - org.utbot.examples.taint.Inner.pass: + get-from: arg1 + add-to: return + marks: bad + +cleaners: + - org.utbot.examples.taint.Inner.cleaner: + remove-from: arg1 + marks: bad + +sinks: + - org.utbot.examples.taint.Inner.sink: + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/taint/TaintPassConditionsConfig.yaml b/utbot-sample/src/main/resources/taint/TaintPassConditionsConfig.yaml new file mode 100644 index 0000000000..86ba0d173d --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintPassConditionsConfig.yaml @@ -0,0 +1,34 @@ +sources: + - org.utbot.examples.taint.TaintPassConditions.source: + add-to: return + marks: bad + - org.utbot.examples.taint.TaintPassConditions.sourceEmpty: + add-to: return + marks: bad + +passes: + - org.utbot.examples.taint.TaintPassConditions.passArgCondition: + get-from: arg1 + add-to: return + marks: bad + conditions: + arg2: true + - org.utbot.examples.taint.TaintPassConditions.passReturnCondition: + get-from: arg1 + add-to: return + marks: bad + conditions: + return: + not: "" + - java.lang.String.concat: + get-from: [ this, arg1 ] + add-to: return + marks: bad + conditions: + this: + not: "" + +sinks: + - org.utbot.examples.taint.TaintPassConditions.sink: + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/taint/TaintPassSimpleConfig.yaml b/utbot-sample/src/main/resources/taint/TaintPassSimpleConfig.yaml new file mode 100644 index 0000000000..f2995edfc9 --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintPassSimpleConfig.yaml @@ -0,0 +1,15 @@ +sources: + - org.utbot.examples.taint.TaintPassSimple.source: + add-to: return + marks: bad + +passes: + - org.utbot.examples.taint.TaintPassSimple.pass: + get-from: arg1 + add-to: return + marks: bad + +sinks: + - org.utbot.examples.taint.TaintPassSimple.sink: + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/taint/TaintSeveralMarksConfig.yaml b/utbot-sample/src/main/resources/taint/TaintSeveralMarksConfig.yaml new file mode 100644 index 0000000000..4b2bd7cce9 --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintSeveralMarksConfig.yaml @@ -0,0 +1,45 @@ +sources: + - org.utbot.examples.taint.TaintSeveralMarks.source1: + add-to: return + marks: mark1 + - org.utbot.examples.taint.TaintSeveralMarks.source2: + add-to: return + marks: mark2 + - org.utbot.examples.taint.TaintSeveralMarks.source12: + add-to: return + marks: [ mark1, mark2 ] + +passes: + - org.utbot.examples.taint.TaintSeveralMarks.pass1: + get-from: arg1 + add-to: return + marks: mark1 + - org.utbot.examples.taint.TaintSeveralMarks.pass2: + get-from: arg1 + add-to: return + marks: mark2 + +cleaners: + - org.utbot.examples.taint.TaintSeveralMarks.cleaner1: + remove-from: arg1 + marks: mark1 + - org.utbot.examples.taint.TaintSeveralMarks.cleaner2: + remove-from: arg1 + marks: mark2 + +sinks: + - org.utbot.examples.taint.TaintSeveralMarks.sink1: + check: arg1 + marks: mark1 + - org.utbot.examples.taint.TaintSeveralMarks.sink2: + check: arg1 + marks: mark2 + - org.utbot.examples.taint.TaintSeveralMarks.sink13: + check: arg1 + marks: [ mark1, mark3 ] + - org.utbot.examples.taint.TaintSeveralMarks.sink123: + check: arg1 + marks: [ mark1, mark2, mark3 ] + - org.utbot.examples.taint.TaintSeveralMarks.sinkAll: + check: arg1 + marks: [] diff --git a/utbot-sample/src/main/resources/taint/TaintSignatureConfig.yaml b/utbot-sample/src/main/resources/taint/TaintSignatureConfig.yaml new file mode 100644 index 0000000000..73ef64c550 --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintSignatureConfig.yaml @@ -0,0 +1,24 @@ +sources: + - org.utbot.examples.taint.TaintSignature.source: + signature: [ ] + add-to: return + marks: bad + +passes: + - org.utbot.examples.taint.TaintSignature.pass: + signature: [ ] + get-from: arg1 + add-to: return + marks: bad + +cleaners: + - org.utbot.examples.taint.TaintSignature.cleaner: + signature: [ ] + remove-from: arg1 + marks: bad + +sinks: + - org.utbot.examples.taint.TaintSignature.sink: + signature: [ ] + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/taint/TaintSimpleConfig.yaml b/utbot-sample/src/main/resources/taint/TaintSimpleConfig.yaml new file mode 100644 index 0000000000..a501b40751 --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintSimpleConfig.yaml @@ -0,0 +1,9 @@ +sources: + - org.utbot.examples.taint.TaintSimple.source: + add-to: return + marks: bad + +sinks: + - org.utbot.examples.taint.TaintSimple.sink: + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/taint/TaintSinkConditionsConfig.yaml b/utbot-sample/src/main/resources/taint/TaintSinkConditionsConfig.yaml new file mode 100644 index 0000000000..75392fc7c9 --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintSinkConditionsConfig.yaml @@ -0,0 +1,21 @@ +sources: + - org.utbot.examples.taint.TaintSinkConditions.source: + add-to: return + marks: bad + - org.utbot.examples.taint.TaintSinkConditions.sourceEmpty: + add-to: return + marks: bad + +sinks: + - org.utbot.examples.taint.TaintSinkConditions.sink: + check: arg1 + marks: bad + conditions: + arg2: true + - java.lang.String.getBytes: + check: this + marks: bad + conditions: + this: + not: + "" diff --git a/utbot-sample/src/main/resources/taint/TaintSourceConditionsConfig.yaml b/utbot-sample/src/main/resources/taint/TaintSourceConditionsConfig.yaml new file mode 100644 index 0000000000..0fc1fc4177 --- /dev/null +++ b/utbot-sample/src/main/resources/taint/TaintSourceConditionsConfig.yaml @@ -0,0 +1,24 @@ +sources: + - org.utbot.examples.taint.TaintSourceConditions.sourceArgCondition: + add-to: return + marks: bad + conditions: + arg1: true + - org.utbot.examples.taint.TaintSourceConditions.sourceReturnCondition: + add-to: return + marks: bad + conditions: + return: + not: "" + - java.lang.String.toLowerCase: + add-to: return + marks: bad + conditions: + this: + not: + "" + +sinks: + - org.utbot.examples.taint.TaintSourceConditions.sink: + check: arg1 + marks: bad diff --git a/utbot-sample/src/main/resources/utbot-api.jar b/utbot-sample/src/main/resources/utbot-api.jar index 8398d3ed07..a29bf950b9 100644 Binary files a/utbot-sample/src/main/resources/utbot-api.jar and b/utbot-sample/src/main/resources/utbot-api.jar differ diff --git a/utbot-spring-analyzer/build.gradle.kts b/utbot-spring-analyzer/build.gradle.kts new file mode 100644 index 0000000000..9f12641120 --- /dev/null +++ b/utbot-spring-analyzer/build.gradle.kts @@ -0,0 +1,61 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer + +val springBootVersion: String by rootProject +val rdVersion: String by rootProject +val commonsLoggingVersion: String by rootProject +val kotlinLoggingVersion: String by rootProject +val commonsIOVersion: String by rootProject + +plugins { + id("com.github.johnrengelman.shadow") version "7.1.2" + id("java") + application +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +val shadowJarConfiguration: Configuration by configurations.creating {} +configurations.implementation.get().extendsFrom(shadowJarConfiguration) + +dependencies { + // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot + compileOnly("org.springframework.boot:spring-boot:$springBootVersion") + compileOnly("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") + + fun ModuleDependency.excludeSlf4jApi() = exclude(group = "org.slf4j", module = "slf4j-api") + + // TODO stop putting dependencies that are only used in SpringAnalyzerProcess into shadow jar + implementation(project(":utbot-rd")) { excludeSlf4jApi() } + implementation(project(":utbot-core")) { excludeSlf4jApi() } + implementation(project(":utbot-framework-api")) { excludeSlf4jApi() } + + runtimeOnly(project(":utbot-spring-commons")) { excludeSlf4jApi() } + implementation(project(":utbot-spring-commons-api")) { excludeSlf4jApi() } + + implementation("com.jetbrains.rd:rd-framework:$rdVersion") { excludeSlf4jApi() } + implementation("com.jetbrains.rd:rd-core:$rdVersion") { excludeSlf4jApi() } + implementation("commons-logging:commons-logging:$commonsLoggingVersion") { excludeSlf4jApi() } +} + +application { + mainClass.set("org.utbot.spring.process.SpringAnalyzerProcessMainKt") +} + +tasks.shadowJar { + isZip64 = true + + transform(Log4j2PluginsCacheFileTransformer::class.java) + archiveFileName.set("utbot-spring-analyzer-shadow.jar") +} + +val springAnalyzerJar: Configuration by configurations.creating { + isCanBeResolved = false + isCanBeConsumed = true +} + +artifacts { + add(springAnalyzerJar.name, tasks.shadowJar) +} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt new file mode 100644 index 0000000000..ba81bff3c1 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt @@ -0,0 +1,28 @@ +package org.utbot.spring.analyzer + +import org.utbot.spring.api.provider.InstantiationSettings +import org.utbot.spring.api.ApplicationData +import org.utbot.spring.api.provider.SpringApiProviderFacade +import org.utbot.spring.exception.UtBotSpringShutdownException +import org.utbot.spring.generated.BeanDefinitionData +import org.utbot.spring.utils.SourceFinder + +class SpringApplicationAnalyzer { + + fun getBeanDefinitions(applicationData: ApplicationData): Array { + // TODO: get rid of SourceFinder + val configurationClasses = SourceFinder(applicationData).findSources() + val instantiationSettings = InstantiationSettings( + configurationClasses, + applicationData.springSettings.profiles.toTypedArray(), + ) + + return SpringApiProviderFacade.getInstance(this::class.java.classLoader) + .useMostSpecificNonFailingApi(instantiationSettings) { springApi -> + UtBotSpringShutdownException + .catch { springApi.getOrLoadSpringApplicationContext() } + .beanDefinitions + .toTypedArray() + }.result.getOrThrow() + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/api/ApplicationData.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/api/ApplicationData.kt new file mode 100644 index 0000000000..b776eb9c76 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/api/ApplicationData.kt @@ -0,0 +1,7 @@ +package org.utbot.spring.api + +import org.utbot.framework.plugin.api.SpringSettings.* + +class ApplicationData( + val springSettings: PresentSpringSettings +) diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/config/TestApplicationConfiguration.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/config/TestApplicationConfiguration.kt new file mode 100644 index 0000000000..5fdee855f6 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/config/TestApplicationConfiguration.kt @@ -0,0 +1,15 @@ +package org.utbot.spring.config + +import org.springframework.beans.factory.config.BeanFactoryPostProcessor +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.ImportResource +import org.utbot.spring.postProcessors.UtBotBeanFactoryPostProcessor + +@Configuration +@ImportResource +open class TestApplicationConfiguration { + + @Bean + open fun utBotBeanFactoryPostProcessor(): BeanFactoryPostProcessor = UtBotBeanFactoryPostProcessor +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/ApplicationConfigurationType.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/ApplicationConfigurationType.kt new file mode 100644 index 0000000000..e48be8751c --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/ApplicationConfigurationType.kt @@ -0,0 +1,11 @@ +package org.utbot.spring.configurators + +enum class ApplicationConfigurationType { + + XmlConfiguration, + + /** + * Any Java-based configuration, including both simple @Configuration and @SpringBootApplication + */ + JavaConfiguration, +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/XmlConfigurationParser.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/XmlConfigurationParser.kt new file mode 100644 index 0000000000..71c75c0833 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/XmlConfigurationParser.kt @@ -0,0 +1,43 @@ +package org.utbot.spring.configurators + +import org.w3c.dom.Document +import org.w3c.dom.Element +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +class XmlConfigurationParser(private val userXmlFilePath: String, private val fakeXmlFilePath: String) { + + fun fillFakeApplicationXml() { + val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val doc = builder.parse(userXmlFilePath) + + // Property placeholders may contain file names relative to user project, + // they will not be found in ours. We import all properties using another approach. + deletePropertyPlaceholders(doc) + writeXmlFile(doc) + } + + private fun deletePropertyPlaceholders(doc: Document) { + val elements = doc.getElementsByTagName("context:property-placeholder") + val elementsCount = elements.length + + // Xml file may contain several property placeholders: + // see https://stackoverflow.com/questions/26618400/how-to-use-multiple-property-placeholder-in-a-spring-xml-file + for (i in 0 until elementsCount) { + val element = elements.item(i) as Element + element.parentNode.removeChild(element) + } + + doc.normalize() + } + + private fun writeXmlFile(doc: Document) { + val tFormer = TransformerFactory.newInstance().newTransformer() + val source = DOMSource(doc) + val destination = StreamResult(fakeXmlFilePath) + + tFormer.transform(source, destination) + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt new file mode 100644 index 0000000000..6d80ba9166 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt @@ -0,0 +1,34 @@ +package org.utbot.spring.exception + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.utbot.spring.generated.BeanDefinitionData + +private val logger = getLogger() + +/** + * Use this exception to shutdown the application + * when all required analysis actions are completed. + */ +class UtBotSpringShutdownException( + message: String, + val beanDefinitions: List +): RuntimeException(message) { + companion object { + fun catch(block: () -> Unit): UtBotSpringShutdownException { + try { + block() + throw IllegalStateException("UtBotSpringShutdownException has not been thrown") + } catch (e: Throwable) { + // Spring sometimes wraps exceptions in other exceptions, so we go over + // all the causes to determine if UtBotSpringShutdownException was thrown + for(cause in generateSequence(e) { it.cause }) + if (cause is UtBotSpringShutdownException) { + logger.info { "UtBotSpringShutdownException has been successfully caught" } + return cause + } + throw e + } + } + } +} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/generated/SpringAnalyzerProcessModel.Generated.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/generated/SpringAnalyzerProcessModel.Generated.kt new file mode 100644 index 0000000000..066048b61e --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/generated/SpringAnalyzerProcessModel.Generated.kt @@ -0,0 +1,348 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.spring.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [SpringAnalyzerModel.kt:8] + */ +class SpringAnalyzerProcessModel private constructor( + private val _analyze: RdCall +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + serializers.register(SpringAnalyzerParams) + serializers.register(BeanAdditionalData) + serializers.register(BeanDefinitionData) + serializers.register(SpringAnalyzerResult) + } + + + @JvmStatic + @JvmName("internalCreateModel") + @Deprecated("Use create instead", ReplaceWith("create(lifetime, protocol)")) + internal fun createModel(lifetime: Lifetime, protocol: IProtocol): SpringAnalyzerProcessModel { + @Suppress("DEPRECATION") + return create(lifetime, protocol) + } + + @JvmStatic + @Deprecated("Use protocol.springAnalyzerProcessModel or revise the extension scope instead", ReplaceWith("protocol.springAnalyzerProcessModel")) + fun create(lifetime: Lifetime, protocol: IProtocol): SpringAnalyzerProcessModel { + SpringAnalyzerRoot.register(protocol.serializers) + + return SpringAnalyzerProcessModel() + } + + + const val serializationHash = 8094902537230457267L + + } + override val serializersOwner: ISerializersOwner get() = SpringAnalyzerProcessModel + override val serializationHash: Long get() = SpringAnalyzerProcessModel.serializationHash + + //fields + val analyze: RdCall get() = _analyze + //methods + //initializer + init { + _analyze.async = true + } + + init { + bindableChildren.add("analyze" to _analyze) + } + + //secondary constructor + private constructor( + ) : this( + RdCall(SpringAnalyzerParams, SpringAnalyzerResult) + ) + + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("SpringAnalyzerProcessModel (") + printer.indent { + print("analyze = "); _analyze.print(printer); println() + } + printer.print(")") + } + //deepClone + override fun deepClone(): SpringAnalyzerProcessModel { + return SpringAnalyzerProcessModel( + _analyze.deepClonePolymorphic() + ) + } + //contexts +} +val IProtocol.springAnalyzerProcessModel get() = getOrCreateExtension(SpringAnalyzerProcessModel::class) { @Suppress("DEPRECATION") SpringAnalyzerProcessModel.create(lifetime, this) } + + + +/** + * #### Generated from [SpringAnalyzerModel.kt:13] + */ +data class BeanAdditionalData ( + val factoryMethodName: String, + val parameterTypes: List, + val configClassFqn: String +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = BeanAdditionalData::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): BeanAdditionalData { + val factoryMethodName = buffer.readString() + val parameterTypes = buffer.readList { buffer.readString() } + val configClassFqn = buffer.readString() + return BeanAdditionalData(factoryMethodName, parameterTypes, configClassFqn) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: BeanAdditionalData) { + buffer.writeString(value.factoryMethodName) + buffer.writeList(value.parameterTypes) { v -> buffer.writeString(v) } + buffer.writeString(value.configClassFqn) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as BeanAdditionalData + + if (factoryMethodName != other.factoryMethodName) return false + if (parameterTypes != other.parameterTypes) return false + if (configClassFqn != other.configClassFqn) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + factoryMethodName.hashCode() + __r = __r*31 + parameterTypes.hashCode() + __r = __r*31 + configClassFqn.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("BeanAdditionalData (") + printer.indent { + print("factoryMethodName = "); factoryMethodName.print(printer); println() + print("parameterTypes = "); parameterTypes.print(printer); println() + print("configClassFqn = "); configClassFqn.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [SpringAnalyzerModel.kt:19] + */ +data class BeanDefinitionData ( + val beanName: String, + val beanTypeFqn: String, + val additionalData: BeanAdditionalData? +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = BeanDefinitionData::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): BeanDefinitionData { + val beanName = buffer.readString() + val beanTypeFqn = buffer.readString() + val additionalData = buffer.readNullable { BeanAdditionalData.read(ctx, buffer) } + return BeanDefinitionData(beanName, beanTypeFqn, additionalData) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: BeanDefinitionData) { + buffer.writeString(value.beanName) + buffer.writeString(value.beanTypeFqn) + buffer.writeNullable(value.additionalData) { BeanAdditionalData.write(ctx, buffer, it) } + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as BeanDefinitionData + + if (beanName != other.beanName) return false + if (beanTypeFqn != other.beanTypeFqn) return false + if (additionalData != other.additionalData) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + beanName.hashCode() + __r = __r*31 + beanTypeFqn.hashCode() + __r = __r*31 + if (additionalData != null) additionalData.hashCode() else 0 + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("BeanDefinitionData (") + printer.indent { + print("beanName = "); beanName.print(printer); println() + print("beanTypeFqn = "); beanTypeFqn.print(printer); println() + print("additionalData = "); additionalData.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [SpringAnalyzerModel.kt:9] + */ +data class SpringAnalyzerParams ( + val springSettings: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = SpringAnalyzerParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): SpringAnalyzerParams { + val springSettings = buffer.readByteArray() + return SpringAnalyzerParams(springSettings) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: SpringAnalyzerParams) { + buffer.writeByteArray(value.springSettings) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as SpringAnalyzerParams + + if (!(springSettings contentEquals other.springSettings)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + springSettings.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("SpringAnalyzerParams (") + printer.indent { + print("springSettings = "); springSettings.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [SpringAnalyzerModel.kt:25] + */ +data class SpringAnalyzerResult ( + val beanDefinitions: Array +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = SpringAnalyzerResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): SpringAnalyzerResult { + val beanDefinitions = buffer.readArray {BeanDefinitionData.read(ctx, buffer)} + return SpringAnalyzerResult(beanDefinitions) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: SpringAnalyzerResult) { + buffer.writeArray(value.beanDefinitions) { BeanDefinitionData.write(ctx, buffer, it) } + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as SpringAnalyzerResult + + if (!(beanDefinitions contentDeepEquals other.beanDefinitions)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + beanDefinitions.contentDeepHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("SpringAnalyzerResult (") + printer.indent { + print("beanDefinitions = "); beanDefinitions.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/generated/SpringAnalyzerRoot.Generated.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/generated/SpringAnalyzerRoot.Generated.kt new file mode 100644 index 0000000000..4a4e0649e0 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/generated/SpringAnalyzerRoot.Generated.kt @@ -0,0 +1,59 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE","EXPERIMENTAL_UNSIGNED_LITERALS","PackageDirectoryMismatch","UnusedImport","unused","LocalVariableName","CanBeVal","PropertyName","EnumEntryName","ClassName","ObjectPropertyName","UnnecessaryVariable","SpellCheckingInspection") +package org.utbot.spring.generated + +import com.jetbrains.rd.framework.* +import com.jetbrains.rd.framework.base.* +import com.jetbrains.rd.framework.impl.* + +import com.jetbrains.rd.util.lifetime.* +import com.jetbrains.rd.util.reactive.* +import com.jetbrains.rd.util.string.* +import com.jetbrains.rd.util.* +import kotlin.time.Duration +import kotlin.reflect.KClass +import kotlin.jvm.JvmStatic + + + +/** + * #### Generated from [SpringAnalyzerModel.kt:6] + */ +class SpringAnalyzerRoot private constructor( +) : RdExtBase() { + //companion + + companion object : ISerializersOwner { + + override fun registerSerializersCore(serializers: ISerializers) { + SpringAnalyzerRoot.register(serializers) + SpringAnalyzerProcessModel.register(serializers) + } + + + + + + const val serializationHash = -4315357569975275049L + + } + override val serializersOwner: ISerializersOwner get() = SpringAnalyzerRoot + override val serializationHash: Long get() = SpringAnalyzerRoot.serializationHash + + //fields + //methods + //initializer + //secondary constructor + //equals trait + //hash code trait + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("SpringAnalyzerRoot (") + printer.print(")") + } + //deepClone + override fun deepClone(): SpringAnalyzerRoot { + return SpringAnalyzerRoot( + ) + } + //contexts +} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/loggers/RDApacheCommonsLogFactory.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/loggers/RDApacheCommonsLogFactory.kt new file mode 100644 index 0000000000..0c78cb87a1 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/loggers/RDApacheCommonsLogFactory.kt @@ -0,0 +1,46 @@ +package org.utbot.spring.loggers + +import com.jetbrains.rd.util.LogLevel +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.apache.commons.logging.Log +import org.apache.commons.logging.impl.LogFactoryImpl +import org.utbot.spring.exception.UtBotSpringShutdownException + +@Suppress("unused") // used via -Dorg.apache.commons.logging.LogFactory=org.utbot.spring.loggers.RDApacheCommonsLogFactory +class RDApacheCommonsLogFactory : LogFactoryImpl() { + override fun getInstance(name: String): Log { + val logger = getLogger(category = name) + return object : Log { + private fun log(level: LogLevel, message: Any?, throwable: Throwable?) { + if (throwable is UtBotSpringShutdownException) { + // avoid polluting logs with stack trace of expected exception + logger.info { message } + logger.info { "${throwable::class.java.name}: ${throwable.message}" } + } else + logger.log(level, message, throwable) + } + private fun isEnabled(level: LogLevel) = logger.isEnabled(level) + + override fun trace(message: Any?) = log(LogLevel.Trace, message, throwable = null) + override fun trace(message: Any?, t: Throwable?) = log(LogLevel.Trace, message, throwable = t) + override fun debug(message: Any?) = log(LogLevel.Debug, message, throwable = null) + override fun debug(message: Any?, t: Throwable?) = log(LogLevel.Debug, message, throwable = t) + override fun info(message: Any?) = log(LogLevel.Info, message, throwable = null) + override fun info(message: Any?, t: Throwable?) = log(LogLevel.Info, message, throwable = t) + override fun warn(message: Any?) = log(LogLevel.Warn, message, throwable = null) + override fun warn(message: Any?, t: Throwable?) = log(LogLevel.Warn, message, throwable = t) + override fun error(message: Any?) = log(LogLevel.Error, message, throwable = null) + override fun error(message: Any?, t: Throwable?) = log(LogLevel.Error, message, throwable = t) + override fun fatal(message: Any?) = log(LogLevel.Fatal, message, throwable = null) + override fun fatal(message: Any?, t: Throwable?) = log(LogLevel.Fatal, message, throwable = t) + + override fun isTraceEnabled() = isEnabled(LogLevel.Trace) + override fun isDebugEnabled() = isEnabled(LogLevel.Debug) + override fun isInfoEnabled() = isEnabled(LogLevel.Info) + override fun isErrorEnabled() = isEnabled(LogLevel.Error) + override fun isFatalEnabled() = isEnabled(LogLevel.Fatal) + override fun isWarnEnabled() = isEnabled(LogLevel.Warn) + } + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt new file mode 100644 index 0000000000..705d0503f4 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt @@ -0,0 +1,92 @@ +package org.utbot.spring.postProcessors + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import com.jetbrains.rd.util.warn +import org.springframework.beans.factory.BeanCreationException +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition +import org.springframework.beans.factory.config.BeanFactoryPostProcessor +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.core.PriorityOrdered +import org.springframework.core.type.StandardMethodMetadata +import org.utbot.spring.exception.UtBotSpringShutdownException +import org.utbot.spring.generated.BeanAdditionalData +import org.utbot.spring.generated.BeanDefinitionData + +val logger = getLogger() + +object UtBotBeanFactoryPostProcessor : BeanFactoryPostProcessor, PriorityOrdered { + /** + * Sets the priority of post processor to highest to avoid side effects from others. + */ + override fun getOrder(): Int = PriorityOrdered.HIGHEST_PRECEDENCE + + override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) { + logger.info { "Started post-processing bean factory in UtBot" } + + val beanDefinitions = findBeanDefinitions(beanFactory) + logger.info { "Detected ${beanDefinitions.size} bean qualified names" } + + logger.info { "Finished post-processing bean factory in UtBot" } + + destroyBeanDefinitions(beanFactory) + throw UtBotSpringShutdownException("Finished post-processing bean factory in UtBot", beanDefinitions) + } + + private fun findBeanDefinitions(beanFactory: ConfigurableListableBeanFactory): List = + beanFactory.beanDefinitionNames + .mapNotNull { getBeanDefinitionData(beanFactory, it) } + .filterNot { + it.beanTypeFqn.startsWith("org.utbot.spring") || + UtBotBeanFactoryPostProcessor.javaClass.simpleName.equals(it.beanName, ignoreCase = true) + } + + private fun getBeanDefinitionData(beanFactory: ConfigurableListableBeanFactory, beanName: String): BeanDefinitionData? { + val beanDefinition = beanFactory.getBeanDefinition(beanName) + + var beanAdditionalData: BeanAdditionalData? = null + val beanTypeFqn = if (beanDefinition is AnnotatedBeanDefinition) { + if (beanDefinition.factoryMethodMetadata == null) { + // there's no factoryMethod so bean is defined with @Component-like annotation rather than @Bean annotation + // same approach isn't applicable for @Bean beans, because for them, it returns name of @Configuration class + beanDefinition.metadata.className + .also { fqn -> + logger.info { "Got $fqn as metadata.className for @Component-like bean: $beanName" } + } + } else try { + val parameterTypes = + (beanDefinition.factoryMethodMetadata as? StandardMethodMetadata) + ?.introspectedMethod + ?.parameterTypes + ?.map { it.name } + ?: emptyList() + + beanAdditionalData = BeanAdditionalData( + factoryMethodName = beanDefinition.factoryMethodMetadata.methodName, + parameterTypes = parameterTypes, + configClassFqn = beanDefinition.factoryMethodMetadata.declaringClassName + ) + + // we determine a real return type later in [UtTestsDialogProcessor.createTests] + beanDefinition.factoryMethodMetadata.returnTypeName + } catch (e: BeanCreationException) { + logger.warn { "Failed to get bean: $beanName" } + null + } + } else { + beanDefinition.beanClassName.also { fqn -> + logger.info { "Got $fqn as beanClassName for XML-like bean: $beanName" } + } + } + + return beanTypeFqn?.let { BeanDefinitionData(beanName, beanTypeFqn, beanAdditionalData) } + } + + private fun destroyBeanDefinitions(beanFactory: ConfigurableListableBeanFactory) { + for (beanDefinitionName in beanFactory.beanDefinitionNames) { + val beanRegistry = beanFactory as BeanDefinitionRegistry + beanRegistry.removeBeanDefinition(beanDefinitionName) + } + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt new file mode 100644 index 0000000000..044ef0c4a9 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt @@ -0,0 +1,99 @@ +package org.utbot.spring.process + +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import org.utbot.common.JarUtils +import org.utbot.common.getPid +import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.services.WorkingDirService +import org.utbot.framework.process.AbstractRDProcessCompanion +import org.utbot.rd.ProcessWithRdServer +import org.utbot.rd.exceptions.InstantProcessDeathException +import org.utbot.rd.generated.LoggerModel +import org.utbot.rd.generated.loggerModel +import org.utbot.rd.loggers.setup +import org.utbot.rd.onSchedulerBlocking +import org.utbot.rd.startBlocking +import org.utbot.rd.startUtProcessWithRdServer +import org.utbot.rd.terminateOnException +import org.utbot.spring.generated.SpringAnalyzerParams +import org.utbot.spring.generated.SpringAnalyzerProcessModel +import org.utbot.spring.generated.SpringAnalyzerResult +import org.utbot.spring.generated.springAnalyzerProcessModel +import java.io.File +import org.utbot.framework.plugin.api.SpringSettings.* +import org.utbot.framework.process.kryo.KryoHelper + +class SpringAnalyzerProcessInstantDeathException : + InstantProcessDeathException( + UtSettings.springAnalyzerProcessDebugPort, + UtSettings.runSpringAnalyzerProcessWithDebug + ) + +private val logger = KotlinLogging.logger {} + +private var classpathArgs = listOf() + +private const val SPRING_ANALYZER_JAR_FILENAME = "utbot-spring-analyzer-shadow.jar" + +private val springAnalyzerJarFile = + JarUtils.extractJarFileFromResources( + jarFileName = SPRING_ANALYZER_JAR_FILENAME, + jarResourcePath = "lib/$SPRING_ANALYZER_JAR_FILENAME", + targetDirectoryName = "spring-analyzer" + ) + +class SpringAnalyzerProcess private constructor( + rdProcess: ProcessWithRdServer +) : ProcessWithRdServer by rdProcess { + + companion object : AbstractRDProcessCompanion( + debugPort = UtSettings.springAnalyzerProcessDebugPort, + runWithDebug = UtSettings.runSpringAnalyzerProcessWithDebug, + suspendExecutionInDebugMode = UtSettings.suspendSpringAnalyzerProcessExecutionInDebugMode, + processSpecificCommandLineArgs = { + listOf("-Dorg.apache.commons.logging.LogFactory=org.utbot.spring.loggers.RDApacheCommonsLogFactory") + classpathArgs + } + ) { + fun createBlocking(classpath: List) = runBlocking { SpringAnalyzerProcess(classpath) } + + suspend operator fun invoke(classpathItems: List): SpringAnalyzerProcess = + LifetimeDefinition().terminateOnException { lifetime -> + val extendedClasspath = listOf(springAnalyzerJarFile.path) + classpathItems + + val rdProcess = startUtProcessWithRdServer(lifetime) { port -> + classpathArgs = listOf( + "-cp", + extendedClasspath.joinToString(File.pathSeparator), + "org.utbot.spring.process.SpringAnalyzerProcessMainKt" + ) + val cmd = obtainProcessCommandLine(port) + val process = ProcessBuilder(cmd) + .directory(WorkingDirService.provide().toFile()) + .start() + + logger.info { "Spring Analyzer process started with PID = ${process.getPid}" } + + if (!process.isAlive) throw SpringAnalyzerProcessInstantDeathException() + + process + } + rdProcess.awaitProcessReady() + val proc = SpringAnalyzerProcess(rdProcess) + proc.loggerModel.setup(logger, proc.lifetime) + return proc + } + } + + private val springAnalyzerModel: SpringAnalyzerProcessModel = onSchedulerBlocking { protocol.springAnalyzerProcessModel } + private val loggerModel: LoggerModel = onSchedulerBlocking { protocol.loggerModel } + private val kryoHelper = KryoHelper(lifetime) + + fun getBeanDefinitions( + springSettings: PresentSpringSettings + ): SpringAnalyzerResult { + val params = SpringAnalyzerParams(kryoHelper.writeObject(springSettings)) + return springAnalyzerModel.analyze.startBlocking(params) + } +} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt new file mode 100644 index 0000000000..8a1d72f8d2 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt @@ -0,0 +1,55 @@ +package org.utbot.spring.process + +import com.jetbrains.rd.framework.IProtocol +import com.jetbrains.rd.util.Logger +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.reactive.adviseOnce +import org.utbot.common.AbstractSettings +import org.utbot.framework.process.kryo.KryoHelper +import org.utbot.rd.ClientProtocolBuilder +import org.utbot.rd.IdleWatchdog +import org.utbot.rd.RdSettingsContainerFactory +import org.utbot.rd.generated.loggerModel +import org.utbot.rd.generated.settingsModel +import org.utbot.rd.loggers.UtRdRemoteLoggerFactory +import org.utbot.spring.analyzer.SpringApplicationAnalyzer +import org.utbot.spring.api.ApplicationData +import org.utbot.spring.generated.SpringAnalyzerProcessModel +import org.utbot.spring.generated.SpringAnalyzerResult +import org.utbot.spring.generated.springAnalyzerProcessModel +import java.io.File +import kotlin.time.Duration.Companion.seconds + +private val messageFromMainTimeoutMillis = 120.seconds +private val logger = getLogger() + +@Suppress("unused") +object SpringAnalyzerProcessMain + +suspend fun main(args: Array) = + ClientProtocolBuilder().withProtocolTimeout(messageFromMainTimeoutMillis).start(args) { + loggerModel.initRemoteLogging.adviseOnce(lifetime) { + Logger.set(Lifetime.Eternal, UtRdRemoteLoggerFactory(loggerModel)) + logger.info { "-----------------------------------------------------------------------" } + logger.info { "------------------NEW SPRING ANALYZER PROCESS STARTED------------------" } + logger.info { "-----------------------------------------------------------------------" } + } + AbstractSettings.setupFactory(RdSettingsContainerFactory(protocol.settingsModel)) + springAnalyzerProcessModel.setup(it, protocol) + } + +private fun SpringAnalyzerProcessModel.setup(watchdog: IdleWatchdog, realProtocol: IProtocol) { + val kryoHelper = KryoHelper(realProtocol.lifetime) + + watchdog.measureTimeForActiveCall(analyze, "Analyzing Spring Application") { params -> + val applicationData = ApplicationData( + kryoHelper.readObject(params.springSettings) + ) + + SpringAnalyzerResult( + SpringApplicationAnalyzer().getBeanDefinitions(applicationData) + ) + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/PathsUtils.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/PathsUtils.kt new file mode 100644 index 0000000000..7fe7dfba42 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/PathsUtils.kt @@ -0,0 +1,9 @@ +package org.utbot.spring.utils + +import kotlin.io.path.Path + +object PathsUtils { + const val EMPTY_PATH = "" + + fun createFakeFilePath(fileName: String): String = "fake_${Path(fileName).fileName}" +} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt new file mode 100644 index 0000000000..027dabbdc5 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt @@ -0,0 +1,46 @@ +package org.utbot.spring.utils + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.springframework.context.annotation.ImportResource +import org.utbot.common.patchAnnotation +import org.utbot.framework.plugin.api.SpringConfiguration.* +import org.utbot.spring.api.ApplicationData +import org.utbot.spring.config.TestApplicationConfiguration +import java.nio.file.Path +import kotlin.io.path.Path + +private val logger = getLogger() + +class SourceFinder( + private val applicationData: ApplicationData +) { + private val classLoader: ClassLoader = this::class.java.classLoader + + fun findSources(): Array> = + when (val config = applicationData.springSettings.configuration) { + is JavaBasedConfiguration -> { + logger.info { "Using java Spring configuration" } + arrayOf( + TestApplicationConfiguration::class.java, + classLoader.loadClass(config.configBinaryName) + ) + } + + is XMLConfiguration -> { + logger.info { "Using xml Spring configuration" } + + // Put `applicationData.configurationFile` in `@ImportResource` of `TestApplicationConfiguration` + patchImportResourceAnnotation(Path(config.absolutePath).fileName) + + arrayOf(TestApplicationConfiguration::class.java) + } + } + + private fun patchImportResourceAnnotation(userXmlFilePath: Path) = + patchAnnotation( + annotation = TestApplicationConfiguration::class.java.getAnnotation(ImportResource::class.java), + property = "value", + newValue = arrayOf(String.format("classpath:%s", "$userXmlFilePath")) + ) +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/TempFileManager.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/TempFileManager.kt new file mode 100644 index 0000000000..14c8e96a85 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/TempFileManager.kt @@ -0,0 +1,37 @@ +package org.utbot.spring.utils + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import java.io.File +import java.io.IOException + +private val logger = getLogger() + +class TempFileManager(private val fakeFilesList: List) { + + fun createTempFiles() { + for (fileName in fakeFilesList) { + val fakeXmlFileAbsolutePath = PathsUtils.createFakeFilePath(fileName) + + try { + File(fakeXmlFileAbsolutePath).createNewFile() + } catch (e: IOException) { + logger.info { "Fake xml file creation failed with exception $e" } + } + + } + } + + fun deleteTempFiles() { + for (fileName in fakeFilesList) { + val fakeXmlFileAbsolutePath = PathsUtils.createFakeFilePath(fileName) + + try { + File(fakeXmlFileAbsolutePath).delete() + } catch (e: IOException) { + logger.info { "Fake xml file deletion failed with exception $e" } + } + + } + } +} \ No newline at end of file diff --git a/utbot-spring-commons-api/build.gradle.kts b/utbot-spring-commons-api/build.gradle.kts new file mode 100644 index 0000000000..76786b8cb9 --- /dev/null +++ b/utbot-spring-commons-api/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("java") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt new file mode 100644 index 0000000000..c856b00c92 --- /dev/null +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt @@ -0,0 +1,52 @@ +package org.utbot.spring.api + +import java.net.URLClassLoader + +//TODO: `userSourcesClassLoader` must not be passed as a method argument, requires refactoring +interface SpringApi { + /** + * NOTE! [Any] return type is used here because Spring itself may not be on the classpath of the API user class loader + * + * @throws [UTSpringContextLoadingException] as a wrapper for all runtime exceptions + */ + fun getOrLoadSpringApplicationContext(): Any + + /** + * NOTE! [Any] return type is used here because Spring itself may not be on the classpath of the API user class loader + * + * Besides that `entityManager` depending on libraries used by user may be either `javax.persistence.EntityManager` + * or `jakarta.persistence.EntityManager` and they don't have common super type other than `Object`. + */ + fun getEntityManager(): Any + + fun getBean(beanName: String): Any + + fun getDependenciesForBean(beanName: String, userSourcesClassLoader: URLClassLoader): Set + + fun resetBean(beanName: String) + + fun resolveRepositories(beanNames: Set, userSourcesClassLoader: URLClassLoader): Set + + /** + * NOTE! Should be called on one thread with method under test and value constructor, + * because transactions are bound to threads + */ + fun beforeTestMethod() + + /** + * NOTE! Should be called on one thread with method under test and value constructor, + * because transactions are bound to threads + */ + fun afterTestMethod() + + /** + * Time-consuming operation that fully resets Spring context + */ + fun resetContext() +} + +data class RepositoryDescription( + val beanName: String, + val repositoryName: String, + val entityName: String, +) \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/UTSpringContextLoadingException.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/UTSpringContextLoadingException.kt new file mode 100644 index 0000000000..d8bbf2b716 --- /dev/null +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/UTSpringContextLoadingException.kt @@ -0,0 +1,11 @@ +package org.utbot.spring.api + +/** + * Used primarily to let code generation distinguish between + * parts of stack trace inside UTBot (including RD, etc.) + * and parts of stack trace inside Spring and user application. + */ +class UTSpringContextLoadingException(override val cause: Throwable) : Exception( + "Failed to load Spring application context", + cause +) diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/InstantiationSettings.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/InstantiationSettings.kt new file mode 100644 index 0000000000..ea2146a327 --- /dev/null +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/InstantiationSettings.kt @@ -0,0 +1,9 @@ +package org.utbot.spring.api.provider + +class InstantiationSettings( + val configurationClasses: Array>, + val profiles: Array, +) { + override fun toString(): String = + "InstantiationSettings(configurationClasses=${configurationClasses.contentToString()}, profiles=${profiles.contentToString()})" +} \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/SpringApiProviderFacade.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/SpringApiProviderFacade.kt new file mode 100644 index 0000000000..49b90d2aef --- /dev/null +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/SpringApiProviderFacade.kt @@ -0,0 +1,41 @@ +package org.utbot.spring.api.provider + +import org.utbot.spring.api.SpringApi + +/** + * Stateless provider of independent [SpringApi] instances that do not have shared state, + * meaning each [SpringApi] instance will when needed start its own Spring Application. + */ +interface SpringApiProviderFacade { + fun provideMostSpecificAvailableApi(instantiationSettings: InstantiationSettings): ProviderResult + + /** + * [apiUser] is consequently invoked on all available (on the classpath) + * [SpringApi] types from most specific (e.g. Spring Boot) to least specific (e.g. Pure Spring) + * until it executes without throwing exception, then obtained result is returned. + * + * All exceptions are collected into [ProviderResult.exceptions]. + */ + fun useMostSpecificNonFailingApi( + instantiationSettings: InstantiationSettings, + apiUser: (SpringApi) -> T + ): ProviderResult + + companion object { + fun getInstance(classLoader: ClassLoader): SpringApiProviderFacade = + classLoader + .loadClass("org.utbot.spring.provider.SpringApiProviderFacadeImpl") + .getConstructor() + .newInstance() as SpringApiProviderFacade + } + + /** + * [result] can be a [Result.success] while [exceptions] is not empty, + * if we failed to use most specific [SpringApi] available (e.g. SpringBoot), but + * were able to successfully fall back to less specific [SpringApi] (e.g. PureSpring). + */ + class ProviderResult( + val result: Result, + val exceptions: List + ) +} \ No newline at end of file diff --git a/utbot-spring-commons/build.gradle.kts b/utbot-spring-commons/build.gradle.kts new file mode 100644 index 0000000000..da6f100f1d --- /dev/null +++ b/utbot-spring-commons/build.gradle.kts @@ -0,0 +1,58 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer + +val springVersion: String by rootProject +val springSecurityVersion: String by rootProject +val springBootVersion: String by rootProject +val javaxVersion: String by rootProject +val jakartaVersion: String by rootProject +val rdVersion: String by rootProject + +plugins { + id("com.github.johnrengelman.shadow") version "7.1.2" + id("java") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation(project(":utbot-spring-commons-api")) + + + // these dependencies are compilieOnly, because they should + // already be present on the classpath in all utbot processes + compileOnly(project(":utbot-core")) + compileOnly("com.jetbrains.rd:rd-core:$rdVersion") { exclude(group = "org.slf4j", module = "slf4j-api") } + + + // these dependencies are compilieOnly, because they should + // be picked up from user classpath if they are present there + compileOnly("org.springframework.boot:spring-boot:$springBootVersion") + compileOnly("org.springframework.boot:spring-boot-test-autoconfigure:$springBootVersion") + compileOnly("org.springframework:spring-test:$springVersion") + compileOnly("org.springframework:spring-tx:$springVersion") + compileOnly("org.springframework:spring-web:$springVersion") + compileOnly("org.springframework.security:spring-security-test:$springSecurityVersion") + compileOnly("org.springframework.data:spring-data-commons:$springBootVersion") + + compileOnly("javax.persistence:javax.persistence-api:$javaxVersion") + compileOnly("jakarta.persistence:jakarta.persistence-api:$jakartaVersion") +} + +tasks.shadowJar { + isZip64 = true + + transform(Log4j2PluginsCacheFileTransformer::class.java) + archiveFileName.set("utbot-spring-commons-shadow.jar") +} + +val springCommonsJar: Configuration by configurations.creating { + isCanBeResolved = false + isCanBeConsumed = true +} + +artifacts { + add(springCommonsJar.name, tasks.shadowJar) +} diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt new file mode 100644 index 0000000000..9bf806e7bb --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt @@ -0,0 +1,181 @@ +package org.utbot.spring + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.warn +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.data.repository.Repository +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestContextManager +import org.utbot.common.hasOnClasspath +import org.utbot.common.patchAnnotation +import org.utbot.spring.api.SpringApi +import org.utbot.spring.api.RepositoryDescription +import org.utbot.spring.api.UTSpringContextLoadingException +import org.utbot.spring.api.provider.InstantiationSettings +import org.utbot.spring.dummy.DummySpringIntegrationTestClass +import org.utbot.spring.utils.DependencyUtils.isSpringDataOnClasspath +import org.utbot.spring.utils.RepositoryUtils +import java.lang.reflect.Method +import java.net.URLClassLoader +import kotlin.reflect.jvm.javaMethod + +private val logger = getLogger() + +class SpringApiImpl( + instantiationSettings: InstantiationSettings, + dummyTestClass: Class, +) : SpringApi { + private lateinit var dummyTestClassInstance: DummySpringIntegrationTestClass + private val dummyTestClass = dummyTestClass.also { + patchAnnotation( + annotation = it.getAnnotation(ActiveProfiles::class.java), + property = "value", + newValue = instantiationSettings.profiles + ) + patchAnnotation( + annotation = it.getAnnotation(ContextConfiguration::class.java), + property = "classes", + newValue = instantiationSettings.configurationClasses + ) + } + private val dummyTestMethod: Method = DummySpringIntegrationTestClass::dummyTestMethod.javaMethod!! + private val testContextManager: TestContextManager = TestContextManager(this.dummyTestClass) + + private val context get() = getOrLoadSpringApplicationContext() + + override fun getEntityManager(): Any = dummyTestClassInstance.entityManager + + override fun getOrLoadSpringApplicationContext() = try { + testContextManager.testContext.applicationContext as ConfigurableApplicationContext + } catch (e: Throwable) { + throw UTSpringContextLoadingException(e) + } + + override fun getBean(beanName: String): Any = context.getBean(beanName) + + override fun getDependenciesForBean(beanName: String, userSourcesClassLoader: URLClassLoader): Set { + val analyzedBeanNames = mutableSetOf() + collectDependenciesForBeanRecursively(beanName, analyzedBeanNames, userSourcesClassLoader) + return analyzedBeanNames + } + + private fun collectDependenciesForBeanRecursively( + beanName: String, + analyzedBeanNames: MutableSet, + userSourcesClassLoader: URLClassLoader, + ) { + if (beanName in analyzedBeanNames) return + + analyzedBeanNames.add(beanName) + + val dependencyBeanNames = context.beanFactory + .getDependenciesForBean(beanName) + // this filtering is applied to avoid inner beans + .filter { it in context.beanDefinitionNames } + .filter { name -> + val clazz = getBean(name)::class.java + // here immediate hierarchy is enough because proxies are inherited directly + val immediateClazzHierarchy = clazz.interfaces + clazz.superclass + clazz + immediateClazzHierarchy.any { clazz -> userSourcesClassLoader.hasOnClasspath(clazz.name) } + } + .toSet() + + dependencyBeanNames.forEach { collectDependenciesForBeanRecursively(it, analyzedBeanNames, userSourcesClassLoader) } + } + + override fun resetBean(beanName: String) { + val beanDefinitionRegistry = context.beanFactory as BeanDefinitionRegistry + + val beanDefinition = context.beanFactory.getBeanDefinition(beanName) + beanDefinitionRegistry.removeBeanDefinition(beanName) + beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinition) + } + + override fun resolveRepositories(beanNames: Set, userSourcesClassLoader: URLClassLoader): Set { + if (!isSpringDataOnClasspath) return emptySet() + val repositoryBeans = beanNames + .map { beanName -> SimpleBeanDefinition(beanName, getBean(beanName)) } + .filter { beanDef -> describesRepository(beanDef.bean) } + .toSet() + + val descriptions = mutableSetOf() + + for (repositoryBean in repositoryBeans) { + val repositoryClass = repositoryBean.bean::class.java + val repositoryClassName = repositoryClass + .interfaces + .filter { clazz -> userSourcesClassLoader.hasOnClasspath(clazz.name) } + .filter { Repository::class.java.isAssignableFrom(it) } + .map { it.name } + .firstOrNull() ?: Repository::class.java.name + + val entity = RepositoryUtils.getEntityClass(repositoryClass) + + if (entity != null) { + descriptions += RepositoryDescription( + beanName = repositoryBean.beanName, + repositoryName = repositoryClassName, + entityName = entity.name, + ) + } else { + logger.warn { + "Failed to get entity class for bean ${repositoryBean.beanName} " + + "that was recognised as a repository of type $repositoryClassName" + } + } + } + + return descriptions + } + + private var beforeTestClassCalled = false + private var isInsideTestMethod = false + + private fun beforeTestClass() { + beforeTestClassCalled = true + testContextManager.beforeTestClass() + dummyTestClassInstance = dummyTestClass.getConstructor().newInstance() + testContextManager.prepareTestInstance(dummyTestClassInstance) + } + + override fun beforeTestMethod() { + if (!beforeTestClassCalled) + beforeTestClass() + if (isInsideTestMethod) { + logger.warn { "afterTestMethod() wasn't called for previous test method, calling it from beforeTestMethod()" } + afterTestMethod() + } + testContextManager.beforeTestMethod(dummyTestClassInstance, dummyTestMethod) + testContextManager.beforeTestExecution(dummyTestClassInstance, dummyTestMethod) + isInsideTestMethod = true + } + + override fun afterTestMethod() { + if (!isInsideTestMethod) { + logger.warn { "afterTestMethod() was probably called twice, ignoring second call" } + return + } + testContextManager.afterTestExecution(dummyTestClassInstance, dummyTestMethod, null) + testContextManager.afterTestMethod(dummyTestClassInstance, dummyTestMethod, null) + isInsideTestMethod = false + } + + override fun resetContext() { + testContextManager.testContext.markApplicationContextDirty(null) + getOrLoadSpringApplicationContext() + } + + private fun describesRepository(bean: Any): Boolean = + try { + bean is Repository<*, *> + } catch (e: ClassNotFoundException) { + false + } + + data class SimpleBeanDefinition( + val beanName: String, + val bean: Any, + ) +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummyPureSpringIntegrationTestClass.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummyPureSpringIntegrationTestClass.kt new file mode 100644 index 0000000000..351472d26f --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummyPureSpringIntegrationTestClass.kt @@ -0,0 +1,8 @@ +package org.utbot.spring.dummy + +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase + +open class DummyPureSpringIntegrationTestClass : DummySpringIntegrationTestClass() + +@AutoConfigureTestDatabase +class DummyPureSpringIntegrationTestClassAutoconfigTestDB : DummyPureSpringIntegrationTestClass() diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringBootIntegrationTestClass.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringBootIntegrationTestClass.kt new file mode 100644 index 0000000000..0337e5a835 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringBootIntegrationTestClass.kt @@ -0,0 +1,21 @@ +package org.utbot.spring.dummy + +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper +import org.springframework.test.context.BootstrapWith + +@SpringBootTest +@BootstrapWith(SpringBootTestContextBootstrapper::class) +open class DummySpringBootIntegrationTestClass : DummySpringIntegrationTestClass() + +@AutoConfigureTestDatabase +class DummySpringBootIntegrationTestClassAutoconfigTestDB : DummySpringBootIntegrationTestClass() + +@AutoConfigureMockMvc +class DummySpringBootIntegrationTestClassAutoconfigMockMvc : DummySpringBootIntegrationTestClass() + +@AutoConfigureMockMvc +@AutoConfigureTestDatabase +class DummySpringBootIntegrationTestClassAutoconfigMockMvcAndTestDB : DummySpringBootIntegrationTestClass() diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringIntegrationTestClass.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringIntegrationTestClass.kt new file mode 100644 index 0000000000..43ad33bda5 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringIntegrationTestClass.kt @@ -0,0 +1,19 @@ +package org.utbot.spring.dummy + +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Transactional + +@ActiveProfiles(/* fills dynamically */) +@ContextConfiguration(/* fills dynamically */) +@Transactional(isolation = Isolation.SERIALIZABLE) +@WithMockUser +abstract class DummySpringIntegrationTestClass { + @javax.persistence.PersistenceContext + @jakarta.persistence.PersistenceContext + lateinit var entityManager: Any + + fun dummyTestMethod() {} +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/PureSpringApiProvider.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/PureSpringApiProvider.kt new file mode 100644 index 0000000000..371bb93a93 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/PureSpringApiProvider.kt @@ -0,0 +1,19 @@ +package org.utbot.spring.provider + +import org.utbot.spring.api.provider.InstantiationSettings +import org.utbot.spring.SpringApiImpl +import org.utbot.spring.dummy.DummyPureSpringIntegrationTestClass +import org.utbot.spring.dummy.DummyPureSpringIntegrationTestClassAutoconfigTestDB +import org.utbot.spring.utils.DependencyUtils.isSpringDataOnClasspath + +class PureSpringApiProvider : SpringApiProvider { + + override fun isAvailable() = true + + override fun provideAPI(instantiationSettings: InstantiationSettings) = + SpringApiImpl( + instantiationSettings, + if (isSpringDataOnClasspath) DummyPureSpringIntegrationTestClassAutoconfigTestDB::class.java + else DummyPureSpringIntegrationTestClass::class.java + ) +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt new file mode 100644 index 0000000000..66b30109a0 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt @@ -0,0 +1,11 @@ +package org.utbot.spring.provider + +import org.utbot.spring.api.SpringApi +import org.utbot.spring.api.provider.InstantiationSettings + +interface SpringApiProvider { + + fun isAvailable(): Boolean + + fun provideAPI(instantiationSettings: InstantiationSettings): SpringApi +} diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt new file mode 100644 index 0000000000..519115a310 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt @@ -0,0 +1,53 @@ +package org.utbot.spring.provider + +import com.jetbrains.rd.util.error +import org.utbot.spring.api.provider.InstantiationSettings + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.springframework.boot.SpringBootVersion +import org.springframework.core.SpringVersion +import org.utbot.spring.api.SpringApi +import org.utbot.spring.api.provider.SpringApiProviderFacade +import org.utbot.spring.api.provider.SpringApiProviderFacade.ProviderResult + +private val logger = getLogger() + +class SpringApiProviderFacadeImpl : SpringApiProviderFacade { + + override fun provideMostSpecificAvailableApi(instantiationSettings: InstantiationSettings): ProviderResult = + useMostSpecificNonFailingApi(instantiationSettings) { api -> + api.getOrLoadSpringApplicationContext() + api + } + + override fun useMostSpecificNonFailingApi( + instantiationSettings: InstantiationSettings, + apiUser: (SpringApi) -> T + ): ProviderResult { + logger.info { "Current Java version is: " + System.getProperty("java.version") } + logger.info { "Current Spring version is: " + runCatching { SpringVersion.getVersion() }.getOrNull() } + logger.info { "Current Spring Boot version is: " + runCatching { SpringBootVersion.getVersion() }.getOrNull() } + logger.info { "InstantiationSettings: $instantiationSettings" } + + val exceptions = mutableListOf() + + val apiProviders = sequenceOf(SpringBootApiProvider(), PureSpringApiProvider()) + + val result = apiProviders + .filter { apiProvider -> apiProvider.isAvailable() } + .map { apiProvider -> + logger.info { "Using Spring API from $apiProvider" } + val result = runCatching { apiUser(apiProvider.provideAPI(instantiationSettings)) } + result.onFailure { e -> + exceptions.add(e) + logger.error("Using Spring API from $apiProvider failed", e) + } + result + } + .firstOrNull { it.isSuccess } + ?: Result.failure(exceptions.first()) + + return ProviderResult(result, exceptions) + } +} diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringBootApiProvider.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringBootApiProvider.kt new file mode 100644 index 0000000000..ed28bcac4c --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringBootApiProvider.kt @@ -0,0 +1,27 @@ +package org.utbot.spring.provider + +import org.utbot.spring.api.provider.InstantiationSettings +import org.utbot.spring.dummy.DummySpringBootIntegrationTestClass +import org.utbot.spring.SpringApiImpl +import org.utbot.spring.dummy.DummySpringBootIntegrationTestClassAutoconfigMockMvc +import org.utbot.spring.dummy.DummySpringBootIntegrationTestClassAutoconfigMockMvcAndTestDB +import org.utbot.spring.dummy.DummySpringBootIntegrationTestClassAutoconfigTestDB +import org.utbot.spring.utils.DependencyUtils.isSpringBootTestOnClasspath +import org.utbot.spring.utils.DependencyUtils.isSpringDataOnClasspath +import org.utbot.spring.utils.DependencyUtils.isSpringWebOnClasspath + +class SpringBootApiProvider : SpringApiProvider { + + override fun isAvailable(): Boolean = isSpringBootTestOnClasspath + + override fun provideAPI(instantiationSettings: InstantiationSettings) = + SpringApiImpl( + instantiationSettings, + when { + isSpringDataOnClasspath && isSpringWebOnClasspath -> DummySpringBootIntegrationTestClassAutoconfigMockMvcAndTestDB::class + isSpringDataOnClasspath && !isSpringWebOnClasspath -> DummySpringBootIntegrationTestClassAutoconfigTestDB::class + !isSpringDataOnClasspath && isSpringWebOnClasspath -> DummySpringBootIntegrationTestClassAutoconfigMockMvc::class + else -> DummySpringBootIntegrationTestClass::class + }.java + ) +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/DependencyUtils.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/DependencyUtils.kt new file mode 100644 index 0000000000..c945d1b4bd --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/DependencyUtils.kt @@ -0,0 +1,28 @@ +package org.utbot.spring.utils + +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper +import org.springframework.data.repository.Repository +import org.springframework.web.bind.annotation.RequestMapping + +object DependencyUtils { + val isSpringDataOnClasspath = try { + Repository::class.java.name + true + } catch (e: Throwable) { + false + } + + val isSpringWebOnClasspath = try { + RequestMapping::class.java.name + true + } catch (e: Throwable) { + false + } + + val isSpringBootTestOnClasspath = try { + SpringBootTestContextBootstrapper::class.java.name + true + } catch (e: Throwable) { + false + } +} diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/RepositoryUtils.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/RepositoryUtils.kt new file mode 100644 index 0000000000..487f3f7690 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/RepositoryUtils.kt @@ -0,0 +1,29 @@ +package org.utbot.spring.utils + +import org.springframework.core.GenericTypeResolver +import org.springframework.data.repository.Repository + +/** + * This util class allows to obtain some data from Spring repository. + * For example, information about entity it is working with. + * + * Private methods implementation is taken from https://stackoverflow.com/a/76229273. + */ +object RepositoryUtils { + + fun getEntityClass(repositoryClass: Class<*>): Class<*>? = + getGenericType(repositoryClass, Repository::class.java, 0) + + private fun getGenericType(classInstance: Class<*>, classToGetGenerics: Class<*>, genericPosition: Int): Class<*>? { + val typeArguments = getGenericType(classInstance, classToGetGenerics) + if (typeArguments != null && typeArguments.size >= genericPosition) { + return typeArguments[genericPosition] + } + + return null + } + + private fun getGenericType(classInstance: Class<*>, classToGetGenerics: Class<*>): Array?>? { + return GenericTypeResolver.resolveTypeArguments(classInstance, classToGetGenerics) + } +} \ No newline at end of file diff --git a/utbot-spring-framework/build.gradle.kts b/utbot-spring-framework/build.gradle.kts new file mode 100644 index 0000000000..0852abd8aa --- /dev/null +++ b/utbot-spring-framework/build.gradle.kts @@ -0,0 +1,39 @@ +val kotlinLoggingVersion: String by rootProject +val rdVersion: String by rootProject +val sootVersion: String by rootProject + +val fetchSpringCommonsJar: Configuration by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false +} + +val fetchSpringAnalyzerJar: Configuration by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false +} + +dependencies { + implementation(project(":utbot-framework")) + implementation(project(":utbot-spring-commons-api")) + implementation(project(":utbot-spring-analyzer")) + + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation(group = "com.jetbrains.rd", name = "rd-core", version = rdVersion) + + implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { + exclude(group = "com.google.guava", module = "guava") + } + + fetchSpringCommonsJar(project(":utbot-spring-commons", configuration = "springCommonsJar")) + fetchSpringAnalyzerJar(project(":utbot-spring-analyzer", configuration = "springAnalyzerJar")) +} + +tasks.processResources { + from(fetchSpringCommonsJar) { + into("lib") + } + + from(fetchSpringAnalyzerJar) { + into("lib") + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/external/api/UtBotSpringApi.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/external/api/UtBotSpringApi.kt new file mode 100644 index 0000000000..0442ef1092 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/external/api/UtBotSpringApi.kt @@ -0,0 +1,66 @@ +package org.utbot.external.api + +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.context.simple.SimpleApplicationContext +import org.utbot.framework.context.spring.SpringApplicationContext +import org.utbot.framework.context.spring.SpringApplicationContextImpl +import org.utbot.framework.plugin.api.SpringConfiguration +import org.utbot.framework.plugin.api.SpringSettings +import org.utbot.framework.plugin.api.SpringTestType +import org.utbot.framework.process.SpringAnalyzerTask +import java.io.File + +object UtBotSpringApi { + private val springBootConfigAnnotations = setOf( + "org.springframework.boot.autoconfigure.SpringBootApplication", + "org.springframework.boot.SpringBootConfiguration" + ) + + /** + * NOTE: [classpath] should include project under test classpath (with all dependencies) as well as + * `spring-test`, `spring-boot-test`, and `spring-security-test` if respectively `spring-beans`, + * `spring-boot`, and `spring-security-core` are dependencies of project under test. + * + * UtBot doesn't add Spring test modules to classpath automatically to let API users control their versions. + */ + @JvmOverloads + @JvmStatic + fun createSpringApplicationContext( + springSettings: SpringSettings, + springTestType: SpringTestType, + classpath: List, + delegateContext: ApplicationContext = SimpleApplicationContext() + ): SpringApplicationContext { + if (springTestType == SpringTestType.INTEGRATION_TEST) { + require(springSettings is SpringSettings.PresentSpringSettings) { + "Integration tests can't be generated without Spring settings" + } + val configuration = springSettings.configuration + require(configuration !is SpringConfiguration.XMLConfiguration) { + "Integration tests aren't supported for XML configurations, consider using Java " + + "configuration that imports your XML configuration with @ImportResource" + } + } + return SpringApplicationContextImpl.internalCreate( + delegateContext = delegateContext, + beanDefinitions = when (springSettings) { + SpringSettings.AbsentSpringSettings -> listOf() + is SpringSettings.PresentSpringSettings -> SpringAnalyzerTask(classpath, springSettings).perform() + }, + springTestType = springTestType, + springSettings = springSettings, + ) + } + + @JvmStatic + fun createXmlSpringConfiguration(xmlConfig: File): SpringConfiguration.XMLConfiguration = + SpringConfiguration.XMLConfiguration(xmlConfig.absolutePath) + + @JvmStatic + fun createJavaSpringConfiguration(javaConfig: Class<*>): SpringConfiguration.JavaBasedConfiguration = + if (javaConfig.annotations.any { it.annotationClass.java.name in springBootConfigAnnotations }) { + SpringConfiguration.SpringBootConfiguration(javaConfig.name, isDefinitelyUnique = false) + } else { + SpringConfiguration.JavaConfiguration(javaConfig.name) + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/domain/SpringModule.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/domain/SpringModule.kt new file mode 100644 index 0000000000..8ada81ee7f --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/domain/SpringModule.kt @@ -0,0 +1,27 @@ +package org.utbot.framework.codegen.domain + +enum class SpringModule( + val testFrameworkDisplayName: String, +) { + SPRING_BEANS( + testFrameworkDisplayName = "spring-test", + ), + SPRING_BOOT( + testFrameworkDisplayName = "spring-boot-test", + ), + SPRING_SECURITY( + testFrameworkDisplayName = "spring-security-test" + ); + + var isInstalled = false + /** + * Generation Spring specific tests requires special spring test framework being installed, + * so we can use `TestContextManager` from `spring-test` to configure test context in + * spring-analyzer and to run integration tests. + */ + var testFrameworkInstalled: Boolean = false + + companion object { + val installedItems get() = values().filter { it.isInstalled } + } +} diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/MockitoAnnotationBuiltins.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/MockitoAnnotationBuiltins.kt new file mode 100644 index 0000000000..0f6bac180e --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/MockitoAnnotationBuiltins.kt @@ -0,0 +1,18 @@ +package org.utbot.framework.codegen.domain.builtin + +import org.utbot.framework.plugin.api.BuiltinClassId + +internal val mockClassId = BuiltinClassId( + canonicalName = "org.mockito.Mock", + simpleName = "Mock", +) + +internal val spyClassId = BuiltinClassId( + canonicalName = "org.mockito.Spy", + simpleName = "Spy" +) + +internal val injectMocksClassId = BuiltinClassId( + canonicalName = "org.mockito.InjectMocks", + simpleName = "InjectMocks", +) \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/generator/SpringCodeGenerator.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/generator/SpringCodeGenerator.kt new file mode 100644 index 0000000000..7aba0c939a --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/generator/SpringCodeGenerator.kt @@ -0,0 +1,69 @@ +package org.utbot.framework.codegen.generator + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.builders.SimpleTestClassModelBuilder +import org.utbot.framework.codegen.services.language.CgLanguageAssistant +import org.utbot.framework.codegen.tree.CgCustomAssertConstructor +import org.utbot.framework.codegen.tree.CgMethodConstructor +import org.utbot.framework.codegen.tree.CgSpringIntegrationTestClassConstructor +import org.utbot.framework.codegen.tree.CgSpringMethodConstructor +import org.utbot.framework.codegen.tree.CgSpringUnitTestClassConstructor +import org.utbot.framework.codegen.tree.CgSpringVariableConstructor +import org.utbot.framework.codegen.tree.CgVariableConstructor +import org.utbot.framework.codegen.tree.ututils.UtilClassKind +import org.utbot.framework.codegen.tree.withCustomAssertForMockMvcResultActions +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConcreteContextLoadingResult +import org.utbot.framework.plugin.api.SpringSettings +import org.utbot.framework.plugin.api.SpringSettings.AbsentSpringSettings +import org.utbot.framework.plugin.api.SpringSettings.PresentSpringSettings +import org.utbot.framework.plugin.api.SpringTestType + +class SpringCodeGenerator( + private val springTestType: SpringTestType, + private val springSettings: SpringSettings, + private val concreteContextLoadingResult: ConcreteContextLoadingResult?, + params: CodeGeneratorParams +) : AbstractCodeGenerator( + params.copy( + cgLanguageAssistant = object : CgLanguageAssistant by params.cgLanguageAssistant { + override fun getVariableConstructorBy(context: CgContext): CgVariableConstructor = + // TODO decorate original `params.cgLanguageAssistant.getVariableConstructorBy(context)` + CgSpringVariableConstructor(context) + + override fun getMethodConstructorBy(context: CgContext): CgMethodConstructor = + CgSpringMethodConstructor(context) + + override fun getCustomAssertConstructorBy(context: CgContext): CgCustomAssertConstructor = + params.cgLanguageAssistant.getCustomAssertConstructorBy(context) + .withCustomAssertForMockMvcResultActions() + } + ) +) { + private val classUnderTest: ClassId = params.classUnderTest + + override fun generate(testSets: List): CodeGeneratorResult { + val testClassModel = SimpleTestClassModelBuilder().createTestClassModel(classUnderTest, testSets) + + logger.info { "Code generation phase started at ${now()}" } + val astConstructor = when (springTestType) { + SpringTestType.UNIT_TEST -> CgSpringUnitTestClassConstructor(context) + SpringTestType.INTEGRATION_TEST -> + when (val settings = springSettings) { + is PresentSpringSettings -> CgSpringIntegrationTestClassConstructor(context, concreteContextLoadingResult, settings) + is AbsentSpringSettings -> error("No Spring settings were provided for Spring integration test generation.") + } + } + val testClassFile = astConstructor.construct(testClassModel) + logger.info { "Code generation phase finished at ${now()}" } + + val generatedCode = renderToString(testClassFile) + + return CodeGeneratorResult( + generatedCode = generatedCode, + utilClassKind = UtilClassKind.fromCgContextOrNull(context), + testsGenerationReport = astConstructor.testsGenerationReport, + ) + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/SpyFrameworkManager.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/SpyFrameworkManager.kt new file mode 100644 index 0000000000..841c045f07 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/SpyFrameworkManager.kt @@ -0,0 +1,12 @@ +package org.utbot.framework.codegen.services.framework + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.plugin.api.UtAssembleModel + +class SpyFrameworkManager(context: CgContext) : CgVariableConstructorComponent(context) { + + fun spyForVariable(model: UtAssembleModel){ + variableConstructor.constructAssembleForVariable(model) + } + +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractSpringTestClassConstructor.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractSpringTestClassConstructor.kt new file mode 100644 index 0000000000..9f46e6abef --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractSpringTestClassConstructor.kt @@ -0,0 +1,131 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.builtin.TestClassUtilMethodProvider +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.AnnotationTarget.Method +import org.utbot.framework.codegen.domain.models.CgClassBody +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgFrameworkUtilMethod +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodTestSet +import org.utbot.framework.codegen.domain.models.CgMethodsCluster +import org.utbot.framework.codegen.domain.models.CgRegion +import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgStaticsRegion +import org.utbot.framework.codegen.domain.models.SimpleTestClassModel +import org.utbot.framework.codegen.tree.fieldmanager.ClassFieldManagerFacade +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.util.id + +abstract class CgAbstractSpringTestClassConstructor(context: CgContext) : + CgAbstractTestClassConstructor(context) { + + protected val variableConstructor: CgSpringVariableConstructor = + CgComponents.getVariableConstructorBy(context) as CgSpringVariableConstructor + + override fun constructTestClassBody(testClassModel: SimpleTestClassModel): CgClassBody { + return buildClassBody(currentTestClass) { + + // TODO: support inner classes here + + fields += constructClassFields(testClassModel) + clearUnwantedVariableModels() + + // constructClassFields may mark fields as initialized, while they are in + // fact not initialized, so we clearAlreadyInitializedFieldModels() + variableConstructor.clearAlreadyInitializedFieldModels() + + constructAdditionalTestMethods()?.let { methodRegions += it } + + for ((testSetIndex, testSet) in testClassModel.methodTestSets.withIndex()) { + updateExecutableUnderTest(testSet.executableUnderTest) + withTestSetIdScope(testSetIndex) { + val currentMethodUnderTestRegions = constructTestSet(testSet) ?: return@withTestSetIdScope + val executableUnderTestCluster = CgMethodsCluster( + "Test suites for executable $currentExecutableUnderTest", + currentMethodUnderTestRegions + ) + methodRegions += executableUnderTestCluster + } + } + + constructAdditionalUtilMethods()?.let { methodRegions += it } + + if (currentTestClass == outerMostTestClass) { + val utilEntities = collectUtilEntities() + // If utilMethodProvider is TestClassUtilMethodProvider, then util entities should be declared + // in the test class. Otherwise, util entities will be located elsewhere (e.g. another class). + if (utilMethodProvider is TestClassUtilMethodProvider && utilEntities.isNotEmpty()) { + staticDeclarationRegions += CgStaticsRegion("Util methods", utilEntities) + } + } + } + } + + override fun constructTestSet(testSet: CgMethodTestSet): List>? { + val regions = mutableListOf>() + + if (testSet.executions.any()) { + runCatching { + createTest(testSet, regions) + }.onFailure { e -> processFailure(testSet, e) } + } + + val errors = testSet.allErrors + if (errors.isNotEmpty()) { + regions += methodConstructor.errorMethod(testSet, errors) + testsGenerationReport.addMethodErrors(testSet, errors) + } + + return if (regions.any()) regions else null + } + + abstract fun constructClassFields(testClassModel: SimpleTestClassModel): List + + /** + * Here "additional" means that these tests are not obtained from + * [UtExecution]s generated by engine or fuzzer, but have another origin. + */ + open fun constructAdditionalTestMethods(): CgMethodsCluster? = null + + open fun constructAdditionalUtilMethods(): CgMethodsCluster? = null + + + /** + * Clears the results of variable instantiations that occurred + * when we create class variables with specific annotations. + * Actually, only mentioned variables should be stored in `valueByModelId`. + * + * This is a kind of HACK. + * It is better to distinguish creating variable by model with all + * related side effects and just creating a variable definition, + * but it will take very long time to do it now. + */ + private fun clearUnwantedVariableModels() { + val trustedListOfModels = ClassFieldManagerFacade(context).findTrustedModels() + + valueByUtModelWrapper + .filterNot { it.key in trustedListOfModels } + .forEach { valueByUtModelWrapper.remove(it.key) } + } + + protected fun constructBeforeMethod(statements: List): CgFrameworkUtilMethod { + val beforeAnnotation = addAnnotation(context.testFramework.beforeMethodId, Method) + return CgFrameworkUtilMethod( + name = "setUp", + statements = statements, + exceptions = emptySet(), + annotations = listOf(beforeAnnotation), + ) + } + + protected fun constructAfterMethod(statements: List): CgFrameworkUtilMethod { + val afterAnnotation = addAnnotation(context.testFramework.afterMethodId, Method) + return CgFrameworkUtilMethod( + name = "tearDown", + statements = statements, + exceptions = setOf(Exception::class.id), + annotations = listOf(afterAnnotation), + ) + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMockMvcResultActionsAssertConstructor.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMockMvcResultActionsAssertConstructor.kt new file mode 100644 index 0000000000..963eabdf3b --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMockMvcResultActionsAssertConstructor.kt @@ -0,0 +1,51 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.services.access.CgCallableAccessManager +import org.utbot.framework.codegen.services.access.CgCallableAccessManagerImpl +import org.utbot.framework.codegen.tree.CgComponents.getStatementConstructorBy +import org.utbot.framework.plugin.api.UtCustomModel +import org.utbot.framework.plugin.api.UtSpringMockMvcResultActionsModel +import org.utbot.framework.plugin.api.util.SpringModelUtils.contentMatchersStringMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.mockMvcResultHandlersClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.mockMvcResultMatchersClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.resultActionsAndDoMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.resultActionsAndExpectMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.resultHandlersPrintMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.resultMatchersContentMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.resultMatchersStatusMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.resultMatchersViewMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.statusMatchersIsMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.viewMatchersNameMethodId + +fun CgCustomAssertConstructor.withCustomAssertForMockMvcResultActions() = + CgMockMvcResultActionsAssertConstructor(context, this) + +class CgMockMvcResultActionsAssertConstructor( + context: CgContext, + private val delegateAssertConstructor: CgCustomAssertConstructor +) : CgCustomAssertConstructor by delegateAssertConstructor, + CgContextOwner by context, + CgStatementConstructor by getStatementConstructorBy(context), + CgCallableAccessManager by CgCallableAccessManagerImpl(context) { + override fun tryConstructCustomAssert(expected: UtCustomModel, actual: CgVariable): Boolean { + if (expected is UtSpringMockMvcResultActionsModel) { + +actual[resultActionsAndDoMethodId](mockMvcResultHandlersClassId[resultHandlersPrintMethodId]()) + +actual[resultActionsAndExpectMethodId]( + mockMvcResultMatchersClassId[resultMatchersStatusMethodId]()[statusMatchersIsMethodId](expected.status) + ) + expected.viewName?.let { viewName -> + +actual[resultActionsAndExpectMethodId]( + mockMvcResultMatchersClassId[resultMatchersViewMethodId]()[viewMatchersNameMethodId](viewName) + ) + } + +actual[resultActionsAndExpectMethodId]( + mockMvcResultMatchersClassId[resultMatchersContentMethodId]()[contentMatchersStringMethodId](expected.contentAsString) + ) + return true + } else + return delegateAssertConstructor.tryConstructCustomAssert(expected, actual) + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringIntegrationTestClassConstructor.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringIntegrationTestClassConstructor.kt new file mode 100644 index 0000000000..7dbcfce6ac --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringIntegrationTestClassConstructor.kt @@ -0,0 +1,247 @@ +package org.utbot.framework.codegen.tree + +import mu.KotlinLogging +import org.utbot.common.tryLoadClass +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.TestNg +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.* +import org.utbot.framework.codegen.domain.models.AnnotationTarget.* +import org.utbot.framework.codegen.domain.models.CgTestMethodType.FAILING +import org.utbot.framework.codegen.domain.models.CgTestMethodType.SUCCESSFUL +import org.utbot.framework.codegen.tree.fieldmanager.CgAutowiredFieldsManager +import org.utbot.framework.codegen.tree.fieldmanager.CgPersistenceContextFieldsManager +import org.utbot.framework.codegen.util.escapeControlChars +import org.utbot.framework.codegen.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConcreteContextLoadingResult +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.SpringSettings.* +import org.utbot.framework.plugin.api.SpringConfiguration.* +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.UtSpringEntityManagerModel +import org.utbot.framework.plugin.api.mapper.UtModelDeepMapper +import org.utbot.framework.plugin.api.mapper.UtModelDeepMapper.Companion.collectAllModels +import org.utbot.framework.plugin.api.util.IndentUtil.TAB +import org.utbot.framework.plugin.api.util.SpringModelUtils +import org.utbot.framework.plugin.api.util.SpringModelUtils.activeProfilesClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.autoConfigureTestDbClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.bootstrapWithClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.contextConfigurationClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassModeClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.extendWithClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.flushMethodIdOrNull +import org.utbot.framework.plugin.api.util.SpringModelUtils.mockMvcClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.repositoryClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.runWithClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.springBootTestClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.springBootTestContextBootstrapperClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.springExtensionClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.springRunnerClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.transactionalClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.withMockUserClassId +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.spring.api.UTSpringContextLoadingException + +class CgSpringIntegrationTestClassConstructor( + context: CgContext, + private val concreteContextLoadingResult: ConcreteContextLoadingResult?, + private val springSettings: PresentSpringSettings, +) : CgAbstractSpringTestClassConstructor(context) { + + private val autowiredFieldManager = CgAutowiredFieldsManager(context) + private val persistenceContextFieldsManager = CgPersistenceContextFieldsManager.createIfPossible(context) + + companion object { + private val logger = KotlinLogging.logger {} + } + + override fun construct(testClassModel: SimpleTestClassModel): CgClassFile = super.construct( + flushMethodIdOrNull?.let { flushMethodId -> + testClassModel.mapStateBeforeModels { UtModelDeepMapper { model -> + shallowlyAddFlushes(model, flushMethodId) + } } + } ?: testClassModel + ) + + private fun shallowlyAddFlushes(model: UtModel, flushMethodId: MethodId): UtModel = + when (model) { + is UtAssembleModel -> model.copy( + modificationsChain = model.modificationsChain.flatMap { modification -> + if (modification.instance is UtSpringEntityManagerModel) + listOf( + modification, + UtExecutableCallModel( + instance = UtSpringEntityManagerModel(), + executable = flushMethodId, + params = emptyList() + ) + ) + else listOf(modification) + } + ) + else -> model + } + + override fun constructTestClass(testClassModel: SimpleTestClassModel): CgClass { + addNecessarySpringSpecificAnnotations(testClassModel) + return super.constructTestClass(testClassModel) + } + + override fun constructClassFields(testClassModel: SimpleTestClassModel): List { + val autowiredFields = autowiredFieldManager.createFieldDeclarations(testClassModel) + val persistentContextFields = persistenceContextFieldsManager?.createFieldDeclarations(testClassModel) + .orEmpty() + + return autowiredFields + persistentContextFields + } + + override fun constructAdditionalTestMethods() = + CgMethodsCluster.withoutDocs( + listOfNotNull(constructContextLoadsMethod()) + ) + + private fun constructContextLoadsMethod() : CgTestMethod { + if (concreteContextLoadingResult == null) + logger.error { "Missing contextLoadingResult" } + val exception = concreteContextLoadingResult?.exceptions?.firstOrNull() + return CgTestMethod( + name = "contextLoads", + statements = listOfNotNull( + exception?.let { e -> constructFailedContextLoadingTraceComment(e) }, + if (concreteContextLoadingResult == null) CgSingleLineComment("Error: context loading result from concrete execution is missing") else null + ), + annotations = listOf(addAnnotation(context.testFramework.testAnnotationId, Method)), + documentation = CgDocumentationComment(listOf( + CgDocRegularLineStmt("This sanity check test fails if the application context cannot start.") + ) + exception?.let { constructFailedContextLoadingDocComment() }.orEmpty()), + type = if (concreteContextLoadingResult != null && exception == null) SUCCESSFUL else FAILING + ) + } + + private fun constructFailedContextLoadingDocComment() = listOf( + CgDocRegularLineStmt("

    "), + CgDocRegularLineStmt("Context loading throws an exception."), + CgDocRegularLineStmt("Please try to fix your context or environment configuration."), + CgDocRegularLineStmt("Spring configuration applied: ${springSettings.configuration.fullDisplayName}."), + ) + + private fun constructFailedContextLoadingTraceComment(exception: Throwable) = CgMultilineComment( + exception + .stackTraceToString() + .lines() + .let { lines -> + if (exception is UTSpringContextLoadingException) lines.dropWhile { !it.contains("Caused") } + else lines + } + .mapIndexed { i, line -> + if (i == 0) "Failure ${line.replace("Caused", "caused")}" + else TAB + line + } + .map { it.escapeControlChars() } + ) + + private fun addNecessarySpringSpecificAnnotations(testClassModel: SimpleTestClassModel) { + val isSpringBootTestAccessible = utContext.classLoader.tryLoadClass(springBootTestClassId.name) != null + if (isSpringBootTestAccessible) { + addAnnotation(springBootTestClassId, Class) + } + + val (testFrameworkExtension, springExtension) = when (testFramework) { + Junit4 -> runWithClassId to springRunnerClassId + Junit5 -> extendWithClassId to springExtensionClassId + TestNg -> error("Spring extension is not implemented in TestNg") + else -> error("Trying to generate tests for Spring project with non-JVM framework") + } + + // @SpringBootTest contains @ExtendWith(SpringExtension.class), no need to add it manually + if (!isSpringBootTestAccessible || testFrameworkExtension != extendWithClassId) { + addAnnotation( + classId = testFrameworkExtension, + argument = createGetClassExpression(springExtension, codegenLanguage), + target = Class, + ) + } + + if (utContext.classLoader.tryLoadClass(springBootTestContextBootstrapperClassId.name) != null) + // TODO in somewhat new versions of Spring Boot, @SpringBootTest + // already includes @BootstrapWith(SpringBootTestContextBootstrapper.class), + // so we should avoid adding it manually to reduce number of annotations + addAnnotation( + classId = bootstrapWithClassId, + argument = createGetClassExpression(springBootTestContextBootstrapperClassId, codegenLanguage), + target = Class, + ) + + val defaultProfileIsUsed = springSettings.profiles.singleOrNull() == "default" + if (!defaultProfileIsUsed) { + addAnnotation( + classId = activeProfilesClassId, + namedArguments = listOf( + CgNamedAnnotationArgument( + name = "profiles", + value = CgArrayAnnotationArgument(springSettings.profiles.map { profile -> profile.resolve() }) + ) + ), + target = Class, + ) + } + + // TODO: we support only JavaBasedConfiguration in integration tests. + // Adapt for XMLConfigurations when supported. + val configClass = springSettings.configuration as JavaBasedConfiguration + if (configClass is JavaConfiguration || configClass is SpringBootConfiguration && !configClass.isDefinitelyUnique) { + addAnnotation( + classId = contextConfigurationClassId, + namedArguments = listOf( + CgNamedAnnotationArgument( + name = "classes", + value = CgArrayAnnotationArgument( + listOf( + createGetClassExpression( + ClassId(configClass.configBinaryName), + codegenLanguage + ) + ) + ) + ) + ), + target = Class, + ) + } + + + addAnnotation( + classId = dirtiesContextClassId, + namedArguments = listOf( + CgNamedAnnotationArgument( + name = "classMode", + value = CgEnumConstantAccess(dirtiesContextClassModeClassId, "BEFORE_EACH_TEST_METHOD") + ), + ), + target = Class, + ) + + if (utContext.classLoader.tryLoadClass(transactionalClassId.name) != null) + addAnnotation(transactionalClassId, Class) + + // `@AutoConfigureTestDatabase` can itself be on the classpath, while spring-data + // (i.e. module containing `Repository`) is not. + // + // If we add `@AutoConfigureTestDatabase` without having spring-data, + // generated tests will fail with `ClassNotFoundException: org.springframework.dao.DataAccessException`. + if (utContext.classLoader.tryLoadClass(repositoryClassId.name) != null) + addAnnotation(autoConfigureTestDbClassId, Class) + + val allStateBeforeModels = collectAllModels { collector -> testClassModel.mapStateBeforeModels { collector } } + if (allStateBeforeModels.any { it.classId == mockMvcClassId }) + addAnnotation(SpringModelUtils.autoConfigureMockMvcClassId, Class) + + if (utContext.classLoader.tryLoadClass(withMockUserClassId.name) != null) + addAnnotation(withMockUserClassId, Class) + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringMethodConstructor.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringMethodConstructor.kt new file mode 100644 index 0000000000..60aa678b41 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringMethodConstructor.kt @@ -0,0 +1,27 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.util.SpringModelUtils.mockMvcPerformMethodId +import org.utbot.framework.plugin.api.util.SpringModelUtils.nestedServletExceptionClassIds +import org.utbot.framework.plugin.api.util.id + +class CgSpringMethodConstructor(context: CgContext) : CgMethodConstructor(context) { + override fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean = + !isNestedServletException(exception) && super.shouldTestPassWithException(execution, exception) + + override fun collectNeededStackTraceLines( + exception: Throwable, + executableToStartCollectingFrom: ExecutableId + ): List = + // `mockMvc.perform` wraps exceptions from user code into NestedServletException, so we unwrap them back + exception.takeIf { + executableToStartCollectingFrom == mockMvcPerformMethodId && isNestedServletException(it) + }?.cause?.let { cause -> + super.collectNeededStackTraceLines(cause, currentExecutableUnderTest!!) + } ?: super.collectNeededStackTraceLines(exception, executableToStartCollectingFrom) + + private fun isNestedServletException(exception: Throwable): Boolean = + exception::class.java.id in nestedServletExceptionClassIds +} diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringUnitTestClassConstructor.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringUnitTestClassConstructor.kt new file mode 100644 index 0000000000..f60627035e --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringUnitTestClassConstructor.kt @@ -0,0 +1,109 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.builtin.closeMethodId +import org.utbot.framework.codegen.domain.builtin.openMocksMethodId +import org.utbot.framework.codegen.domain.builtin.clearMethodId +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgAssignment +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgMethodsCluster +import org.utbot.framework.codegen.domain.models.CgStatementExecutableCall +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.domain.models.SimpleTestClassModel +import org.utbot.framework.codegen.tree.fieldmanager.CgInjectingMocksFieldsManager +import org.utbot.framework.codegen.tree.fieldmanager.CgMockedFieldsManager +import org.utbot.framework.codegen.tree.fieldmanager.CgSpiedFieldsManager +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.objectClassId + +class CgSpringUnitTestClassConstructor(context: CgContext) : CgAbstractSpringTestClassConstructor(context) { + + private var additionalMethodsRequired: Boolean = false + private lateinit var mockitoCloseableVariable: CgValue + private lateinit var spyClearVariables: List + + private val mocksFieldsManager = CgMockedFieldsManager(context) + private val spiesFieldsManager = CgSpiedFieldsManager(context) + private val injectingMocksFieldsManager = + CgInjectingMocksFieldsManager(context, mocksFieldsManager, spiesFieldsManager) + + override fun constructClassFields(testClassModel: SimpleTestClassModel): List { + val fields = mutableListOf() + + val spiesFields = spiesFieldsManager.createFieldDeclarations(testClassModel) + val mockedFields = mocksFieldsManager.createFieldDeclarations(testClassModel) + + if ((spiesFields + mockedFields).isNotEmpty()) { + val injectingMocksFields = injectingMocksFieldsManager.createFieldDeclarations(testClassModel) + + fields += injectingMocksFields + fields += mockedFields + fields += spiesFields + fields += constructMockitoCloseables() + + additionalMethodsRequired = true + spyClearVariables = spiesFields.map { it.declaration.variable } + } + + return fields + } + + override fun constructAdditionalUtilMethods(): CgMethodsCluster? { + if (!additionalMethodsRequired) return null + + importIfNeeded(openMocksMethodId) + + val openMocksCall = CgMethodCall( + caller = null, + executableId = openMocksMethodId, + //TODO: this is a hack of this + arguments = listOf(CgVariable("this", objectClassId)) + ) + + val closeCall = CgMethodCall( + caller = mockitoCloseableVariable, + executableId = closeMethodId, + arguments = emptyList(), + ) + + val clearSpyModelCalls = spyClearVariables.map { spyVariable -> + CgMethodCall( + caller = spyVariable, + executableId = clearMethodId(spyVariable.type.jClass), + arguments = emptyList() + ) + } + + val openMocksStatement = CgAssignment(mockitoCloseableVariable, openMocksCall) + val closeStatement = CgStatementExecutableCall(closeCall) + val clearSpyModels = clearSpyModelCalls.map { CgStatementExecutableCall(it) } + + return CgMethodsCluster.withoutDocs( + listOf( + constructBeforeMethod(listOf(openMocksStatement)), + constructAfterMethod(clearSpyModels + listOf(closeStatement)), + ) + ) + } + + private fun constructMockitoCloseables(): CgFieldDeclaration { + val mockitoCloseableVarName = "mockitoCloseable" + val mockitoCloseableVarType = java.lang.AutoCloseable::class.id + + val mockitoCloseableModel = UtCompositeModel( + id = null, + classId = mockitoCloseableVarType, + isMock = false, + ) + + mockitoCloseableVariable = + variableConstructor.getOrCreateVariable(mockitoCloseableModel, mockitoCloseableVarName) + val mockitoCloseableDeclaration = CgDeclaration(mockitoCloseableVarType, mockitoCloseableVarName, initializer = null) + return CgFieldDeclaration(ownerClassId = currentTestClass, mockitoCloseableDeclaration) + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringVariableConstructor.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringVariableConstructor.kt new file mode 100644 index 0000000000..2ca542e6c6 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringVariableConstructor.kt @@ -0,0 +1,44 @@ +package org.utbot.framework.codegen.tree + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.tree.fieldmanager.ClassFieldManagerFacade +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtSpringContextModel +import org.utbot.framework.plugin.api.UtSpringEntityManagerModel +import org.utbot.framework.plugin.api.util.stringClassId + +class CgSpringVariableConstructor(context: CgContext) : CgVariableConstructor(context) { + + private val fieldManagerFacade = ClassFieldManagerFacade(context) + + fun clearAlreadyInitializedFieldModels() { + fieldManagerFacade.clearAlreadyInitializedModels() + } + + override fun getOrCreateVariable(model: UtModel, name: String?): CgValue { + val variable = fieldManagerFacade.constructVariableForField(model) + + variable?.let { return it } + + return when (model) { + is UtSpringContextModel, is UtSpringEntityManagerModel -> createDummyVariable(model) + else -> super.getOrCreateVariable(model, name) + } + } + + private fun createDummyVariable(model: UtModel): CgVariable { + // This is a kind of HACK + // Actually, this value is not supposed to be used in the generated code. + // However, this value existence is required for fields declaration process. + val dummyVariable = newVar(model.classId) { + CgLiteral(stringClassId, "dummy") + } + + valueByUtModelWrapper[model.wrap()] = dummyVariable + + return dummyVariable + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgAbstractClassFieldManager.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgAbstractClassFieldManager.kt new file mode 100644 index 0000000000..4a7accda5e --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgAbstractClassFieldManager.kt @@ -0,0 +1,100 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.framework.codegen.domain.UtModelWrapper +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.AnnotationTarget +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.domain.models.builders.TypedModelWrappers +import org.utbot.framework.codegen.tree.CgComponents +import org.utbot.framework.codegen.tree.CgSpringVariableConstructor +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtModel + +abstract class CgAbstractClassFieldManager(context: CgContext) : + CgClassFieldManager, + CgContextOwner by context { + + val annotatedModels: MutableSet = mutableSetOf() + protected val modelGroupsProvider = ModelGroupsProvider(context) + + fun findCgValueByModel(model: UtModel, setOfModels: Set?): CgValue? { + val key = setOfModels?.find { it == model.wrap() } ?: return null + return valueByUtModelWrapper[key] + } + + protected abstract fun fieldWithAnnotationIsRequired(classId: ClassId): Boolean + + protected fun constructFieldsWithAnnotation(modelWrappers: Set): List { + val groupedModelsByClassId = modelWrappers.groupByClassId() + val annotation = statementConstructor.addAnnotation(annotationType, AnnotationTarget.Field) + + val constructedDeclarations = mutableListOf() + for ((classId, modelWrappers) in groupedModelsByClassId) { + + val modelWrapper = modelWrappers.firstOrNull() ?: continue + val model = modelWrapper.model + + val fieldWithAnnotationIsRequired = fieldWithAnnotationIsRequired(model.classId) + if (!fieldWithAnnotationIsRequired) { + continue + } + + val baseVarName = constructBaseVarName(model) + + /* + * `withNameScope` is used to avoid saving names for sub-models of model. + * + * Different models from different executions may have the same id. + * Therefore, when creating a variable for a new class field and using existing `currentTestSetId` and `currentExecutionId`, + * field`s `model.wrap()` may match one of the values in `annotatedModels`. + * To avoid false matches when creating a variable for a new class field, `withTestSetIdScope(-1)` and `withExecutionIdScope(-1)` are used. + */ + val createdVariable = withNameScope { + withTestSetIdScope(-1) { + withExecutionIdScope(-1) { + variableConstructor.getOrCreateVariable(model, baseVarName) as? CgVariable + ?: error("`CgVariable` cannot be constructed from a $model model") + } + } + } + existingVariableNames.add(createdVariable.name) + + val declaration = CgDeclaration(classId, variableName = createdVariable.name, initializer = null) + + constructedDeclarations += CgFieldDeclaration( + ownerClassId = currentTestClass, + declaration, + annotation + ) + + modelWrappers.forEach { modelWrapper -> + valueByUtModelWrapper[modelWrapper] = createdVariable + annotatedModels += modelWrapper + } + } + + return constructedDeclarations + } + + protected open fun constructBaseVarName(model: UtModel): String? = nameGenerator.nameFrom(model.classId) + + private fun Set.groupByClassId(): TypedModelWrappers { + val classModels = mutableMapOf>() + + for (modelGroup in this.groupBy { it.model.classId }) { + classModels[modelGroup.key] = modelGroup.value.toSet() + } + + return classModels + } + + protected val variableConstructor: CgSpringVariableConstructor by lazy { + CgComponents.getVariableConstructorBy(context) as CgSpringVariableConstructor + } + protected val nameGenerator = CgComponents.getNameGeneratorBy(context) + protected val statementConstructor = CgComponents.getStatementConstructorBy(context) +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgAutowiredFieldsManager.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgAutowiredFieldsManager.kt new file mode 100644 index 0000000000..be9c29e1d6 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgAutowiredFieldsManager.kt @@ -0,0 +1,42 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.TestClassModel +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.util.SpringModelUtils +import org.utbot.framework.plugin.api.util.SpringModelUtils.getBeanNameOrNull +import org.utbot.framework.plugin.api.util.SpringModelUtils.isAutowiredFromContext + +class CgAutowiredFieldsManager(context: CgContext) : CgAbstractClassFieldManager(context) { + init { + relevantFieldManagers += this + } + + override val annotationType = SpringModelUtils.autowiredClassId + override fun createFieldDeclarations(testClassModel: TestClassModel): List { + val modelsByOrigin = modelGroupsProvider.collectModelsByOrigin(testClassModel) + val autowiredFromContextModels = modelsByOrigin + .stateBeforeDependentModels + .filterTo(HashSet()) { it.model.isAutowiredFromContext() } + + return constructFieldsWithAnnotation(autowiredFromContextModels) + } + + override fun useVariableForModel(model: UtModel, variable: CgValue) { + when { + model.isAutowiredFromContext() -> { + variableConstructor.constructAssembleForVariable(model as UtAssembleModel) + } + + else -> error("Trying to autowire model $model but it is not appropriate") + } + } + + override fun constructBaseVarName(model: UtModel): String? = model.getBeanNameOrNull() + + override fun fieldWithAnnotationIsRequired(classId: ClassId): Boolean = true +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgClassFieldManager.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgClassFieldManager.kt new file mode 100644 index 0000000000..8212d57d24 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgClassFieldManager.kt @@ -0,0 +1,18 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.TestClassModel +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtModel + +interface CgClassFieldManager : CgContextOwner { + + val annotationType: ClassId + + fun createFieldDeclarations(testClassModel: TestClassModel): List + + fun useVariableForModel(model: UtModel, variable: CgValue) +} + diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgInjectingMocksFieldsManager.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgInjectingMocksFieldsManager.kt new file mode 100644 index 0000000000..2e8a14740d --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgInjectingMocksFieldsManager.kt @@ -0,0 +1,57 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.framework.codegen.domain.builtin.injectMocksClassId +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.TestClassModel +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtModelWithCompositeOrigin +import org.utbot.framework.plugin.api.isMockModel + +class CgInjectingMocksFieldsManager( + val context: CgContext, + private val mocksFieldsManager: CgMockedFieldsManager, + private val spiesFieldsManager: CgSpiedFieldsManager, + ) : CgAbstractClassFieldManager(context) { + init { + relevantFieldManagers += this + } + + override val annotationType = injectMocksClassId + + override fun createFieldDeclarations(testClassModel: TestClassModel): List { + val modelsByOrigin = modelGroupsProvider.collectModelsByOrigin(testClassModel) + return constructFieldsWithAnnotation(modelsByOrigin.thisInstanceModels) + } + + override fun useVariableForModel(model: UtModel, variable: CgValue) { + val modelFields = when (model) { + is UtCompositeModel -> model.fields + is UtModelWithCompositeOrigin -> model.origin?.fields + else -> null + } + + modelFields?.forEach { (fieldId, fieldModel) -> + // creating variables for modelVariable fields + val variableForField = variableConstructor.getOrCreateVariable(fieldModel) + + // is variable mocked by @Mock annotation + val isMocked = findCgValueByModel(fieldModel, mocksFieldsManager.annotatedModels) != null + + // is variable spied by @Spy annotation + val isSpied = findCgValueByModel(fieldModel, spiesFieldsManager.annotatedModels) != null + + // If field model is a mock model and is mocked by @Mock annotation in classFields or is spied by @Spy annotation, + // it is set in the connected with instance under test automatically via @InjectMocks. + // Otherwise, we need to set this field manually. + if ((!fieldModel.isMockModel() || !isMocked) && !isSpied) { + variableConstructor.setFieldValue(variable, fieldId, variableForField) + } + } + } + + override fun fieldWithAnnotationIsRequired(classId: ClassId): Boolean = true +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgMockedFieldsManager.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgMockedFieldsManager.kt new file mode 100644 index 0000000000..b9a3e81dc5 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgMockedFieldsManager.kt @@ -0,0 +1,53 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.framework.codegen.domain.builtin.mockClassId +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.domain.models.TestClassModel +import org.utbot.framework.codegen.tree.CgComponents +import org.utbot.framework.codegen.tree.fieldmanager.MockitoInjectionUtils.canBeInjectedByTypeInto +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.isMockModel + +class CgMockedFieldsManager(context: CgContext) : CgAbstractClassFieldManager(context) { + init { + relevantFieldManagers += this + } + + override val annotationType = mockClassId + override fun createFieldDeclarations(testClassModel: TestClassModel): List { + val modelsByOrigin = modelGroupsProvider.collectModelsByOrigin(testClassModel) + + val dependentMockModels = + modelsByOrigin.thisInstanceDependentModels + .filterTo(mutableSetOf()) { cgModel -> + cgModel.model.isMockModel() && cgModel !in modelsByOrigin.thisInstanceModels + } + + return constructFieldsWithAnnotation(dependentMockModels) + } + + private val mockFrameworkManager = CgComponents.getMockFrameworkManagerBy(context) + override fun useVariableForModel(model: UtModel, variable: CgValue) { + if (!model.isMockModel()) { + error("$model does not represent a mock") + } + + mockFrameworkManager.createMockForVariable( + model as UtCompositeModel, + variable as CgVariable, + ) + + for ((fieldId, fieldModel) in model.fields) { + val variableForField = variableConstructor.getOrCreateVariable(fieldModel) + variableConstructor.setFieldValue(variable, fieldId, variableForField) + } + } + + override fun fieldWithAnnotationIsRequired(classId: ClassId): Boolean = + classId.canBeInjectedByTypeInto(classUnderTest) +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgPersistenceContextFieldsManager.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgPersistenceContextFieldsManager.kt new file mode 100644 index 0000000000..83bd944dad --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgPersistenceContextFieldsManager.kt @@ -0,0 +1,43 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.TestClassModel +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtSpringEntityManagerModel +import org.utbot.framework.plugin.api.util.SpringModelUtils + +class CgPersistenceContextFieldsManager private constructor( + context: CgContext, + override val annotationType: ClassId, +) : CgAbstractClassFieldManager(context) { + + init { + relevantFieldManagers += this + } + + companion object { + fun createIfPossible(context: CgContext): CgPersistenceContextFieldsManager? = SpringModelUtils.persistenceContextClassIds.firstOrNull() + ?.let { persistenceContextClassId -> CgPersistenceContextFieldsManager(context, persistenceContextClassId) } + } + + override fun createFieldDeclarations(testClassModel: TestClassModel): List { + val modelsByOrigin = modelGroupsProvider.collectModelsByOrigin(testClassModel) + val entityManagerModels = modelsByOrigin + .stateBeforeDependentModels + .filterTo(HashSet()) { it.model is UtSpringEntityManagerModel } + + return constructFieldsWithAnnotation(entityManagerModels) + } + + override fun useVariableForModel(model: UtModel, variable: CgValue) { + return when(model) { + is UtSpringEntityManagerModel -> {} + else -> error("Trying to use @PersistenceContext for model $model but it is not appropriate") + } + } + + override fun fieldWithAnnotationIsRequired(classId: ClassId): Boolean = true +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgSpiedFieldsManager.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgSpiedFieldsManager.kt new file mode 100644 index 0000000000..6c37ff3602 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/CgSpiedFieldsManager.kt @@ -0,0 +1,88 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.framework.codegen.domain.UtModelWrapper +import org.utbot.framework.codegen.domain.builtin.spyClassId +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgFieldDeclaration +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.TestClassModel +import org.utbot.framework.codegen.services.framework.SpyFrameworkManager +import org.utbot.framework.codegen.tree.fieldmanager.MockitoInjectionUtils.canBeInjectedByTypeInto +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.canBeSpied +import org.utbot.framework.plugin.api.isMockModel +import org.utbot.framework.plugin.api.spiedTypes +import org.utbot.framework.plugin.api.util.jClass + +class CgSpiedFieldsManager(context: CgContext) : CgAbstractClassFieldManager(context) { + + init { + relevantFieldManagers += this + } + + override val annotationType = spyClassId + + override fun createFieldDeclarations(testClassModel: TestClassModel): List { + val modelsByOrigin = modelGroupsProvider.collectModelsByOrigin(testClassModel) + + val dependentMockModels = + modelsByOrigin.thisInstanceDependentModels + .filterTo(mutableSetOf()) { cgModel -> + cgModel.model.isMockModel() && cgModel !in modelsByOrigin.thisInstanceModels + } + + val dependentSpyModels = + modelsByOrigin.thisInstanceDependentModels + .filterTo(mutableSetOf()) { cgModel -> + cgModel.model.canBeSpied() && + cgModel !in modelsByOrigin.thisInstanceModels && + cgModel !in dependentMockModels + } + + val suitableSpyModels = getSuitableSpyModels(dependentSpyModels) + return constructFieldsWithAnnotation(suitableSpyModels) + } + + private val spyFrameworkManager = SpyFrameworkManager(context) + + override fun useVariableForModel(model: UtModel, variable: CgValue) { + if (!model.canBeSpied()) { + error("$model does not represent a spy") + } + spyFrameworkManager.spyForVariable( + model as UtAssembleModel, + ) + } + + override fun fieldWithAnnotationIsRequired(classId: ClassId): Boolean = + classId.canBeInjectedByTypeInto(classUnderTest) + + override fun constructBaseVarName(model: UtModel): String = super.constructBaseVarName(model) + "Spy" + + private fun getSuitableSpyModels(potentialSpyModels: MutableSet): Set = + spiedTypes.fold(setOf()) { spyModels, type -> + spyModels + getSuitableSpyModelsOfType(type, potentialSpyModels) + } + + /* + * Filters out cases when different tests use different [clazz] + * implementations and hence we need to inject different types. + * + * This limitation is reasoned by @InjectMocks behaviour. + * Otherwise, injection may be misleading: + * for example, several spies may be injected into one field. + */ + private fun getSuitableSpyModelsOfType( + clazz: Class<*>, + potentialSpyModels: MutableSet + ): Set { + val spyModelsAssignableFrom = potentialSpyModels + .filter { clazz.isAssignableFrom(it.model.classId.jClass) } + .toSet() + val spyModelsTypesCount = spyModelsAssignableFrom.map { it.model.classId }.toSet().size + + return if (spyModelsTypesCount == 1) spyModelsAssignableFrom else emptySet() + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/ClassFieldManagerFacade.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/ClassFieldManagerFacade.kt new file mode 100644 index 0000000000..3b61b4a403 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/ClassFieldManagerFacade.kt @@ -0,0 +1,51 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.common.getOrPut +import org.utbot.framework.codegen.domain.UtModelWrapper +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.context.CgContextProperty +import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtSpringContextModel + +object RelevantFieldManagersProperty : CgContextProperty> + +/** + * Managers to process annotated fields of the class under test + * relevant for the current generation type. + */ +val CgContextOwner.relevantFieldManagers: MutableList + get() = properties.getOrPut(RelevantFieldManagersProperty) { mutableListOf() } + +class ClassFieldManagerFacade(context: CgContext) : CgContextOwner by context { + + private val alreadyInitializedModels = mutableSetOf() + + fun clearAlreadyInitializedModels() { + alreadyInitializedModels.clear() + } + + fun constructVariableForField(model: UtModel): CgValue? { + relevantFieldManagers.forEach { manager -> + val alreadyCreatedVariable = manager.findCgValueByModel(model, manager.annotatedModels) + if (alreadyCreatedVariable != null) { + if (alreadyInitializedModels.add(model.wrap())) + manager.useVariableForModel(model, alreadyCreatedVariable) + return alreadyCreatedVariable + } + } + + return null + } + + fun findTrustedModels(): List { + val trustedModels = mutableListOf(UtSpringContextModel.wrap()) + + relevantFieldManagers.forEach { manager -> + trustedModels += manager.annotatedModels + } + + return trustedModels + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/MockitoInjectionUtils.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/MockitoInjectionUtils.kt new file mode 100644 index 0000000000..5f5176257b --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/MockitoInjectionUtils.kt @@ -0,0 +1,14 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.allDeclaredFieldIds +import org.utbot.framework.plugin.api.util.isSubtypeOf + +object MockitoInjectionUtils { + /* + * If count of fields of the same type is 1, then we mock/spy variable by @Mock/@Spy annotation, + * otherwise we will create this variable by simple variable constructor. + */ + fun ClassId.canBeInjectedByTypeInto(classToInjectInto: ClassId): Boolean = + classToInjectInto.allDeclaredFieldIds.filter { isSubtypeOf(it.type) }.toList().size == 1 +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/ModelGroupsProvider.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/ModelGroupsProvider.kt new file mode 100644 index 0000000000..7b42558f12 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/fieldmanager/ModelGroupsProvider.kt @@ -0,0 +1,128 @@ +package org.utbot.framework.codegen.tree.fieldmanager + +import org.utbot.framework.codegen.domain.UtModelWrapper +import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.TestClassModel +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtClassRefModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtCustomModel +import org.utbot.framework.plugin.api.UtDirectSetFieldModel +import org.utbot.framework.plugin.api.UtEnumConstantModel +import org.utbot.framework.plugin.api.UtLambdaModel +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.UtStatementCallModel +import org.utbot.framework.plugin.api.UtVoidModel + +data class ModelsByOrigin( + val thisInstanceModels: Set, + val thisInstanceDependentModels: Set, + val stateBeforeDependentModels: Set, +) + +class ModelGroupsProvider(val context: CgContext): CgContextOwner by context { + + private val modelsCache = mutableMapOf() + + fun collectModelsByOrigin(testClassModel: TestClassModel): ModelsByOrigin { + if (testClassModel in modelsCache) { + return modelsCache[testClassModel]!! + } + + val thisInstanceModels = mutableSetOf() + val thisInstancesDependentModels = mutableSetOf() + val stateBeforeDependentModels = mutableSetOf() + + for ((testSetIndex, testSet) in testClassModel.methodTestSets.withIndex()) { + withTestSetIdScope(testSetIndex) { + for ((executionIndex, execution) in testSet.executions.withIndex()) { + withExecutionIdScope(executionIndex) { + setOf(execution.stateBefore.thisInstance, execution.stateAfter.thisInstance) + .filterNotNull() + .forEach { model -> + thisInstanceModels += model.wrap() + thisInstancesDependentModels += collectImmediateDependentModels( + model, + skipModificationChains = true + ) + } + + (execution.stateBefore.parameters + execution.stateBefore.thisInstance) + .filterNotNull() + .forEach { model -> stateBeforeDependentModels += collectRecursively(model) } + } + } + } + } + + val modelsByOrigin = ModelsByOrigin(thisInstanceModels, thisInstancesDependentModels, stateBeforeDependentModels) + modelsCache[testClassModel] = modelsByOrigin + + return modelsByOrigin + } + + private fun collectImmediateDependentModels(model: UtModel, skipModificationChains: Boolean): Set { + val dependentModels = mutableSetOf() + + when (model) { + is UtNullModel, + is UtPrimitiveModel, + is UtClassRefModel, + is UtVoidModel, + is UtEnumConstantModel, + is UtCustomModel -> {} + is UtLambdaModel -> { + model.capturedValues.forEach { dependentModels.add(it.wrap()) } + } + is UtArrayModel -> { + model.stores.values.forEach { dependentModels.add(it.wrap()) } + if (model.stores.count() < model.length) { + dependentModels.add(model.constModel.wrap()) + } + } + is UtCompositeModel -> { + // Here we traverse fields only. + // Traversing mocks as well will result in wrong models playing + // a role of class fields with @Mock annotation. + model.fields.forEach { (_, model) -> dependentModels.add(model.wrap()) } + } + is UtAssembleModel -> { + model.instantiationCall.instance?.let { dependentModels.add(it.wrap()) } + model.instantiationCall.params.forEach { dependentModels.add(it.wrap()) } + + if(!skipModificationChains) { + model.modificationsChain.forEach { stmt -> + stmt.instance?.let { dependentModels.add(it.wrap()) } + when (stmt) { + is UtStatementCallModel -> stmt.params.forEach { dependentModels.add(it.wrap()) } + is UtDirectSetFieldModel -> dependentModels.add(stmt.fieldModel.wrap()) + } + } + } + } + } + + return dependentModels + } + + private fun collectRecursively(model: UtModel): Set { + val allDependentModels = mutableSetOf() + + collectRecursively(model, allDependentModels) + + return allDependentModels + } + + private fun collectRecursively(model: UtModel, allDependentModels: MutableSet){ + if(!allDependentModels.add(model.wrap())){ + return + } + collectImmediateDependentModels(model, skipModificationChains = false).forEach { + collectRecursively(it.model, allDependentModels) + } + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContext.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContext.kt new file mode 100644 index 0000000000..458c9db3f6 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContext.kt @@ -0,0 +1,25 @@ +package org.utbot.framework.context.spring + +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.plugin.api.BeanDefinitionData +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConcreteContextLoadingResult +import org.utbot.framework.plugin.api.SpringSettings + +/** + * Data we get from Spring application context + * to manage engine and code generator behaviour. + */ +interface SpringApplicationContext : ApplicationContext { + val springSettings: SpringSettings + + /** + * Describes bean definitions (bean name, type, some optional additional data) + */ + val beanDefinitions: List + val injectedTypes: Set + val allInjectedSuperTypes: Set + + var concreteContextLoadingResult: ConcreteContextLoadingResult? + fun getBeansAssignableTo(classId: ClassId): List +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContextImpl.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContextImpl.kt new file mode 100644 index 0000000000..bec479112e --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContextImpl.kt @@ -0,0 +1,211 @@ +package org.utbot.framework.context.spring + +import mu.KotlinLogging +import org.utbot.common.dynamicPropertiesOf +import org.utbot.common.isAbstract +import org.utbot.common.isStatic +import org.utbot.common.withValue +import org.utbot.external.api.UtBotSpringApi +import org.utbot.framework.UtSettings +import org.utbot.framework.codegen.generator.AbstractCodeGenerator +import org.utbot.framework.codegen.generator.CodeGeneratorParams +import org.utbot.framework.codegen.generator.SpringCodeGenerator +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.context.NonNullSpeculator +import org.utbot.framework.context.TypeReplacer +import org.utbot.framework.context.custom.CoverageFilteringConcreteExecutionContext +import org.utbot.framework.context.custom.RerunningConcreteExecutionContext +import org.utbot.framework.context.custom.useMocks +import org.utbot.framework.context.utils.transformInstrumentationFactory +import org.utbot.framework.context.utils.transformJavaFuzzingContext +import org.utbot.framework.context.utils.transformValueProvider +import org.utbot.framework.plugin.api.BeanDefinitionData +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConcreteContextLoadingResult +import org.utbot.framework.plugin.api.SpringSettings +import org.utbot.framework.plugin.api.SpringTestType +import org.utbot.framework.plugin.api.util.SpringModelUtils.entityClassIds +import org.utbot.framework.plugin.api.util.allSuperTypes +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.fuzzing.spring.JavaLangObjectValueProvider +import org.utbot.fuzzing.spring.FuzzedTypeFlag +import org.utbot.fuzzing.spring.addProperties +import org.utbot.fuzzing.spring.decorators.replaceTypes +import org.utbot.fuzzing.spring.properties +import org.utbot.fuzzing.spring.unit.InjectMockValueProvider +import org.utbot.fuzzing.toFuzzerType +import org.utbot.instrumentation.instrumentation.execution.RemovingConstructFailsUtExecutionInstrumentation + +class SpringApplicationContextImpl private constructor( + private val delegateContext: ApplicationContext, + override val beanDefinitions: List, + private val springTestType: SpringTestType, + override val springSettings: SpringSettings, +): ApplicationContext by delegateContext, SpringApplicationContext { + companion object { + private val logger = KotlinLogging.logger {} + + /** + * Used internally by UtBot to create an instance of [SpringApplicationContextImpl] + * when [beanDefinitions] are already known. + * + * NOTE: Bean definitions defined in config from [springSettings] are IGNORED. + * + * API users should use [UtBotSpringApi.createSpringApplicationContext] + */ + fun internalCreate( + delegateContext: ApplicationContext, + beanDefinitions: List, + springTestType: SpringTestType, + springSettings: SpringSettings, + ) = SpringApplicationContextImpl(delegateContext, beanDefinitions, springTestType, springSettings) + } + + private object ReplacedFuzzedTypeFlag : FuzzedTypeFlag + + override val typeReplacer: TypeReplacer = SpringTypeReplacer(delegateContext.typeReplacer, this) + override val nonNullSpeculator: NonNullSpeculator = SpringNonNullSpeculator(delegateContext.nonNullSpeculator, this) + + override var concreteContextLoadingResult: ConcreteContextLoadingResult? = null + + override fun createConcreteExecutionContext( + fullClasspath: String, + classpathWithoutDependencies: String + ): ConcreteExecutionContext { + var delegateConcreteExecutionContext = delegateContext.createConcreteExecutionContext( + fullClasspath, + classpathWithoutDependencies + ).transformValueProvider { valueProvider -> + valueProvider.with(JavaLangObjectValueProvider( + classesToTryUsingAsJavaLangObject = listOf(objectClassId, classUnderTest) + )) + } + + // to avoid filtering out all coverage, we only filter + // coverage when `classpathWithoutDependencies` is provided + // (e.g. when we are launched from IDE plugin) + if (classpathWithoutDependencies.isNotEmpty()) + delegateConcreteExecutionContext = CoverageFilteringConcreteExecutionContext( + delegateContext = delegateConcreteExecutionContext, + classpathToIncludeCoverageFrom = classpathWithoutDependencies, + annotationsToIgnoreCoverage = entityClassIds.toSet(), + keepOriginalCoverageOnEmptyFilteredCoverage = true + ) + + return when (springTestType) { + SpringTestType.UNIT_TEST -> delegateConcreteExecutionContext.transformJavaFuzzingContext { fuzzingContext -> + fuzzingContext + .useMocks { type -> + ReplacedFuzzedTypeFlag !in type.properties && + mockStrategy.eligibleToMock( + classToMock = type.classId, + classUnderTest = fuzzingContext.classUnderTest + ) + } + .transformValueProvider { origValueProvider -> + InjectMockValueProvider( + idGenerator = fuzzingContext.idGenerator, + classUnderTest = fuzzingContext.classUnderTest, + isFieldNonNull = { fieldId -> + nonNullSpeculator.speculativelyCannotProduceNullPointerException(fieldId, classUnderTest) + }, + ) + .withFallback(origValueProvider) + .replaceTypes { description, type -> + typeReplacer.replaceTypeIfNeeded(type.classId) + ?.let { replacementClassId -> + // TODO infer generic type of replacement + val replacement = + if (type.classId == replacementClassId) type + else toFuzzerType(replacementClassId.jClass, description.typeCache) + replacement.addProperties( + dynamicPropertiesOf(ReplacedFuzzedTypeFlag.withValue(Unit)) + ) + } ?: type + } + } + } + SpringTestType.INTEGRATION_TEST -> + RerunningConcreteExecutionContext( + SpringIntegrationTestConcreteExecutionContext( + delegateConcreteExecutionContext, + classpathWithoutDependencies, + springApplicationContext = this + ), + maxRerunsPerMethod = UtSettings.maxSpringContextResetsPerMethod + ) + }.transformInstrumentationFactory { delegateInstrumentationFactory -> + RemovingConstructFailsUtExecutionInstrumentation.Factory(delegateInstrumentationFactory) + } + } + + override fun createCodeGenerator(params: CodeGeneratorParams): AbstractCodeGenerator = + // TODO decorate original `delegateContext.createCodeGenerator(params)` + SpringCodeGenerator( + springTestType = springTestType, + springSettings = springSettings, + concreteContextLoadingResult = concreteContextLoadingResult, + params = params, + ) + + override fun getBeansAssignableTo(classId: ClassId): List = beanDefinitions.filter { beanDef -> + // some bean classes may fail to load + runCatching { + val beanClass = ClassId(beanDef.beanTypeName).jClass + classId.jClass.isAssignableFrom(beanClass) + }.getOrElse { false } + } + + // Classes representing concrete types that are actually used in Spring application + override val injectedTypes: Set + get() { + if (!areAllInjectedSuperTypesInitialized) { + for (beanTypeName in beanDefinitions.map { it.beanTypeName }) { + try { + val beanClass = utContext.classLoader.loadClass(beanTypeName) + if (!beanClass.isAbstract && !beanClass.isInterface && + !beanClass.isLocalClass && (!beanClass.isMemberClass || beanClass.isStatic)) { + _injectedTypes += beanClass.id + } + } catch (e: Throwable) { + // For some Spring beans (e.g. with anonymous classes) + // it is possible to have problems with classes loading. + when (e) { + is ClassNotFoundException, is NoClassDefFoundError, is IllegalAccessError -> + logger.warn { "Failed to load bean class for $beanTypeName (${e.message})" } + + else -> throw e + } + } + } + + // This is done to be sure that this storage is not empty after the first class loading iteration. + // So, even if all loaded classes were filtered out, we will not try to load them again. + areAllInjectedSuperTypesInitialized = true + } + + return _injectedTypes + } + + override val allInjectedSuperTypes: Set + get() { + if (!areInjectedTypesInitialized) { + _allInjectedSuperTypes = injectedTypes.flatMap { it.allSuperTypes() }.toSet() + areInjectedTypesInitialized = true + } + + return _allInjectedSuperTypes + } + + // the following properties help to imitate `by lazy` behaviour, do not use them directly + // (we can't use actual `by lazy` because communication via RD breaks it) + private var _allInjectedSuperTypes: Set = emptySet() + private var areAllInjectedSuperTypesInitialized : Boolean = false + + private val _injectedTypes = mutableSetOf() + private var areInjectedTypesInitialized: Boolean = false +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestConcreteExecutionContext.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestConcreteExecutionContext.kt new file mode 100644 index 0000000000..12fc787d9b --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestConcreteExecutionContext.kt @@ -0,0 +1,94 @@ +package org.utbot.framework.context.spring + +import mu.KotlinLogging +import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.context.ConcreteExecutionContext.FuzzingContextParams +import org.utbot.framework.context.JavaFuzzingContext +import org.utbot.framework.plugin.api.ConcreteContextLoadingResult +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.SpringSettings +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.isSuccess +import org.utbot.framework.plugin.api.util.SpringModelUtils +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.getRelevantSpringRepositories +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation +import org.utbot.instrumentation.instrumentation.spring.SpringUtExecutionInstrumentation +import org.utbot.instrumentation.tryLoadingSpringContext +import java.io.File + +class SpringIntegrationTestConcreteExecutionContext( + private val delegateContext: ConcreteExecutionContext, + classpathWithoutDependencies: String, + private val springApplicationContext: SpringApplicationContext, +) : ConcreteExecutionContext by delegateContext { + private val springSettings = (springApplicationContext.springSettings as? SpringSettings.PresentSpringSettings) ?: + error("Integration tests cannot be generated without Spring configuration") + + companion object { + private val logger = KotlinLogging.logger {} + } + + override val instrumentationFactory: UtExecutionInstrumentation.Factory<*> = + SpringUtExecutionInstrumentation.Factory( + delegateContext.instrumentationFactory, + springSettings, + springApplicationContext.beanDefinitions, + buildDirs = classpathWithoutDependencies.split(File.pathSeparator) + .map { File(it).toURI().toURL() } + .toTypedArray(), + ) + + override fun loadContext( + concreteExecutor: ConcreteExecutor, + ): ConcreteContextLoadingResult = + delegateContext.loadContext(concreteExecutor).andThen { + springApplicationContext.concreteContextLoadingResult ?: concreteExecutor.tryLoadingSpringContext().also { + springApplicationContext.concreteContextLoadingResult = it + } + } + + override fun tryCreateFuzzingContext(params: FuzzingContextParams): JavaFuzzingContext { + if (springApplicationContext.getBeansAssignableTo(params.classUnderTest).isEmpty()) + error( + "No beans of type ${params.classUnderTest} are found. " + + "Try choosing different Spring configuration or adding beans to " + + springSettings.configuration.fullDisplayName + ) + + val relevantRepositories = params.concreteExecutor.getRelevantSpringRepositories(params.classUnderTest) + logger.info { "Detected relevant repositories for class ${params.classUnderTest}: $relevantRepositories" } + + return SpringIntegrationTestJavaFuzzingContext( + delegateContext = delegateContext.tryCreateFuzzingContext(params), + relevantRepositories = relevantRepositories, + springApplicationContext = springApplicationContext, + fuzzingStartTimeMillis = params.fuzzingStartTimeMillis, + fuzzingEndTimeMillis = params.fuzzingEndTimeMillis, + ) + } + + override fun transformExecutionsBeforeMinimization( + executions: List, + methodUnderTest: ExecutableId + ): List { + val (mockMvcExecutions, regularExecutions) = + delegateContext.transformExecutionsBeforeMinimization(executions, methodUnderTest) + .partition { it.executableToCall == SpringModelUtils.mockMvcPerformMethodId } + + val classUnderTestName = methodUnderTest.classId.name + val methodUnderTestSignature = methodUnderTest.signature + + fun UtExecution.getMethodUnderTestCoverage(): List? = + coverage?.coveredInstructions?.filter { + it.className == classUnderTestName && it.methodSignature == methodUnderTestSignature + }?.map { it.id } + + fun UtExecution.getKey() = getMethodUnderTestCoverage()?.let { it to result.isSuccess } + + val mockMvcExecutionKeys = mockMvcExecutions.mapNotNullTo(mutableSetOf()) { it.getKey() } + + return mockMvcExecutions + regularExecutions.filter { it.getKey() !in mockMvcExecutionKeys } + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestJavaFuzzingContext.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestJavaFuzzingContext.kt new file mode 100644 index 0000000000..0b22966f15 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestJavaFuzzingContext.kt @@ -0,0 +1,144 @@ +package org.utbot.framework.context.spring + +import mu.KotlinLogging +import org.utbot.common.tryLoadClass +import org.utbot.framework.context.JavaFuzzingContext +import org.utbot.framework.fuzzer.IdentityPreservingIdGenerator +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.EnvironmentModels +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.SpringRepositoryId +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtSpringMockMvcResultActionsModel +import org.utbot.framework.plugin.api.util.SpringModelUtils +import org.utbot.framework.plugin.api.util.SpringModelUtils.allControllerParametersAreSupported +import org.utbot.framework.plugin.api.util.allDeclaredFieldIds +import org.utbot.framework.plugin.api.util.jField +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.providers.AnyDepthNullValueProvider +import org.utbot.fuzzing.spring.decorators.ModifyingWithMethodsProviderWrapper +import org.utbot.fuzzing.providers.ObjectValueProvider +import org.utbot.fuzzing.spring.GeneratedFieldValueProvider +import org.utbot.fuzzing.spring.SpringBeanValueProvider +import org.utbot.fuzzing.spring.decorators.preserveProperties +import org.utbot.fuzzing.spring.valid.EmailValueProvider +import org.utbot.fuzzing.spring.valid.NotBlankStringValueProvider +import org.utbot.fuzzing.spring.valid.NotEmptyStringValueProvider +import org.utbot.fuzzing.spring.valid.ValidEntityValueProvider +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult + +class SpringIntegrationTestJavaFuzzingContext( + private val delegateContext: JavaFuzzingContext, + relevantRepositories: Set, + springApplicationContext: SpringApplicationContext, + private val fuzzingStartTimeMillis: Long, + private val fuzzingEndTimeMillis: Long, +) : JavaFuzzingContext by delegateContext { + companion object { + private val logger = KotlinLogging.logger {} + } + + private val springBeanValueProvider: JavaValueProvider = + SpringBeanValueProvider( + idGenerator, + beanNameProvider = { classId -> + springApplicationContext.getBeansAssignableTo(classId).map { it.beanName } + }, + relevantRepositories = relevantRepositories + ) + + override val valueProvider: JavaValueProvider = + springBeanValueProvider.withModifyingMethodsBuddy() + .withFallback(ValidEntityValueProvider(idGenerator, onlyAcceptWhenValidIsRequired = true).withModifyingMethodsBuddy()) + .withFallback(EmailValueProvider()) + .withFallback(NotBlankStringValueProvider()) + .withFallback(NotEmptyStringValueProvider()) + .withFallback( + delegateContext.valueProvider + .with(ObjectValueProvider(idGenerator).withModifyingMethodsBuddy()) + .with(ValidEntityValueProvider(idGenerator, onlyAcceptWhenValidIsRequired = false).withModifyingMethodsBuddy()) + .with(createGeneratedFieldValueProviders(relevantRepositories, idGenerator)) + .withFallback(AnyDepthNullValueProvider) + ) + .preserveProperties() + + private fun JavaValueProvider.withModifyingMethodsBuddy(): JavaValueProvider = + with(modifyingMethodsBuddy(this)) + + private fun modifyingMethodsBuddy(provider: JavaValueProvider): JavaValueProvider = + ModifyingWithMethodsProviderWrapper(classUnderTest, provider) + + + private fun createGeneratedFieldValueProviders( + relevantRepositories: Set, + idGenerator: IdentityPreservingIdGenerator + ): JavaValueProvider { + val generatedValueAnnotationClasses = SpringModelUtils.generatedValueClassIds.mapNotNull { + @Suppress("UNCHECKED_CAST") // type system fails to understand that @GeneratedValue is indeed an annotation + utContext.classLoader.tryLoadClass(it.name) as Class? + } + + val generatedValueFields = + relevantRepositories + .flatMap { springRepositoryId -> + val entityClassId = springRepositoryId.entityClassId + entityClassId.allDeclaredFieldIds + .filter { fieldId -> generatedValueAnnotationClasses.any { fieldId.jField.isAnnotationPresent(it) } } + .map { entityClassId to it } + } + + logger.info { "Detected @GeneratedValue fields: $generatedValueFields" } + + return ValueProvider.of(generatedValueFields.map { (entityClassId, fieldId) -> + GeneratedFieldValueProvider(idGenerator, entityClassId, fieldId) + }) + } + + private val methodsSuccessfullyCalledViaMockMvc = mutableSetOf() + + override fun createStateBefore( + thisInstance: UtModel?, + parameters: List, + statics: Map, + executableToCall: ExecutableId + ): EnvironmentModels { + val delegateStateBefore = delegateContext.createStateBefore(thisInstance, parameters, statics, executableToCall) + return when (executableToCall) { + is ConstructorId -> delegateStateBefore + is MethodId -> { + val requestBuilderModel = SpringModelUtils.createRequestBuilderModelOrNull( + methodId = executableToCall, + arguments = parameters, + idGenerator = { idGenerator.createId() } + )?.takeIf { + val halfOfFuzzingTimePassed = System.currentTimeMillis() > (fuzzingStartTimeMillis + fuzzingEndTimeMillis) / 2 + val allParamsSupported = allControllerParametersAreSupported(executableToCall) + val successfullyCalledViaMockMvc = executableToCall in methodsSuccessfullyCalledViaMockMvc + !halfOfFuzzingTimePassed || allParamsSupported && successfullyCalledViaMockMvc + } ?: return delegateStateBefore + + delegateStateBefore.copy( + thisInstance = SpringModelUtils.createMockMvcModel(controller = thisInstance) { idGenerator.createId() }, + parameters = listOf(requestBuilderModel), + executableToCall = SpringModelUtils.mockMvcPerformMethodId, + ) + } + } + } + + override fun handleFuzzedConcreteExecutionResult( + methodUnderTest: ExecutableId, + concreteExecutionResult: UtConcreteExecutionResult + ) { + delegateContext.handleFuzzedConcreteExecutionResult(methodUnderTest, concreteExecutionResult) + ((concreteExecutionResult.result as? UtExecutionSuccess)?.model as? UtSpringMockMvcResultActionsModel)?.let { + if (it.status < 400) + methodsSuccessfullyCalledViaMockMvc.add(methodUnderTest) + } + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringNonNullSpeculator.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringNonNullSpeculator.kt new file mode 100644 index 0000000000..4bd44f01a2 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringNonNullSpeculator.kt @@ -0,0 +1,114 @@ +package org.utbot.framework.context.spring + +import mu.KotlinLogging +import org.utbot.framework.context.NonNullSpeculator +import org.utbot.framework.context.spring.SpringNonNullSpeculator.InjectionKind.* +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.SpringModelUtils.autowiredClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.componentClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.injectClassIds +import org.utbot.framework.plugin.api.util.allDeclaredFieldIds +import org.utbot.framework.plugin.api.util.executable +import org.utbot.framework.plugin.api.util.fieldId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.jField +import org.utbot.framework.plugin.api.util.jFieldOrNull +import org.utbot.modifications.ExecutableAnalyzer +import soot.SootField +import java.lang.reflect.AnnotatedElement + +class SpringNonNullSpeculator( + private val delegateNonNullSpeculator: NonNullSpeculator, + private val springApplicationContext: SpringApplicationContext +) : NonNullSpeculator { + private val executableAnalyzer = ExecutableAnalyzer() + + companion object { + private val logger = KotlinLogging.logger {} + } + + override fun speculativelyCannotProduceNullPointerException( + field: SootField, + classUnderTest: ClassId, + ): Boolean = + ((field.jFieldOrNull?.let { it.fieldId in getInjectedFields(it.declaringClass.id) } ?: false) || + delegateNonNullSpeculator.speculativelyCannotProduceNullPointerException(field, classUnderTest)) + + override fun speculativelyCannotProduceNullPointerException(field: FieldId, classUnderTest: ClassId): Boolean = + (field in getInjectedFields(field.declaringClass) || + delegateNonNullSpeculator.speculativelyCannotProduceNullPointerException(field, classUnderTest)) + + private val injectedFieldsCache = mutableMapOf>() + + /** + * As described in [this issue](https://github.com/UnitTestBot/UTBotJava/issues/2589) we should consider + * `FieldType fieldName` to be non-`null`, iff one of the following statements holds: + * - the field itself is annotated with `@Autowired(required=true)`, `@Autowired` or `@Inject` + * - all the following statements hold: + * - one of the following statements hold: + * - there’s a constructor or a method annotated with `@Autowired(required=true)`, `@Autowired` or `@Inject` + * - there’s only one constructor defined in the class, said constructor is not annotated with + * `@Autowired(required=false)`, while the class itself is annotated with `@Component`-like annotation + * (either `@Component` itself or other annotation that is itself annotated with `@Component`) + * - said constructor/method accepts a parameter of type `FieldType` (or its subtype) + * - said parameter is not annotated with `@Nullable` nor with `@Autowired(required=false)` + * - said constructor/method contains an assignment of said parameter to `fieldName` + */ + private fun getInjectedFields(classId: ClassId): Set = injectedFieldsCache.getOrPut(classId) { + try { + (classId.allDeclaredFieldIds.filter { it.jField.getInjectionAnnotationKind() == INJECTED_AND_REQUIRED } + + classId.getInjectingExecutables().flatMap { injectingExecutable -> + executableAnalyzer.analyze(injectingExecutable).params + .map { (paramIdx, fieldId) -> injectingExecutable.executable.parameters[paramIdx] to fieldId } + .filter { (param, fieldId) -> + fieldId.type.jClass.isAssignableFrom(param.type) && + param.annotations.none { + it.annotationClass.simpleName?.endsWith("Nullable") ?: false + } && + param.getInjectionAnnotationKind() != INJECTED_BUT_NOT_REQUIRED + } + .map { (_, fieldId) -> fieldId } + }).toSet().also { + logger.debug { "Injected fields for $classId: $it" } + } + } catch (e: Throwable) { + logger.warn(e) { "Failed to determine injected fields for class $classId" } + emptySet() + } + } + + private fun ClassId.getInjectingExecutables(): Sequence { + return (allConstructors + allMethods).filter { + it.executable.getInjectionAnnotationKind() == INJECTED_AND_REQUIRED + } + listOfNotNull(allConstructors.singleOrNull()?.takeIf { constructor -> + isBeanDefiningClass() && constructor.executable.getInjectionAnnotationKind() == NOT_INJECTED + }) + } + + private fun ClassId.isBeanDefiningClass(): Boolean = + this in springApplicationContext.injectedTypes || jClass.annotations.any { it.isComponentLike() } + + private fun Annotation.isComponentLike() = + annotationClass.id == componentClassId || + annotationClass.annotations.any { it.annotationClass.id == componentClassId } + + private fun AnnotatedElement.getInjectionAnnotationKind(): InjectionKind { + if(annotations.any { it.annotationClass.id in injectClassIds }) + return INJECTED_AND_REQUIRED + val autowiredAnnotation = annotations.firstOrNull { it.annotationClass.id == autowiredClassId } + ?: return NOT_INJECTED + return if (autowiredAnnotation.annotationClass.java.getMethod("required").invoke(autowiredAnnotation) as Boolean) + INJECTED_AND_REQUIRED + else + INJECTED_BUT_NOT_REQUIRED + } + + private enum class InjectionKind { + NOT_INJECTED, + INJECTED_BUT_NOT_REQUIRED, + INJECTED_AND_REQUIRED, + } +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringTypeReplacer.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringTypeReplacer.kt new file mode 100644 index 0000000000..b8a9e7de77 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringTypeReplacer.kt @@ -0,0 +1,23 @@ +package org.utbot.framework.context.spring + +import org.utbot.framework.context.TypeReplacer +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.TypeReplacementMode +import org.utbot.framework.plugin.api.util.isAbstract +import org.utbot.framework.plugin.api.util.isSubtypeOf + +class SpringTypeReplacer( + private val delegateTypeReplacer: TypeReplacer, + private val springApplicationContext: SpringApplicationContext +) : TypeReplacer { + override val typeReplacementMode: TypeReplacementMode = + if (springApplicationContext.beanDefinitions.isNotEmpty() || + delegateTypeReplacer.typeReplacementMode == TypeReplacementMode.KnownImplementor) + TypeReplacementMode.KnownImplementor + else + TypeReplacementMode.NoImplementors + + override fun replaceTypeIfNeeded(classId: ClassId): ClassId? = + if (classId.isAbstract) springApplicationContext.injectedTypes.singleOrNull { it.isSubtypeOf(classId) } + else delegateTypeReplacer.replaceTypeIfNeeded(classId) +} \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/SpringDependencyPatterns.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/SpringDependencyPatterns.kt new file mode 100644 index 0000000000..8f18fda134 --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/SpringDependencyPatterns.kt @@ -0,0 +1,77 @@ +package org.utbot.framework.plugin.api.utils + +import org.utbot.framework.codegen.domain.SpringModule +import org.utbot.framework.codegen.domain.SpringModule.* + +fun SpringModule.patterns(): Patterns { + val moduleLibraryPatterns = when (this) { + SPRING_BOOT -> springBootModulePatterns + SPRING_BEANS -> springBeansModulePatterns + SPRING_SECURITY -> springSecurityModulePatterns + } + val libraryPatterns = when (this) { + SPRING_BOOT -> springBootPatterns + SPRING_BEANS -> springBeansPatterns + SPRING_SECURITY -> springSecurityPatterns + } + + return Patterns(moduleLibraryPatterns, libraryPatterns) +} + +fun SpringModule.testPatterns(): Patterns { + val moduleLibraryPatterns = when (this) { + SPRING_BOOT -> springBootTestModulePatterns + SPRING_BEANS -> springBeansTestModulePatterns + SPRING_SECURITY -> springSecurityTestModulePatterns + } + val libraryPatterns = when (this) { + SPRING_BOOT -> springBootTestPatterns + SPRING_BEANS -> springBeansTestPatterns + SPRING_SECURITY -> springSecurityTestPatterns + } + + return Patterns(moduleLibraryPatterns, libraryPatterns) +} + +val SPRING_BEANS_JAR_PATTERN = Regex("spring-beans-([0-9]+)(\\.[0-9]+){1,2}") +val SPRING_BEANS_MVN_PATTERN = Regex("org\\.springframework:spring-beans:([0-9]+)(\\.[0-9]+){1,2}") +val springBeansPatterns = listOf(SPRING_BEANS_JAR_PATTERN, SPRING_BEANS_MVN_PATTERN) + +val SPRING_BEANS_BASIC_MODULE_PATTERN = Regex("spring-beans") +val springBeansModulePatterns = listOf(SPRING_BEANS_BASIC_MODULE_PATTERN) + +val SPRING_BEANS_TEST_JAR_PATTERN = Regex("spring-test-([0-9]+)(\\.[0-9]+){1,2}") +val SPRING_BEANS_TEST_MVN_PATTERN = Regex("org\\.springframework:spring-test:([0-9]+)(\\.[0-9]+){1,2}") +val springBeansTestPatterns = listOf(SPRING_BEANS_TEST_JAR_PATTERN, SPRING_BEANS_TEST_MVN_PATTERN) + +val SPRING_BEANS_TEST_BASIC_MODULE_PATTERN = Regex("spring-test") +val springBeansTestModulePatterns = listOf(SPRING_BEANS_TEST_BASIC_MODULE_PATTERN) + +val SPRING_BOOT_JAR_PATTERN = Regex("spring-boot-([0-9]+)(\\.[0-9]+){1,2}") +val SPRING_BOOT_MVN_PATTERN = Regex("org\\.springframework\\.boot:spring-boot:([0-9]+)(\\.[0-9]+){1,2}") +val springBootPatterns = listOf(SPRING_BOOT_JAR_PATTERN, SPRING_BOOT_MVN_PATTERN) + +val SPRING_BOOT_BASIC_MODULE_PATTERN = Regex("spring-boot") +val springBootModulePatterns = listOf(SPRING_BOOT_BASIC_MODULE_PATTERN) + +val SPRING_BOOT_TEST_JAR_PATTERN = Regex("spring-boot-test-([0-9]+)(\\.[0-9]+){1,2}") +val SPRING_BOOT_TEST_MVN_PATTERN = Regex("org\\.springframework\\.boot:spring-boot-test:([0-9]+)(\\.[0-9]+){1,2}") + +val springBootTestPatterns = listOf(SPRING_BOOT_TEST_JAR_PATTERN, SPRING_BOOT_TEST_MVN_PATTERN) + +val SPRING_BOOT_TEST_BASIC_MODULE_PATTERN = Regex("spring-boot-test") +val springBootTestModulePatterns = listOf(SPRING_BOOT_TEST_BASIC_MODULE_PATTERN) + +val SPRING_SECURITY_JAR_PATTERN = Regex("spring-security-core-([0-9]+)(\\.[0-9]+){1,2}") +val SPRING_SECURITY_MVN_PATTERN = Regex("org\\.springframework\\.security:spring-security-core:([0-9]+)(\\.[0-9]+){1,2}") +val springSecurityPatterns = listOf(SPRING_SECURITY_JAR_PATTERN, SPRING_SECURITY_MVN_PATTERN) + +val SPRING_SECURITY_BASIC_MODULE_PATTERN = Regex("spring-security-core") +val springSecurityModulePatterns = listOf(SPRING_SECURITY_BASIC_MODULE_PATTERN) + +val SPRING_SECURITY_TEST_JAR_PATTERN = Regex("spring-security-test-([0-9]+)(\\.[0-9]+){1,2}") +val SPRING_SECURITY_TEST_MVN_PATTERN = Regex("org\\.springframework\\.security:spring-security-test:([0-9]+)(\\.[0-9]+){1,2}") +val springSecurityTestPatterns = listOf(SPRING_SECURITY_TEST_JAR_PATTERN, SPRING_SECURITY_TEST_MVN_PATTERN) + +val SPRING_SECURITY_TEST_BASIC_MODULE_PATTERN = Regex("spring-security-test") +val springSecurityTestModulePatterns = listOf(SPRING_SECURITY_TEST_BASIC_MODULE_PATTERN) \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/process/SpringAnalyzerTask.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/process/SpringAnalyzerTask.kt new file mode 100644 index 0000000000..b4f3ea2a2f --- /dev/null +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/process/SpringAnalyzerTask.kt @@ -0,0 +1,41 @@ +package org.utbot.framework.process + +import mu.KotlinLogging +import org.utbot.framework.plugin.api.BeanAdditionalData +import org.utbot.framework.plugin.api.BeanDefinitionData +import org.utbot.framework.plugin.api.SpringSettings.PresentSpringSettings +import org.utbot.rd.use +import org.utbot.spring.process.SpringAnalyzerProcess + +class SpringAnalyzerTask( + private val classpath: List, + private val settings: PresentSpringSettings, +) : EngineProcessTask> { + companion object { + private val logger = KotlinLogging.logger {} + } + + override fun perform(): List = try { + SpringAnalyzerProcess.createBlocking(classpath).use { + it.getBeanDefinitions(settings) + }.beanDefinitions + .map { data -> + // mapping RD models to API models + val additionalData = data.additionalData?.let { + BeanAdditionalData( + it.factoryMethodName, + it.parameterTypes, + it.configClassFqn + ) + } + BeanDefinitionData( + data.beanName, + data.beanTypeFqn, + additionalData + ) + } + } catch (e: Exception) { + logger.error(e) { "Spring Analyzer crashed, resorting to using empty bean list" } + emptyList() + } +} \ No newline at end of file diff --git a/utbot-spring-sample/build.gradle.kts b/utbot-spring-sample/build.gradle.kts new file mode 100644 index 0000000000..bd0b3b4ea3 --- /dev/null +++ b/utbot-spring-sample/build.gradle.kts @@ -0,0 +1,45 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer +import com.github.jengelman.gradle.plugins.shadow.transformers.PropertiesFileTransformer + +plugins { + id("com.github.johnrengelman.shadow") version "7.1.2" + id("java") +} + +val springBootVersion: String by rootProject + +dependencies { + implementation("org.projectlombok:lombok:1.18.20") + annotationProcessor("org.projectlombok:lombok:1.18.20") + + implementation(group = "org.springframework.boot", name = "spring-boot-starter-web", version = springBootVersion) + implementation(group = "org.springframework.boot", name = "spring-boot-starter-data-jpa", version = springBootVersion) + implementation(group = "org.springframework.boot", name = "spring-boot-starter-test", version = springBootVersion) +} + +tasks.shadowJar { + isZip64 = true + + transform(Log4j2PluginsCacheFileTransformer::class.java) + archiveFileName.set("utbot-spring-sample-shadow.jar") + + // Required for Spring to run properly when using shadowJar + // More details: https://github.com/spring-projects/spring-boot/issues/1828 + mergeServiceFiles() + append("META-INF/spring.handlers") + append("META-INF/spring.schemas") + append("META-INF/spring.tooling") + transform(PropertiesFileTransformer().apply { + paths = listOf("META-INF/spring.factories") + mergeStrategy = "append" + }) +} + +val springSampleJar: Configuration by configurations.creating { + isCanBeResolved = false + isCanBeConsumed = true +} + +artifacts { + add(springSampleJar.name, tasks.shadowJar) +} \ No newline at end of file diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/MyService.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/MyService.java new file mode 100644 index 0000000000..7e9f49b37e --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/MyService.java @@ -0,0 +1,5 @@ +package org.utbot.examples.spring.app; + +public interface MyService { + String getName(); +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/MyServiceImpl.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/MyServiceImpl.java new file mode 100644 index 0000000000..6f711df70b --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/MyServiceImpl.java @@ -0,0 +1,11 @@ +package org.utbot.examples.spring.app; + +import org.springframework.stereotype.Service; + +@Service +public class MyServiceImpl implements MyService { + @Override + public String getName() { + return "impl"; + } +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/MyServiceUser.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/MyServiceUser.java new file mode 100644 index 0000000000..d1db7260c2 --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/MyServiceUser.java @@ -0,0 +1,18 @@ +package org.utbot.examples.spring.app; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class MyServiceUser { + private final MyService myService; + + @Autowired + public MyServiceUser(MyService myService) { + this.myService = myService; + } + + public String useMyService() { + return myService.getName(); + } +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/SpringExampleApp.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/SpringExampleApp.java new file mode 100644 index 0000000000..32f4cd0c94 --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/app/SpringExampleApp.java @@ -0,0 +1,7 @@ +package org.utbot.examples.spring.app; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringExampleApp { +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/OrderRepository.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/OrderRepository.java new file mode 100644 index 0000000000..c4446c5924 --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/OrderRepository.java @@ -0,0 +1,7 @@ +package org.utbot.examples.spring.autowiring; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.utbot.examples.spring.autowiring.oneBeanForOneType.Order; + +public interface OrderRepository extends JpaRepository { +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/oneBeanForOneType/Order.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/oneBeanForOneType/Order.java new file mode 100644 index 0000000000..5a2f2529e1 --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/oneBeanForOneType/Order.java @@ -0,0 +1,24 @@ +package org.utbot.examples.spring.autowiring.oneBeanForOneType; + +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +import javax.persistence.*; + +@Getter +@Setter +@Builder +@ToString +@Jacksonized +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "orders") +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + String buyer; + Double price; + int qty; +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedAndNonInjectedField.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedAndNonInjectedField.java new file mode 100644 index 0000000000..89309ca35a --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedAndNonInjectedField.java @@ -0,0 +1,22 @@ +package org.utbot.examples.spring.autowiring.oneBeanForOneType; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.utbot.examples.spring.autowiring.OrderRepository; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class ServiceWithInjectedAndNonInjectedField { + + public List selectedOrders = new ArrayList<>(); + + @Autowired + private OrderRepository orderRepository; + + public Integer getOrdersSize() { + return orderRepository.findAll().size() + selectedOrders.size(); + } + +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedField.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedField.java new file mode 100644 index 0000000000..9b492c30d9 --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedField.java @@ -0,0 +1,22 @@ +package org.utbot.examples.spring.autowiring.oneBeanForOneType; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.utbot.examples.spring.autowiring.OrderRepository; + +import java.util.List; + +@Service +public class ServiceWithInjectedField { + + @Autowired + private OrderRepository orderRepository; + + public List getOrders() { + return orderRepository.findAll(); + } + + public Order createOrder(Order order) { + return orderRepository.save(order); + } +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/Person.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/Person.java new file mode 100644 index 0000000000..64c9cb9d91 --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/Person.java @@ -0,0 +1,22 @@ +package org.utbot.examples.spring.autowiring.twoAndMoreBeansForOneType; + +public class Person { + private String firstName; + private String lastName; + + private Integer age; + + public Person(String firstName, String secondName, Integer age) { + this.firstName = firstName; + this.lastName = secondName; + this.age = age; + } + + public String getName() { + return firstName + " " + lastName; + } + + public Integer getAge(){ + return age; + } +} \ No newline at end of file diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameType.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameType.java new file mode 100644 index 0000000000..3c3c9bf239 --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameType.java @@ -0,0 +1,31 @@ +package org.utbot.examples.spring.autowiring.twoAndMoreBeansForOneType; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class ServiceOfBeansWithSameType { + @Autowired + private Person personOne; + + @Autowired + private Person personTwo; + + // A method for testing both cases when the Engine produces + // - two models for two @Autowired fields of the same type + // - one model for two @Autowired fields of the same type + public Boolean checker() { + String name1 = personOne.getName();// shouldn't produce NPE because `personOne` is `@Autowired` + int length = name1.length(); // can produce NPE because `Person.name` isn't `@Autowired` + Integer age2 = personTwo.getAge(); // shouldn't produce NPE because `personTwo` is `@Autowired` + return personOne == personTwo; + } + + public Person getPersonOne() { + return personOne; + } + + public Person getPersonTwo() { + return personTwo; + } +} \ No newline at end of file diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/boot/ExampleSpringBootConfig.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/boot/ExampleSpringBootConfig.java new file mode 100644 index 0000000000..b437460c7d --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/boot/ExampleSpringBootConfig.java @@ -0,0 +1,7 @@ +package org.utbot.examples.spring.config.boot; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ExampleSpringBootConfig { +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/boot/ExampleSpringBootService.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/boot/ExampleSpringBootService.java new file mode 100644 index 0000000000..99e89e7b9d --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/boot/ExampleSpringBootService.java @@ -0,0 +1,11 @@ +package org.utbot.examples.spring.config.boot; + +import org.springframework.stereotype.Service; +import org.utbot.examples.spring.config.utils.SafetyUtils; + +@Service +public class ExampleSpringBootService { + public ExampleSpringBootService() { + SafetyUtils.shouldNeverBeCalled(); + } +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/pure/ExamplePureSpringConfig.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/pure/ExamplePureSpringConfig.java new file mode 100644 index 0000000000..e0fa208ee8 --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/pure/ExamplePureSpringConfig.java @@ -0,0 +1,34 @@ +package org.utbot.examples.spring.config.pure; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.utbot.examples.spring.config.utils.SafetyUtils; + +public class ExamplePureSpringConfig { + @Bean(name = "exampleService0") + public ExamplePureSpringService exampleService() { + SafetyUtils.shouldNeverBeCalled(); + return null; + } + + @Bean(name = "exampleServiceTest1") + @Profile("test1") + public ExamplePureSpringService exampleServiceTest1() { + SafetyUtils.shouldNeverBeCalled(); + return null; + } + + @Bean(name = "exampleServiceTest2") + @Profile("test2") + public ExamplePureSpringService exampleServiceTest2() { + SafetyUtils.shouldNeverBeCalled(); + return null; + } + + @Bean(name = "exampleServiceTest3") + @Profile("test3") + public ExamplePureSpringService exampleServiceTest3() { + SafetyUtils.shouldNeverBeCalled(); + return null; + } +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/pure/ExamplePureSpringService.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/pure/ExamplePureSpringService.java new file mode 100644 index 0000000000..bed899616d --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/pure/ExamplePureSpringService.java @@ -0,0 +1,4 @@ +package org.utbot.examples.spring.config.pure; + +public class ExamplePureSpringService { +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/utils/SafetyUtils.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/utils/SafetyUtils.java new file mode 100644 index 0000000000..94f12774bb --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/utils/SafetyUtils.java @@ -0,0 +1,15 @@ +package org.utbot.examples.spring.config.utils; + +public class SafetyUtils { + + private static final int UNEXPECTED_CALL_EXIT_STATUS = -1182; + + /** + * Bean constructors and factory methods should never be executed during bean analysis, + * hence call to this method is added into them to ensure they are actually never called. + */ + public static void shouldNeverBeCalled() { + System.err.println("shouldNeverBeCalled() is unexpectedly called"); + System.exit(UNEXPECTED_CALL_EXIT_STATUS); + } +} diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/xml/ExampleXmlService.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/xml/ExampleXmlService.java new file mode 100644 index 0000000000..d2f57e7d32 --- /dev/null +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/config/xml/ExampleXmlService.java @@ -0,0 +1,9 @@ +package org.utbot.examples.spring.config.xml; + +import org.utbot.examples.spring.config.utils.SafetyUtils; + +public class ExampleXmlService { + public ExampleXmlService() { + SafetyUtils.shouldNeverBeCalled(); + } +} diff --git a/utbot-spring-sample/src/main/resources/xml-spring-config.xml b/utbot-spring-sample/src/main/resources/xml-spring-config.xml new file mode 100644 index 0000000000..5118152230 --- /dev/null +++ b/utbot-spring-sample/src/main/resources/xml-spring-config.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/utbot-spring-test/build.gradle b/utbot-spring-test/build.gradle new file mode 100644 index 0000000000..631e4a6d19 --- /dev/null +++ b/utbot-spring-test/build.gradle @@ -0,0 +1,46 @@ +configurations { + fetchSpringSampleJar +} + +dependencies { + testImplementation(project(":utbot-framework")) + testImplementation(project(":utbot-spring-framework")) + testImplementation project(':utbot-testing') + testImplementation project(':utbot-spring-sample') + + // 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.8.1' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.1' + + // used for testing code generation + testImplementation group: 'junit', name: 'junit', version: junit4Version + testImplementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4PlatformVersion + testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + testImplementation group: 'org.mockito', name: 'mockito-inline', version: mockitoInlineVersion + testImplementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion + + testImplementation group: 'commons-io', name: 'commons-io', version: commonsIoVersion + + testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: springBootVersion + + fetchSpringSampleJar project(path: ':utbot-spring-sample', configuration: 'springSampleJar') +} + +configurations { + all { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' + } +} + +test { + if (System.getProperty('DEBUG', 'false') == 'true') { + jvmArgs '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9009' + } +} + +processTestResources { + from(configurations.fetchSpringSampleJar) { + into "lib" + } +} diff --git a/utbot-spring-test/src/test/java/org/utbot/api/java/SpringUtBotJavaApiTest.java b/utbot-spring-test/src/test/java/org/utbot/api/java/SpringUtBotJavaApiTest.java new file mode 100644 index 0000000000..09a5594bc1 --- /dev/null +++ b/utbot-spring-test/src/test/java/org/utbot/api/java/SpringUtBotJavaApiTest.java @@ -0,0 +1,105 @@ +package org.utbot.api.java; + +import org.utbot.examples.spring.app.MyServiceImpl; +import org.junit.jupiter.api.Test; +import org.utbot.examples.spring.app.MyServiceUser; +import org.utbot.examples.spring.app.SpringExampleApp; +import org.utbot.examples.spring.config.pure.ExamplePureSpringConfig; +import org.utbot.external.api.UtBotJavaApi; +import org.utbot.external.api.UtBotSpringApi; +import org.utbot.framework.codegen.domain.ForceStaticMocking; +import org.utbot.framework.codegen.domain.Junit4; +import org.utbot.framework.codegen.domain.MockitoStaticMocking; +import org.utbot.framework.codegen.domain.ProjectType; +import org.utbot.framework.context.ApplicationContext; +import org.utbot.framework.context.spring.SpringApplicationContext; +import org.utbot.framework.plugin.api.*; +import org.utbot.framework.util.Snippet; + +import java.io.File; +import java.lang.reflect.Method; +import java.util.*; + +import static org.utbot.framework.plugin.api.MockFramework.MOCKITO; +import static org.utbot.framework.util.TestUtilsKt.compileClassFile; + +public class SpringUtBotJavaApiTest extends AbstractUtBotJavaApiTest { + + /** + * We are using the environment to check spring generation the only difference in the data supplied by the argument + * @param applicationContext returns Spring settings to run the generation with + */ + private void supplyConfigurationAndRunSpringTest(ApplicationContext applicationContext) { + + UtBotJavaApi.setStopConcreteExecutorOnExit(false); + + String classpath = getClassPath(MyServiceImpl.class); + String dependencyClassPath = getDependencyClassPath(); + + Method getNameMethod = getMethodByName( + MyServiceImpl.class, + "getName" + ); + + Method useMyServiceMethod = getMethodByName( + MyServiceUser.class, + "useMyService" + ); + + List myServiceUserTestSets = UtBotJavaApi.generateTestSetsForMethods( + Collections.singletonList(useMyServiceMethod), + MyServiceUser.class, + classpath, + dependencyClassPath, + MockStrategyApi.OTHER_PACKAGES, + 60000L, + applicationContext + ); + + String generationResult = UtBotJavaApi.generateTestCode( + Collections.emptyList(), + myServiceUserTestSets, + GENERATED_TEST_CLASS_NAME, + classpath, + dependencyClassPath, + MyServiceUser.class, + ProjectType.Spring, + Junit4.INSTANCE, + MOCKITO, + CodegenLanguage.JAVA, + MockitoStaticMocking.INSTANCE, + false, + ForceStaticMocking.DO_NOT_FORCE, + MyServiceUser.class.getPackage().getName(), + applicationContext + ); + + Snippet snippet = new Snippet(CodegenLanguage.JAVA, generationResult); + compileClassFile(GENERATED_TEST_CLASS_NAME, snippet); + } + + @Test + public void testUnitTestWithoutSettings() { + supplyConfigurationAndRunSpringTest(UtBotSpringApi.createSpringApplicationContext( + SpringSettings.AbsentSpringSettings, + SpringTestType.UNIT_TEST, + Collections.emptyList())); + } + + @Test + public void testUnitTestWithSettings() { + SpringConfiguration configuration = + UtBotSpringApi.createJavaSpringConfiguration(SpringExampleApp.class); + + SpringSettings springSettings = + new SpringSettings.PresentSpringSettings(configuration, Arrays.asList("test1", "test2")); + + ApplicationContext applicationContext = UtBotSpringApi.createSpringApplicationContext( + springSettings, + SpringTestType.UNIT_TEST, + Arrays.asList(getDependencyClassPath().split(File.pathSeparator)) + ); + + supplyConfigurationAndRunSpringTest(applicationContext); + } +} diff --git a/utbot-spring-test/src/test/java/org/utbot/examples/spring/manual/UtBotSpringApiTest.java b/utbot-spring-test/src/test/java/org/utbot/examples/spring/manual/UtBotSpringApiTest.java new file mode 100644 index 0000000000..7e318a3c03 --- /dev/null +++ b/utbot-spring-test/src/test/java/org/utbot/examples/spring/manual/UtBotSpringApiTest.java @@ -0,0 +1,178 @@ +package org.utbot.examples.spring.manual; + +import org.apache.commons.io.FileUtils; +import org.junit.Test; +import org.utbot.common.FileUtil; +import org.utbot.common.JarUtils; +import org.utbot.examples.spring.config.boot.ExampleSpringBootConfig; +import org.utbot.examples.spring.config.pure.ExamplePureSpringConfig; +import org.utbot.external.api.UtBotSpringApi; +import org.utbot.framework.context.spring.SpringApplicationContext; +import org.utbot.framework.plugin.api.*; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +public class UtBotSpringApiTest { + private static final List DEFAULT_PROFILES = Collections.singletonList("default"); + + @Test + public void testOnAbsentSpringSettings() { + SpringApplicationContext springApplicationContext = + UtBotSpringApi.createSpringApplicationContext( + SpringSettings.AbsentSpringSettings, + SpringTestType.UNIT_TEST, + new ArrayList<>() + ); + + assertEquals(new ArrayList<>(), springApplicationContext.getBeanDefinitions()); + } + + @Test + public void testOnSpringBootConfig() { + List actual = getBeansFromSampleProject( + UtBotSpringApi.createJavaSpringConfiguration(ExampleSpringBootConfig.class), + DEFAULT_PROFILES + ); + + List expected = new ArrayList<>(); + expected.add(new BeanDefinitionData( + "exampleSpringBootConfig", + "org.utbot.examples.spring.config.boot.ExampleSpringBootConfig", + null + )); + expected.add(new BeanDefinitionData( + "exampleSpringBootService", + "org.utbot.examples.spring.config.boot.ExampleSpringBootService", + null + )); + + assertEquals(expected, actual); + } + + @Test + public void testOnPureSpringConfig() { + List actual = getBeansFromSampleProject( + UtBotSpringApi.createJavaSpringConfiguration(ExamplePureSpringConfig.class), + DEFAULT_PROFILES + ); + + List expected = new ArrayList<>(); + expected.add(new BeanDefinitionData("examplePureSpringConfig", "org.utbot.examples.spring.config.pure.ExamplePureSpringConfig", null)); + expected.add(new BeanDefinitionData( + "exampleService0", + "org.utbot.examples.spring.config.pure.ExamplePureSpringService", + new BeanAdditionalData( + "exampleService", + Collections.emptyList(), + "org.utbot.examples.spring.config.pure.ExamplePureSpringConfig" + ) + )); + + assertEquals(expected, actual); + } + + @Test + public void testOnPureSpringConfigWithProfiles() { + List profiles = new ArrayList<>(); + profiles.add("test1"); + profiles.add("test3"); + + List actual = getBeansFromSampleProject( + UtBotSpringApi.createJavaSpringConfiguration(ExamplePureSpringConfig.class), + profiles + ); + + List expected = new ArrayList<>(); + expected.add(new BeanDefinitionData("examplePureSpringConfig", "org.utbot.examples.spring.config.pure.ExamplePureSpringConfig", null)); + expected.add(new BeanDefinitionData( + "exampleService0", + "org.utbot.examples.spring.config.pure.ExamplePureSpringService", + new BeanAdditionalData( + "exampleService", + Collections.emptyList(), + "org.utbot.examples.spring.config.pure.ExamplePureSpringConfig" + ) + )); + expected.add(new BeanDefinitionData( + "exampleServiceTest1", + "org.utbot.examples.spring.config.pure.ExamplePureSpringService", + new BeanAdditionalData( + "exampleServiceTest1", + Collections.emptyList(), + "org.utbot.examples.spring.config.pure.ExamplePureSpringConfig" + ) + )); + expected.add(new BeanDefinitionData( + "exampleServiceTest3", + "org.utbot.examples.spring.config.pure.ExamplePureSpringService", + new BeanAdditionalData( + "exampleServiceTest3", + Collections.emptyList(), + "org.utbot.examples.spring.config.pure.ExamplePureSpringConfig" + ) + )); + + assertEquals(expected, actual); + } + + @Test + public void testOnXmlConfig() throws IOException { + URL configUrl = Objects.requireNonNull(getClass().getClassLoader().getResource("xml-spring-config.xml")); + File configDir = FileUtil.INSTANCE.createTempDirectory("xml-config").toFile(); + File configFile = new File(configDir, "xml-spring-config.xml"); + FileUtils.copyURLToFile(configUrl, configFile); + List additionalClasspath = Collections.singletonList(configDir.getAbsolutePath()); + + List actual = getBeansFromSampleProject( + UtBotSpringApi.createXmlSpringConfiguration(configFile), + additionalClasspath + ); + + List expected = new ArrayList<>(); + expected.add(new BeanDefinitionData( + "xmlService", + "org.utbot.examples.spring.config.xml.ExampleXmlService", + null + )); + + assertEquals(expected, actual); + } + + static List getBeansFromSampleProject( + SpringConfiguration configuration, + List profiles + ) { + return getBeansFromSampleProject(configuration, profiles, new ArrayList<>()); + } + + static List getBeansFromSampleProject( + SpringConfiguration configuration, + List profiles, + List additionalClasspath + ) { + SpringSettings springSettings = new SpringSettings.PresentSpringSettings(configuration, profiles); + SpringTestType springTestType = SpringTestType.UNIT_TEST; + List classpath = new ArrayList<>(additionalClasspath); + classpath.add( + JarUtils.INSTANCE.extractJarFileFromResources( + "utbot-spring-sample-shadow.jar", + "lib/utbot-spring-sample-shadow.jar", + "spring-sample" + ).getAbsolutePath() + ); + SpringApplicationContext springApplicationContext = + UtBotSpringApi.createSpringApplicationContext(springSettings, springTestType, classpath); + return springApplicationContext.getBeanDefinitions().stream() + .filter(beanDef -> beanDef.getBeanTypeName().startsWith("org.utbot.examples")) + .collect(Collectors.toList()); + } +} diff --git a/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/SpringNoConfigUtValueTestCaseChecker.kt b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/SpringNoConfigUtValueTestCaseChecker.kt new file mode 100644 index 0000000000..a0977e9e35 --- /dev/null +++ b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/SpringNoConfigUtValueTestCaseChecker.kt @@ -0,0 +1,24 @@ +package org.utbot.examples.spring.autowiring + +import org.utbot.examples.spring.utils.standardSpringTestingConfigurations +import org.utbot.framework.context.spring.SpringApplicationContextImpl +import org.utbot.framework.plugin.api.SpringSettings +import org.utbot.framework.plugin.api.SpringTestType +import org.utbot.testing.UtValueTestCaseChecker +import org.utbot.testing.defaultApplicationContext +import kotlin.reflect.KClass + +val springNoConfigApplicationContext = SpringApplicationContextImpl.internalCreate( + delegateContext = defaultApplicationContext, + springTestType = SpringTestType.UNIT_TEST, + springSettings = SpringSettings.AbsentSpringSettings, + beanDefinitions = emptyList() +) + +abstract class SpringNoConfigUtValueTestCaseChecker( + testClass: KClass<*> +) : UtValueTestCaseChecker( + testClass, + configurations = standardSpringTestingConfigurations, + applicationContext = springNoConfigApplicationContext +) \ No newline at end of file diff --git a/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedAndNonInjectedFieldTests.kt b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedAndNonInjectedFieldTests.kt new file mode 100644 index 0000000000..f86ddf9101 --- /dev/null +++ b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedAndNonInjectedFieldTests.kt @@ -0,0 +1,35 @@ +package org.utbot.examples.spring.autowiring.oneBeanForOneType + +import org.junit.jupiter.api.Test +import org.utbot.examples.spring.autowiring.SpringNoConfigUtValueTestCaseChecker +import org.utbot.examples.spring.utils.findAllRepositoryCall +import org.utbot.examples.spring.utils.springAdditionalDependencies +import org.utbot.examples.spring.utils.springMockStrategy +import org.utbot.testing.DoNotCalculate +import org.utbot.testing.ignoreExecutionsNumber +import org.utbot.testing.isException +import org.utbot.testing.singleMock +import org.utbot.testing.value + +internal class ServiceWithInjectedAndNonInjectedFieldTests : SpringNoConfigUtValueTestCaseChecker( + testClass = ServiceWithInjectedAndNonInjectedField::class, +) { + @Test + fun testGetOrdersSize() { + checkThisMocksAndExceptions( + method = ServiceWithInjectedAndNonInjectedField::getOrdersSize, + // TODO: replace with `branches = eq(3)` + // after the fix of `speculativelyCannotProduceNullPointerException` in SpringApplicationContext + branches = ignoreExecutionsNumber, + { thisInstance, mocks, r: Result -> + val orderRepository = mocks.singleMock("orderRepository", findAllRepositoryCall) + val repositorySize = orderRepository.value?>()!!.size + repositorySize + thisInstance.selectedOrders.size == r.getOrNull() + }, + { _, _, r: Result -> r.isException() }, + coverage = DoNotCalculate, + mockStrategy = springMockStrategy, + additionalDependencies = springAdditionalDependencies, + ) + } +} \ No newline at end of file diff --git a/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedFieldTests.kt b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedFieldTests.kt new file mode 100644 index 0000000000..c123d7f2bd --- /dev/null +++ b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/oneBeanForOneType/ServiceWithInjectedFieldTests.kt @@ -0,0 +1,45 @@ +package org.utbot.examples.spring.autowiring.oneBeanForOneType + +import org.junit.jupiter.api.Test +import org.utbot.examples.spring.autowiring.SpringNoConfigUtValueTestCaseChecker +import org.utbot.examples.spring.utils.findAllRepositoryCall +import org.utbot.examples.spring.utils.saveRepositoryCall +import org.utbot.examples.spring.utils.springAdditionalDependencies +import org.utbot.examples.spring.utils.springMockStrategy +import org.utbot.testcheckers.eq +import org.utbot.testing.* + +internal class ServiceWithInjectedFieldTests : SpringNoConfigUtValueTestCaseChecker( + testClass = ServiceWithInjectedField::class, +) { + @Test + fun testGetOrders() { + checkMocks( + method = ServiceWithInjectedField::getOrders, + branches = eq(1), + { mocks, r -> + val orderRepository = mocks.singleMock("orderRepository", findAllRepositoryCall) + orderRepository.value?>() == r + }, + coverage = DoNotCalculate, + mockStrategy = springMockStrategy, + additionalDependencies = springAdditionalDependencies, + ) + } + + @Test + fun testCreateOrder() { + checkThisMocksAndExceptions( + method = ServiceWithInjectedField::createOrder, + // TODO: replace with `branches = eq(1)` after fix of https://github.com/UnitTestBot/UTBotJava/issues/2367 + branches = ignoreExecutionsNumber, + { _, _, mocks, r: Result -> + val orderRepository = mocks.singleMock("orderRepository", saveRepositoryCall) + orderRepository.value() == r.getOrNull() + }, + coverage = DoNotCalculate, + mockStrategy = springMockStrategy, + additionalDependencies = springAdditionalDependencies, + ) + } +} \ No newline at end of file diff --git a/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameTypeTest.kt b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameTypeTest.kt new file mode 100644 index 0000000000..aa748579c4 --- /dev/null +++ b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameTypeTest.kt @@ -0,0 +1,59 @@ +package org.utbot.examples.spring.autowiring.twoAndMoreBeansForOneType + +import org.junit.jupiter.api.Test +import org.utbot.examples.spring.autowiring.SpringNoConfigUtValueTestCaseChecker +import org.utbot.examples.spring.utils.* +import org.utbot.testcheckers.eq +import org.utbot.testing.* + +class ServiceOfBeansWithSameTypeTest : SpringNoConfigUtValueTestCaseChecker( + testClass = ServiceOfBeansWithSameType::class, +) { + + /** + * In this test, we check both cases when the Engine produces + * - two models for two @Autowired fields of the same type + * - one model for two @Autowired fields of the same type + */ + @Test + fun testChecker() { + checkThisMocksAndExceptions( + method = ServiceOfBeansWithSameType::checker, + branches = eq(3), + {_, mocks, r -> + val personOne = mocks.singleMock("personOne", namePersonCall) + + val personOneName = personOne.value() + + val r1 = personOneName == null + + r1 && r.isException() + }, + {_, mocks, r -> + val personOne = mocks.singleMock("personOne", namePersonCall) + val personTwo = mocks.singleMockOrNull("personTwo", namePersonCall) + + val personOneName = personOne.value() + + val r1 = personOneName != null + val r2 = personTwo == null // `personTwo.getName()` isn't mocked, meaning `personOne != personTwo` + + r1 && r2 && (r.getOrNull() == false) + }, + {_, mocks, r -> + val personOne = mocks.singleMock("personOne", namePersonCall) + val personTwo = mocks.singleMockOrNull("personTwo", namePersonCall) + + val personOneName = personOne.value() + + val r1 = personOneName != null + val r2 = personTwo != null // `personTwo.getName()` is mocked, meaning `personOne == personTwo` + + r1 && r2 && (r.getOrNull() == true) + }, + coverage = DoNotCalculate, + mockStrategy = springMockStrategy, + additionalDependencies = springAdditionalDependencies, + ) + } +} diff --git a/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/utils/CallUtils.kt b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/utils/CallUtils.kt new file mode 100644 index 0000000000..86cb0648e4 --- /dev/null +++ b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/utils/CallUtils.kt @@ -0,0 +1,38 @@ +package org.utbot.examples.spring.utils + +import org.utbot.examples.spring.autowiring.OrderRepository +import org.utbot.examples.spring.autowiring.oneBeanForOneType.Order +import org.utbot.examples.spring.autowiring.twoAndMoreBeansForOneType.Person +import kotlin.reflect.KFunction1 +import kotlin.reflect.KFunction2 +import kotlin.reflect.full.functions + +@Suppress("UNCHECKED_CAST") +val findAllRepositoryCall: KFunction1?> = + OrderRepository::class + .functions + .single { it.name == "findAll" && it.parameters.size == 1 } + as KFunction1?> + + +@Suppress("UNCHECKED_CAST") +val saveRepositoryCall: KFunction2 = + OrderRepository::class + .functions + .single { it.name == "save" && it.parameters.size == 2 } + as KFunction2 + + +@Suppress("UNCHECKED_CAST") +val namePersonCall: KFunction1 = + Person::class + .functions + .single { it.name == "getName" && it.parameters.size == 1 } + as KFunction1 + +@Suppress("UNCHECKED_CAST") +val agePersonCall: KFunction1 = + Person::class + .functions + .single { it.name == "getAge" && it.parameters.size == 1 } + as KFunction1 \ No newline at end of file diff --git a/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/utils/SpringTestingConfiguration.kt b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/utils/SpringTestingConfiguration.kt new file mode 100644 index 0000000000..3a967775a1 --- /dev/null +++ b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/utils/SpringTestingConfiguration.kt @@ -0,0 +1,20 @@ +package org.utbot.examples.spring.utils + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.PagingAndSortingRepository +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.SpringConfiguration +import org.utbot.testing.TestExecution + +val standardSpringTestingConfigurations: List = listOf( + SpringConfiguration(CodegenLanguage.JAVA, ParametrizedTestSource.DO_NOT_PARAMETRIZE, TestExecution) +) + +val springMockStrategy = MockStrategyApi.OTHER_CLASSES + +val springAdditionalDependencies: Array> = arrayOf( + JpaRepository::class.java, + PagingAndSortingRepository::class.java, +) \ No newline at end of file diff --git a/utbot-spring-test/src/test/resources/log4j2.xml b/utbot-spring-test/src/test/resources/log4j2.xml new file mode 100644 index 0000000000..ac3d2f2abf --- /dev/null +++ b/utbot-spring-test/src/test/resources/log4j2.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-summary-tests/build.gradle b/utbot-summary-tests/build.gradle index 376fc6af1b..8f356f6e58 100644 --- a/utbot-summary-tests/build.gradle +++ b/utbot-summary-tests/build.gradle @@ -10,15 +10,6 @@ dependencies { implementation(project(':utbot-instrumentation')) testImplementation project(':utbot-sample') testImplementation group: 'junit', name: 'junit', version: junit4Version + testImplementation project(':utbot-testing') testImplementation project(':utbot-framework').sourceSets.test.output } - -test { - minHeapSize = "128m" - maxHeapSize = "3072m" - - jvmArgs '-XX:MaxHeapSize=3072m' - useJUnitPlatform() { - excludeTags 'slow', 'IntegrationTest' - } -} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/SummaryTestCaseGeneratorTest.kt b/utbot-summary-tests/src/test/kotlin/examples/SummaryTestCaseGeneratorTest.kt index 86400d6d00..4de641132b 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/SummaryTestCaseGeneratorTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/SummaryTestCaseGeneratorTest.kt @@ -3,8 +3,6 @@ package examples import org.junit.jupiter.api.* import org.utbot.common.WorkaroundReason import org.utbot.common.workaround -import org.utbot.tests.infrastructure.UtValueTestCaseChecker -import org.utbot.tests.infrastructure.CoverageMatcher import org.utbot.framework.UtSettings.checkNpeInNestedMethods import org.utbot.framework.UtSettings.checkNpeInNestedNotPrivateMethods import org.utbot.framework.UtSettings.checkSolverTimeoutMillis @@ -12,12 +10,13 @@ import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.executableId import org.utbot.summary.comment.nextSynonyms -import org.utbot.summary.summarize -import org.utbot.tests.infrastructure.TestExecution +import org.utbot.summary.summarizeAll +import org.utbot.testing.CoverageMatcher +import org.utbot.testing.TestExecution +import org.utbot.testing.UtValueTestCaseChecker import kotlin.reflect.KClass import kotlin.reflect.KFunction - private const val NEW_LINE = "\n" private const val POINT_IN_THE_LIST = " * " private const val COMMENT_SEPARATOR = "-------------------------------------------------------------" @@ -26,11 +25,7 @@ private const val COMMENT_SEPARATOR = "----------------------------------------- open class SummaryTestCaseGeneratorTest( testClass: KClass<*>, testCodeGeneration: Boolean = false, - languagePipelines: List = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, TestExecution) - ) -) : UtValueTestCaseChecker(testClass, testCodeGeneration, languagePipelines) { +) : UtValueTestCaseChecker(testClass, testCodeGeneration) { private lateinit var cookie: AutoCloseable @BeforeEach @@ -50,7 +45,8 @@ open class SummaryTestCaseGeneratorTest( summaryKeys: List, methodNames: List = listOf(), displayNames: List = listOf(), - clusterInfo: List> = listOf() + clusterInfo: List> = listOf(), + additionalMockAlwaysClasses: Set = emptySet() ) { workaround(WorkaroundReason.HACK) { // @todo change to the constructor parameter @@ -58,8 +54,12 @@ open class SummaryTestCaseGeneratorTest( checkNpeInNestedMethods = true checkNpeInNestedNotPrivateMethods = true } - val testSet = executionsModel(method.executableId, mockStrategy) - val testSetWithSummarization = testSet.summarize(searchDirectory) + val testSet = executionsModel( + method.executableId, + mockStrategy, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + val testSetWithSummarization = listOf(testSet).summarizeAll(searchDirectory, sourceFile = null).single() testSetWithSummarization.executions.checkMatchersWithTextSummary(summaryKeys) testSetWithSummarization.executions.checkMatchersWithMethodNames(methodNames) diff --git a/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryArrayQuickSortExampleTest.kt b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryArrayQuickSortExampleTest.kt index 2187f50f4d..45c4717763 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryArrayQuickSortExampleTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryArrayQuickSortExampleTest.kt @@ -4,9 +4,9 @@ import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.utbot.examples.algorithms.ArraysQuickSort -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.UtClusterInfo +import org.utbot.testing.DoNotCalculate @Tag("slow") class SummaryArrayQuickSortExampleTest : SummaryTestCaseGeneratorTest( diff --git a/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryBinarySearchTest.kt b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryBinarySearchTest.kt new file mode 100644 index 0000000000..a9175c1b9c --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryBinarySearchTest.kt @@ -0,0 +1,94 @@ +package examples.algorithms + +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.utbot.examples.algorithms.BinarySearch +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate + +class SummaryBinarySearchTest : SummaryTestCaseGeneratorTest( + BinarySearch::class, +) { + @Test + fun testLeftBinSearch() { + val summary1 = "Test does not iterate while(left < right - 1), executes conditions:\n" + + " (found): False\n" + + "returns from: return -1;\n" + val summary2 = "Test 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 later executes conditions:\n" + + " (found): False\n" + + "returns from: return -1;" + val summary3 = "Test 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 later executes conditions:\n" + + " (found): True\n" + + "returns from: return right + 1;\n" + val summary4 = "Test 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 afterwards executes conditions:\n" + + " (found): True\n" + + "returns from: return right + 1;\n" + val summary5 = "Test invokes:\n" + + " org.utbot.examples.algorithms.BinarySearch#isUnsorted(long[]) once\n" + + "throws NullPointerException when: isUnsorted(array)\n" + val summary6 = "Test invokes:\n" + + " org.utbot.examples.algorithms.BinarySearch#isUnsorted(long[]) once\n" + + "executes conditions:\n" + + " (isUnsorted(array)): True\n" + + "throws IllegalArgumentException when: isUnsorted(array)\n" + + val methodName1 = "testLeftBinSearch_NotFound" + val methodName2 = "testLeftBinSearch_MiddleOfArrayLessThanKey" + val methodName3 = "testLeftBinSearch_Found" + val methodName4 = "testLeftBinSearch_Found_1" + val methodName5 = "testLeftBinSearch_ThrowNullPointerException" + val methodName6 = "testLeftBinSearch_ThrowIllegalArgumentException" + + val displayName1 = "found : False -> return -1" + val displayName2 = "array[middle] == key : False -> return -1" + val displayName3 = "while(left < right - 1) -> return right + 1" + val displayName4 = "while(left < right - 1) -> return right + 1" + val displayName5 = "isUnsorted(array) -> ThrowNullPointerException" + val displayName6 = "isUnsorted(array) -> ThrowIllegalArgumentException" + + 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 = BinarySearch::leftBinSearch + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryCorrectBracketSequences.kt b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryCorrectBracketSequences.kt new file mode 100644 index 0000000000..9fb1ef82da --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryCorrectBracketSequences.kt @@ -0,0 +1,52 @@ +package examples.algorithms + +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.utbot.examples.algorithms.CorrectBracketSequences +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate + +class SummaryCorrectBracketSequences : SummaryTestCaseGeneratorTest( + CorrectBracketSequences::class, +) { + @Test + fun testIsTheSameType() { + val commonSummary = "Test returns from: return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']';\n" + + val methodName1 = "testIsTheSameType_ANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsChar" + val methodName2 = "testIsTheSameType_ANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsChar_1" + val methodName3 = "testIsTheSameType_AEqualsCharAndBEqualsCharOrAEqualsCharAndBEqualsCharOrAEqualsCharAndBEqualsChar" + val methodName4 = "testIsTheSameType_ANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsChar_2" + val methodName5 = "testIsTheSameType_ANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsCharOrANotEqualsCharAndBNotEqualsChar_3" + val methodName6 = "testIsTheSameType_AEqualsCharAndBEqualsCharOrAEqualsCharAndBEqualsCharOrAEqualsCharAndBEqualsChar_1" + val methodName7 = "testIsTheSameType_AEqualsCharAndBEqualsCharOrAEqualsCharAndBEqualsCharOrAEqualsCharAndBEqualsChar_2" + + val commonDiplayName1 = "return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']' : False -> return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']'" + val commonDisplayName2 = "return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']' : True -> return a == '(' && b == ')' || a == '{' && b == '}' || a == '[' && b == ']'" + + val summaryKeys = listOf( + commonSummary + ) + + val displayNames = listOf( + commonDiplayName1, + commonDisplayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5, + methodName6, + methodName7 + ) + + val method = CorrectBracketSequences::isTheSameType + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ 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 index d52bc5a02f..ac60b7d8ea 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryReturnExampleTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryReturnExampleTest.kt @@ -3,8 +3,8 @@ package examples.algorithms import examples.SummaryTestCaseGeneratorTest import org.utbot.examples.algorithms.ReturnExample import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate class SummaryReturnExampleTest : SummaryTestCaseGeneratorTest( ReturnExample::class, diff --git a/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummarySortTest.kt b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummarySortTest.kt new file mode 100644 index 0000000000..416c868587 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummarySortTest.kt @@ -0,0 +1,57 @@ +package examples.algorithms + +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.utbot.examples.algorithms.Sort +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate + +class SummarySortTest : SummaryTestCaseGeneratorTest( + Sort::class, +) { + @Test + fun testDefaultSort() { + val summary1 = "Test \n" + + "throws NullPointerException when: array.length < 4\n" + val summary2 = "Test executes conditions:\n" + + " (array.length < 4): True\n" + + "throws IllegalArgumentException when: array.length < 4\n" + val summary3 = "Test executes conditions:\n" + + " (array.length < 4): False\n" + + "invokes:\n" + + " {@link java.util.Arrays#sort(int[])} once\n" + + "returns from: return array;\n" + + val methodName1 = "testDefaultSort_ThrowNullPointerException" + val methodName2 = "testDefaultSort_ThrowIllegalArgumentException" + val methodName3 = "testDefaultSort_ArrayLengthGreaterOrEqual4" + + val displayName1 = "array.length < 4 -> ThrowNullPointerException" + val displayName2 = "array.length < 4 -> ThrowIllegalArgumentException" + val displayName3 = "array.length < 4 : False -> return array" + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + val method = Sort::defaultSort + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/collections/SummaryListWrapperReturnsVoidTest.kt b/utbot-summary-tests/src/test/kotlin/examples/collections/SummaryListWrapperReturnsVoidTest.kt index 787c630847..e95a5940f4 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/collections/SummaryListWrapperReturnsVoidTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/collections/SummaryListWrapperReturnsVoidTest.kt @@ -2,9 +2,9 @@ package examples.collections import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.collections.ListWrapperReturnsVoidExample import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate /** * Tests verify that the previously discovered bug is not reproducible anymore. @@ -16,10 +16,12 @@ class SummaryListWrapperReturnsVoidTest : SummaryTestCaseGeneratorTest( ) { @Test fun testRunForEach() { - val summary1 = "Test throws NullPointerException in: list.forEach(o -> {\n" + + val summary1 = "Test invokes:\n" + + " {@link java.util.List#forEach(java.util.function.Consumer)} once\n" + + "throws NullPointerException in: list.forEach(o -> {\n" + " if (o == null)\n" + " i[0]++;\n" + - "});" + "});\n" val summary2 = "Test returns from: return i[0];" val summary3 = "Test returns from: return i[0];" val summary4 = "Test returns from: return i[0];" @@ -86,7 +88,7 @@ class SummaryListWrapperReturnsVoidTest : SummaryTestCaseGeneratorTest( "returns from: return sum[0];" val methodName1 = "testSumPositiveForEach_ThrowNullPointerException" - val methodName2 = "testSumPositiveForEach_ListForEach" + val methodName2 = "testSumPositiveForEach_ThrowNullPointerException_1" val methodName3 = "testSumPositiveForEach_0OfSumEqualsZero" val methodName4 = "testSumPositiveForEach_0OfSumEqualsZero_1" val methodName5 = "testSumPositiveForEach_0OfSumNotEqualsZero" diff --git a/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryConditionsTest.kt b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryConditionsTest.kt index 239951a80c..39f854f67e 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryConditionsTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryConditionsTest.kt @@ -4,7 +4,7 @@ import examples.CustomJavaDocTagsEnabler import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.utbot.tests.infrastructure.DoNotCalculate +import org.utbot.testing.DoNotCalculate import org.utbot.examples.controlflow.Conditions import org.utbot.framework.plugin.api.MockStrategyApi @@ -51,4 +51,101 @@ class SummaryConditionsTest : SummaryTestCaseGeneratorTest( summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) } -} \ No newline at end of file + + @Test + fun testReturnCastFromTernaryOperator() { + val summary1 = "@utbot.classUnderTest {@link Conditions}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Conditions#returnCastFromTernaryOperator(long,int)}\n" + + "@utbot.returnsFrom {@code return (int) (a < 0 ? a + b : a);}\n" + val summary2 = "@utbot.classUnderTest {@link Conditions}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Conditions#returnCastFromTernaryOperator(long,int)}\n" + + "@utbot.returnsFrom {@code return (int) (a < 0 ? a + b : a);}\n" + val summary3 = "@utbot.classUnderTest {@link Conditions}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Conditions#returnCastFromTernaryOperator(long,int)}\n" + + "@utbot.throwsException {@link java.lang.ArithmeticException} in: a = a % b;\n" + + val methodName1 = "testReturnCastFromTernaryOperator_A0aba" + val methodName2 = "testReturnCastFromTernaryOperator_A0aba_1" + val methodName3 = "testReturnCastFromTernaryOperator_ThrowArithmeticException" + + val displayName1 = "return (int) (a < 0 ? a + b : a) : False -> return (int) (a < 0 ? a + b : a)" + val displayName2 = "return (int) (a < 0 ? a + b : a) : True -> return (int) (a < 0 ? a + b : a)" + val displayName3 = "a = a % b -> ThrowArithmeticException" + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + val method = Conditions::returnCastFromTernaryOperator + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testElseIf() { + val summary1 = "@utbot.classUnderTest {@link Conditions}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Conditions#elseIf(int)}\n" + + "@utbot.executesCondition {@code (id > 0): True}\n" + + "@utbot.returnsFrom {@code return 0;}" + + val summary2 = "@utbot.classUnderTest {@link Conditions}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Conditions#elseIf(int)}\n" + + "@utbot.executesCondition {@code (id > 0): False}\n" + + "@utbot.executesCondition {@code (id == 0): False}\n" + + "@utbot.returnsFrom {@code return 1;}" + + val summary3 = "@utbot.classUnderTest {@link Conditions}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Conditions#elseIf(int)}\n" + + "@utbot.executesCondition {@code (id > 0): False}\n" + + "@utbot.executesCondition {@code (id == 0): True}\n" + + "@utbot.throwsException {@link java.lang.RuntimeException} when: id == 0" + + val methodName1 = "testElseIf_IdGreaterThanZero" + val methodName2 = "testElseIf_IdNotEqualsZero" + val methodName3 = "testElseIf_ThrowRuntimeException" + + val displayName1 = "id > 0 : True -> id > 0" + val displayName2 = "id > 0 : False -> return 1" + val displayName3 = "id == 0 -> ThrowRuntimeException" + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + val method = Conditions::elseIf + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} diff --git a/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryCycleTest.kt b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryCycleTest.kt index 1f37c2db2b..63c2b84457 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryCycleTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryCycleTest.kt @@ -2,9 +2,9 @@ package examples.controlflow import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.controlflow.Cycles import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate class SummaryCycleTest : SummaryTestCaseGeneratorTest( Cycles::class, diff --git a/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummarySwitchTest.kt b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummarySwitchTest.kt index 13d377a1d1..8b2730593e 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummarySwitchTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummarySwitchTest.kt @@ -5,30 +5,31 @@ import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.utbot.examples.controlflow.Switch +import org.utbot.examples.exceptions.ExceptionExamples import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.tests.infrastructure.DoNotCalculate +import org.utbot.testing.DoNotCalculate @ExtendWith(CustomJavaDocTagsEnabler::class) class SummarySwitchTest : SummaryTestCaseGeneratorTest( Switch::class ) { @Test - fun testDifferentExceptions() { + fun testSimpleSwitch() { val summary1 = "@utbot.classUnderTest {@link Switch}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#simpleSwitch(int)}\n" + - "@utbot.activatesSwitch {@code case 10}\n" + + "@utbot.activatesSwitch {@code switch(x) case: 10}\n" + "@utbot.returnsFrom {@code return 10;}" val summary2 = "@utbot.classUnderTest {@link Switch}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#simpleSwitch(int)}\n" + - "@utbot.activatesSwitch {@code case default}\n" + + "@utbot.activatesSwitch {@code switch(x) case: default}\n" + "@utbot.returnsFrom {@code return -1;}" val summary3 = "@utbot.classUnderTest {@link Switch}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#simpleSwitch(int)}\n" + - "@utbot.activatesSwitch {@code case 12}\n" + + "@utbot.activatesSwitch {@code switch(x) case: 12}\n" + "@utbot.returnsFrom {@code return 12;}" val summary4 = "@utbot.classUnderTest {@link Switch}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#simpleSwitch(int)}\n" + - "@utbot.activatesSwitch {@code case 13}\n" + + "@utbot.activatesSwitch {@code switch(x) case: 13}\n" + "@utbot.returnsFrom {@code return 13;}" val methodName1 = "testSimpleSwitch_Return10" @@ -37,7 +38,7 @@ class SummarySwitchTest : SummaryTestCaseGeneratorTest( val methodName4 = "testSimpleSwitch_Return13" val displayName1 = "switch(x) case: 10 -> return 10" - val displayName2 = "switch(x) case: Default -> return -1" + val displayName2 = "switch(x) case: default -> return -1" val displayName3 = "switch(x) case: 12 -> return 12" val displayName4 = "switch(x) case: 13 -> return 13" @@ -68,4 +69,130 @@ class SummarySwitchTest : SummaryTestCaseGeneratorTest( summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) } -} \ No newline at end of file + + @Test + fun testCharToIntSwitch() { + val summary1 = "@utbot.classUnderTest {@link Switch}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#charToIntSwitch(char)}\n" + + "@utbot.activatesSwitch {@code switch(c) case: 'C'}\n" + + "@utbot.returnsFrom {@code return 100;}\n" + val summary2 = "@utbot.classUnderTest {@link Switch}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#charToIntSwitch(char)}\n" + + "@utbot.activatesSwitch {@code switch(c) case: 'V'}\n" + + "@utbot.returnsFrom {@code return 5;}\n" + val summary3 = "@utbot.classUnderTest {@link Switch}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#charToIntSwitch(char)}\n" + + "@utbot.activatesSwitch {@code switch(c) case: 'I'}\n" + + "@utbot.returnsFrom {@code return 1;}\n" + val summary4 = "@utbot.classUnderTest {@link Switch}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#charToIntSwitch(char)}\n" + + "@utbot.activatesSwitch {@code switch(c) case: 'X'}\n" + + "@utbot.returnsFrom {@code return 10;}\n" + val summary5 = "@utbot.classUnderTest {@link Switch}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#charToIntSwitch(char)}\n" + + "@utbot.activatesSwitch {@code switch(c) case: 'M'}\n" + + "@utbot.returnsFrom {@code return 1000;}\n" + val summary6 = "@utbot.classUnderTest {@link Switch}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#charToIntSwitch(char)}\n" + + "@utbot.activatesSwitch {@code switch(c) case: 'D'}\n" + + "@utbot.returnsFrom {@code return 500;}\n" + val summary7 = "@utbot.classUnderTest {@link Switch}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#charToIntSwitch(char)}\n" + + "@utbot.activatesSwitch {@code switch(c) case: 'L'}\n" + + "@utbot.returnsFrom {@code return 50;}\n" + val summary8 = "@utbot.classUnderTest {@link Switch}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#charToIntSwitch(char)}\n" + + "@utbot.invokes {@link java.lang.StringBuilder#append(java.lang.String)}\n" + + "@utbot.invokes {@link java.lang.StringBuilder#append(char)}\n" + + "@utbot.invokes {@link java.lang.StringBuilder#toString()}\n" + + "@utbot.activatesSwitch {@code switch(c) case: default}\n" + + "@utbot.throwsException {@link java.lang.IllegalArgumentException} when: switch(c) case: default\n" + + val methodName1 = "testCharToIntSwitch_Return100" + val methodName2 = "testCharToIntSwitch_Return5" + val methodName3 = "testCharToIntSwitch_Return1" + val methodName4 = "testCharToIntSwitch_Return10" + val methodName5 = "testCharToIntSwitch_Return1000" + val methodName6 = "testCharToIntSwitch_Return500" + val methodName7 = "testCharToIntSwitch_Return50" + val methodName8 = "testCharToIntSwitch_ThrowIllegalArgumentException" + + val displayName1 = "switch(c) case: 'C' -> return 100" + val displayName2 = "switch(c) case: 'V' -> return 5" + val displayName3 = "switch(c) case: 'I' -> return 1" + val displayName4 = "switch(c) case: 'X' -> return 10" + val displayName5 = "switch(c) case: 'M' -> return 1000" + val displayName6 = "switch(c) case: 'D' -> return 500" + val displayName7 = "switch(c) case: 'L' -> return 50" + val displayName8 = "switch(c) case: default -> ThrowIllegalArgumentException" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5, + summary6, + summary7, + summary8, + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5, + displayName6, + displayName7, + displayName8, + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5, + methodName6, + methodName7, + methodName8, + ) + + val method = Switch::charToIntSwitch + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testThrowExceptionInSwitchArgument() { + val summary1 = "@utbot.classUnderTest {@link Switch}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.controlflow.Switch#throwExceptionInSwitchArgument()}\n" + + "@utbot.invokes org.utbot.examples.controlflow.Switch#getChar()\n" + + "@utbot.throwsException {@link java.lang.RuntimeException} in: switch(getChar())\n" + + val methodName1 = "testThrowExceptionInSwitchArgument_ThrowRuntimeException" + + val displayName1 = "switch(getChar()) -> ThrowRuntimeException" + + val summaryKeys = listOf( + summary1, + ) + + val displayNames = listOf( + displayName1, + ) + + val methodNames = listOf( + methodName1, + ) + + val method = Switch::throwExceptionInSwitchArgument + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} diff --git a/utbot-summary-tests/src/test/kotlin/examples/enums/SummaryComplexEnumExampleTest.kt b/utbot-summary-tests/src/test/kotlin/examples/enums/SummaryComplexEnumExampleTest.kt new file mode 100644 index 0000000000..66021da212 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/enums/SummaryComplexEnumExampleTest.kt @@ -0,0 +1,137 @@ +package examples.enums + +import examples.CustomJavaDocTagsEnabler +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.utbot.examples.enums.ComplexEnumExamples +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate + +@ExtendWith(CustomJavaDocTagsEnabler::class) +class SummaryComplexEnumExampleTest : SummaryTestCaseGeneratorTest( + ComplexEnumExamples::class +) { + @Test + fun testUnsafeWithField() { + val summary1 = "@utbot.classUnderTest {@link ComplexEnumExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.enums.ComplexEnumExamples#countEqualColors(org.utbot.examples.enums.ComplexEnumExamples.Color,org.utbot.examples.enums.ComplexEnumExamples.Color,org.utbot.examples.enums.ComplexEnumExamples.Color)}\n" + + "@utbot.executesCondition {@code (b == a): False}\n" + + "@utbot.executesCondition {@code (c == a): False}\n" + + "@utbot.executesCondition {@code (a == b): False}\n" + + "@utbot.executesCondition {@code (c == b): False}\n" + + "@utbot.executesCondition {@code (equalToA > equalToB): False}\n" + + "@utbot.returnsFrom {@code return equalToB;}" + val summary2 = "@utbot.classUnderTest {@link ComplexEnumExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.enums.ComplexEnumExamples#countEqualColors(org.utbot.examples.enums.ComplexEnumExamples.Color,org.utbot.examples.enums.ComplexEnumExamples.Color,org.utbot.examples.enums.ComplexEnumExamples.Color)}\n" + + "@utbot.executesCondition {@code (b == a): False}\n" + + "@utbot.executesCondition {@code (c == a): True}\n" + + "@utbot.executesCondition {@code (a == b): False}\n" + + "@utbot.executesCondition {@code (c == b): False}\n" + + "@utbot.executesCondition {@code (equalToA > equalToB): True}\n" + + "@utbot.returnsFrom {@code return equalToA;}\n" + val summary3 = "@utbot.classUnderTest {@link ComplexEnumExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.enums.ComplexEnumExamples#countEqualColors(org.utbot.examples.enums.ComplexEnumExamples.Color,org.utbot.examples.enums.ComplexEnumExamples.Color,org.utbot.examples.enums.ComplexEnumExamples.Color)}\n" + + "@utbot.executesCondition {@code (b == a): False}\n" + + "@utbot.executesCondition {@code (c == a): False}\n" + + "@utbot.executesCondition {@code (a == b): False}\n" + + "@utbot.executesCondition {@code (c == b): True}\n" + + "@utbot.executesCondition {@code (equalToA > equalToB): False}\n" + + "@utbot.returnsFrom {@code return equalToB;}\n" + val summary4 = "@utbot.classUnderTest {@link ComplexEnumExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.enums.ComplexEnumExamples#countEqualColors(org.utbot.examples.enums.ComplexEnumExamples.Color,org.utbot.examples.enums.ComplexEnumExamples.Color,org.utbot.examples.enums.ComplexEnumExamples.Color)}\n" + + "@utbot.executesCondition {@code (b == a): True}\n" + + "@utbot.executesCondition {@code (c == a): True}\n" + + "@utbot.executesCondition {@code (a == b): True}\n" + + "@utbot.executesCondition {@code (c == b): True}\n" + + "@utbot.executesCondition {@code (equalToA > equalToB): False}\n" + + "@utbot.returnsFrom {@code return equalToB;}" + + val methodName1 = "testCountEqualColors_EqualToALessOrEqualEqualToB" + val methodName2 = "testCountEqualColors_EqualToAGreaterThanEqualToB" + val methodName3 = "testCountEqualColors_EqualToALessOrEqualEqualToB_1" + val methodName4 = "testCountEqualColors_AEqualsB" + + val displayName1 = "b == a : False -> return equalToB" + val displayName2 = "equalToA > equalToB : True -> return equalToA" + val displayName3 = "b == a : False -> return equalToB" + val displayName4 = "b == a : True -> return equalToB" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + + val method = ComplexEnumExamples::countEqualColors + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testFindState() { + val summary1 = "@utbot.classUnderTest {@link ComplexEnumExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.enums.ComplexEnumExamples#findState(int)}\n" + + "@utbot.invokes {@link org.utbot.examples.enums.State#findStateByCode(int)}\n" + + "@utbot.returnsFrom {@code return State.findStateByCode(code);}\n" + val summary2 = "@utbot.classUnderTest {@link ComplexEnumExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.enums.ComplexEnumExamples#findState(int)}\n" + + "@utbot.invokes {@link org.utbot.examples.enums.State#findStateByCode(int)}\n" + + "@utbot.returnsFrom {@code return State.findStateByCode(code);}\n" + val summary3 = "@utbot.classUnderTest {@link ComplexEnumExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.enums.ComplexEnumExamples#findState(int)}\n" + + "@utbot.invokes {@link org.utbot.examples.enums.State#findStateByCode(int)}\n" + + "@utbot.returnsFrom {@code return State.findStateByCode(code);}\n" + + val methodName1 = "testFindState_ReturnStateFindStateByCode" + val methodName2 = "testFindState_ReturnStateFindStateByCode_1" + val methodName3 = "testFindState_ReturnStateFindStateByCode_2" + + val displayName1 = "-> return State.findStateByCode(code)" + val displayName2 = "-> return State.findStateByCode(code)" + val displayName3 = "-> return State.findStateByCode(code)" + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + + val method = ComplexEnumExamples::findState + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionClusteringExamplesTest.kt b/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionClusteringExamplesTest.kt index f2a7ede1ab..3751f75dd0 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionClusteringExamplesTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionClusteringExamplesTest.kt @@ -4,7 +4,7 @@ import examples.CustomJavaDocTagsEnabler import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.utbot.tests.infrastructure.DoNotCalculate +import org.utbot.testing.DoNotCalculate import org.utbot.examples.exceptions.ExceptionClusteringExamples import org.utbot.framework.plugin.api.MockStrategyApi @@ -23,13 +23,13 @@ class SummaryExceptionClusteringExamplesTest : SummaryTestCaseGeneratorTest( "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionClusteringExamples#differentExceptions(int)}\n" + "@utbot.executesCondition {@code (i == 0): False}\n" + "@utbot.executesCondition {@code (i == 1): True}\n" + - "@utbot.throwsException {@link org.utbot.examples.exceptions.MyCheckedException} after condition: i == 1" + "@utbot.throwsException {@link org.utbot.examples.exceptions.MyCheckedException} when: i == 1" val summary3 = "@utbot.classUnderTest {@link ExceptionClusteringExamples}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionClusteringExamples#differentExceptions(int)}\n" + "@utbot.executesCondition {@code (i == 0): False}\n" + "@utbot.executesCondition {@code (i == 1): False}\n" + "@utbot.executesCondition {@code (i == 2): True}\n" + - "@utbot.throwsException {@link java.lang.IllegalArgumentException} after condition: i == 2" + "@utbot.throwsException {@link java.lang.IllegalArgumentException} when: i == 2" val summary4 = "@utbot.classUnderTest {@link ExceptionClusteringExamples}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionClusteringExamples#differentExceptions(int)}\n" + "@utbot.executesCondition {@code (i == 0): False}\n" + @@ -37,9 +37,9 @@ class SummaryExceptionClusteringExamplesTest : SummaryTestCaseGeneratorTest( "@utbot.executesCondition {@code (i == 2): False}\n" + "@utbot.returnsFrom {@code return i * 2;}\n" - val methodName1 = "testDifferentExceptions_IEqualsZero" - val methodName2 = "testDifferentExceptions_IEquals1" - val methodName3 = "testDifferentExceptions_IEquals2" + val methodName1 = "testDifferentExceptions_ThrowArithmeticException" + val methodName2 = "testDifferentExceptions_ThrowMyCheckedException" + val methodName3 = "testDifferentExceptions_ThrowIllegalArgumentException" val methodName4 = "testDifferentExceptions_INotEquals2" val displayName1 = "return 100 / i : True -> ThrowArithmeticException" diff --git a/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionExampleTest.kt b/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionExampleTest.kt index 5dfca3db48..8d054cfdce 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionExampleTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/exceptions/SummaryExceptionExampleTest.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.utbot.examples.exceptions.ExceptionExamples import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.tests.infrastructure.DoNotCalculate +import org.utbot.testing.DoNotCalculate @ExtendWith(CustomJavaDocTagsEnabler::class) class SummaryExceptionExampleTest : SummaryTestCaseGeneratorTest( @@ -58,4 +58,72 @@ class SummaryExceptionExampleTest : SummaryTestCaseGeneratorTest( summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) } + + @Test + fun testHangForSeconds() { + val summary1 = "@utbot.classUnderTest {@link ExceptionExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionExamples#hangForSeconds(int)}\n" + + "@utbot.returnsFrom {@code return seconds;}\n" + val summary2 = "@utbot.classUnderTest {@link ExceptionExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionExamples#hangForSeconds(int)}\n" + + "@utbot.iterates iterate the loop {@code for(int i = 0; i < seconds; i++)} once\n" + + "@utbot.returnsFrom {@code return seconds;}\n" + + "@utbot.detectsSuspiciousBehavior in: return seconds;\n" + + val methodName1 = "testHangForSeconds_ReturnSeconds" + val methodName2 = "testHangForSeconds_ThreadSleep" + + val displayName1 = "-> return seconds" + val displayName2 = "return seconds -> TimeoutExceeded" + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + val method = ExceptionExamples::hangForSeconds + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testThrowExceptionInMethodUnderTest() { + val summary1 = "@utbot.classUnderTest {@link ExceptionExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.exceptions.ExceptionExamples#throwExceptionInMethodUnderTest()}\n" + + "@utbot.throwsException {@link java.lang.RuntimeException} in: throw new RuntimeException(\"Exception message\");\n" + + val methodName1 = "testThrowExceptionInMethodUnderTest_ThrowRuntimeException" + + val displayName1 = " -> ThrowRuntimeException" + + val summaryKeys = listOf( + summary1, + ) + + val displayNames = listOf( + displayName1, + ) + + val methodNames = listOf( + methodName1, + ) + + val method = ExceptionExamples::throwExceptionInMethodUnderTest + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(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 index d264d2c3f6..b6072aa076 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryInnerCallsTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryInnerCallsTest.kt @@ -2,9 +2,9 @@ package examples.inner import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.inner.InnerCalls import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( InnerCalls::class, @@ -146,7 +146,7 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( val methodName4 = "testCallLeftBinSearch_Found" val methodName5 = "testCallLeftBinSearch_ThrowIllegalArgumentException" val methodName6 = "testCallLeftBinSearch_ThrowIllegalArgumentException_1" - val methodName7 = "testCallLeftBinSearch_BinarySearchIsUnsorted" + val methodName7 = "testCallLeftBinSearch_ThrowNullPointerException" val displayName1 = "found : False -> return -1" @@ -327,9 +327,9 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( " triggers recursion of factorial once, \n" + "Test throws IllegalArgumentException in: return r.factorial(n);\n" - val methodName1 = "testCallFactorial_NEqualsZero" + val methodName1 = "testCallFactorial_ThrowIllegalArgumentException" val methodName2 = "testCallFactorial_NNotEqualsZero" - val methodName3 = "testCallFactorial_NLessThanZero" + val methodName3 = "testCallFactorial_NEqualsZero" val displayName1 = "n == 0 : True -> return 1" val displayName2 = "n == 0 : False -> return n * factorial(n - 1)" @@ -384,8 +384,8 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( " \n" + "Test then returns from: return invokeExample.simpleFormula(f, s);\n" - val methodName1 = "testCallSimpleInvoke_SndLessThan100" - val methodName2 = "testCallSimpleInvoke_FstLessThan100" + val methodName1 = "testCallSimpleInvoke_ThrowIllegalArgumentException" + val methodName2 = "testCallSimpleInvoke_ThrowIllegalArgumentException_1" val methodName3 = "testCallSimpleInvoke_SndGreaterOrEqual100" val displayName1 = "return invokeExample.simpleFormula(f, s) : True -> ThrowIllegalArgumentException" @@ -455,8 +455,8 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( " \n" + "Test afterwards returns from: return stringExamples.indexOf(s, key);\n" - val methodName1 = "testCallStringExample_StringIndexOf" - val methodName2 = "testCallStringExample_StringIndexOf_1" + val methodName1 = "testCallStringExample_ThrowNullPointerException" + val methodName2 = "testCallStringExample_ThrowNullPointerException_1" val methodName3 = "testCallStringExample_IEqualsZero" val methodName4 = "testCallStringExample_INotEqualsZero" val methodName5 = "testCallStringExample_IGreaterThanZero" @@ -501,16 +501,16 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( @Test fun testCallSimpleSwitch() { val summary1 = "Test calls {@link org.utbot.examples.controlflow.Switch#simpleSwitch(int)},\n" + - " there it activates switch case: 12, returns from: return 12;\n" + + " there it activates switch(x) case: 12, returns from: return 12;\n" + " " val summary2 = "Test calls {@link org.utbot.examples.controlflow.Switch#simpleSwitch(int)},\n" + - " there it activates switch case: 13, returns from: return 13;\n" + + " there it activates switch(x) case: 13, returns from: return 13;\n" + " " val summary3 = "Test calls {@link org.utbot.examples.controlflow.Switch#simpleSwitch(int)},\n" + - " there it activates switch case: 10, returns from: return 10;\n" + + " there it activates switch(x) case: 10, returns from: return 10;\n" + " " val summary4 = "Test calls {@link org.utbot.examples.controlflow.Switch#simpleSwitch(int)},\n" + - " there it activates switch case: default, returns from: return -1;\n" + + " there it activates switch(x) case: default, returns from: return -1;\n" + " " val methodName1 = "testCallSimpleSwitch_Return12" @@ -521,7 +521,7 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( 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 displayName4 = "switch(x) case: default -> return -1" val method = InnerCalls::callSimpleSwitch val mockStrategy = MockStrategyApi.NO_MOCKS @@ -554,16 +554,16 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( @Test fun testCallLookup() { val summary1 = "Test calls {@link org.utbot.examples.controlflow.Switch#lookupSwitch(int)},\n" + - " there it activates switch case: 20, returns from: return 20;\n" + + " there it activates switch(x) case: 20, returns from: return 20;\n" + " " val summary2 = "Test calls {@link org.utbot.examples.controlflow.Switch#lookupSwitch(int)},\n" + - " there it activates switch case: 30, returns from: return 30;\n" + + " there it activates switch(x) case: 30, returns from: return 30;\n" + " " val summary3 = "Test calls {@link org.utbot.examples.controlflow.Switch#lookupSwitch(int)},\n" + - " there it activates switch case: 0, returns from: return 0;\n" + + " there it activates switch(x) case: 0, returns from: return 0;\n" + " " val summary4 = "Test calls {@link org.utbot.examples.controlflow.Switch#lookupSwitch(int)},\n" + - " there it activates switch case: default, returns from: return -1;\n" + + " there it activates switch(x) case: default, returns from: return -1;\n" + " " val methodName1 = "testCallLookup_Return20" @@ -574,7 +574,7 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( 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 displayName4 = "switch(x) case: default -> return -1" val method = InnerCalls::callLookup val mockStrategy = MockStrategyApi.NO_MOCKS @@ -632,8 +632,8 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( " Test later returns from: return invokeExample.simpleFormula(f, s);\n" + " " - val methodName1 = "testDoubleSimpleInvoke_FstLessThan100" - val methodName2 = "testDoubleSimpleInvoke_SndLessThan100" + val methodName1 = "testDoubleSimpleInvoke_ThrowIllegalArgumentException" + val methodName2 = "testDoubleSimpleInvoke_ThrowIllegalArgumentException_1" val methodName3 = "testDoubleSimpleInvoke_SndGreaterOrEqual100" val displayName1 = "callSimpleInvoke(f, s) : True -> ThrowIllegalArgumentException" @@ -789,9 +789,9 @@ class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( "Test throws IllegalArgumentException in: return r.fib(n);\n" val methodName1 = "testCallFib_NEquals1" - val methodName2 = "testCallFib_NEqualsZero" + val methodName2 = "testCallFib_ThrowIllegalArgumentException" val methodName3 = "testCallFib_NNotEquals1" - val methodName4 = "testCallFib_NLessThanZero" + val methodName4 = "testCallFib_NEqualsZero" val displayName1 = "n == 1 : True -> return 1" val displayName2 = "n == 0 : True -> return 0" diff --git a/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryNestedCallsTest.kt b/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryNestedCallsTest.kt index 19e14e9b41..93216593e4 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryNestedCallsTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryNestedCallsTest.kt @@ -2,9 +2,9 @@ package examples.inner import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.inner.NestedCalls import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate class SummaryNestedCallsTest : SummaryTestCaseGeneratorTest( NestedCalls::class, diff --git a/utbot-summary-tests/src/test/kotlin/examples/mock/SummaryCommonMocksExample.kt b/utbot-summary-tests/src/test/kotlin/examples/mock/SummaryCommonMocksExample.kt new file mode 100644 index 0000000000..77933d82de --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/mock/SummaryCommonMocksExample.kt @@ -0,0 +1,41 @@ +package examples.mock + +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.utbot.examples.mock.CommonMocksExample +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate + +class SummaryCommonMocksExample : SummaryTestCaseGeneratorTest( + CommonMocksExample::class, +) { + @Test + fun testClinitMockExample() { + val summary1 = "Test invokes:\n" + + " {@link java.lang.Integer#intValue()} twice\n" + + "returns from: return -ObjectWithFinalStatic.keyValue;\n" + + val methodName1 = "testClinitMockExample_IntegerIntValue" + + val displayName1 = "IntegerIntValue -> return -ObjectWithFinalStatic.keyValue" + + + val summaryKeys = listOf( + summary1 + ) + + val displayNames = listOf( + displayName1 + ) + + val methodNames = listOf( + methodName1 + ) + + val method = CommonMocksExample::clinitMockExample + val mockStrategy = MockStrategyApi.OTHER_CLASSES + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/nested/SummaryNestedTest.kt b/utbot-summary-tests/src/test/kotlin/examples/nested/SummaryNestedTest.kt new file mode 100644 index 0000000000..0252e3d970 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/nested/SummaryNestedTest.kt @@ -0,0 +1,55 @@ +package examples.nested + +import examples.CustomJavaDocTagsEnabler +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.utbot.examples.nested.DeepNested +import org.utbot.examples.recursion.Recursion +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate + +@ExtendWith(CustomJavaDocTagsEnabler::class) +class SummaryNestedTest : SummaryTestCaseGeneratorTest( + DeepNested.Nested1.Nested2::class +) { + @Test + fun testNested() { + val summary1 = "@utbot.classUnderTest {@link DeepNested.Nested1.Nested2}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.nested.DeepNested.Nested1.Nested2#f(int)}\n" + + "@utbot.executesCondition {@code (i > 0): False}\n" + + "@utbot.returnsFrom {@code return 0;}\n" + + val summary2 = "@utbot.classUnderTest {@link DeepNested.Nested1.Nested2}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.nested.DeepNested.Nested1.Nested2#f(int)}\n" + + "@utbot.executesCondition {@code (i > 0): True}\n" + + "@utbot.returnsFrom {@code return 10;}" + + val methodName1 = "testF_ILessOrEqualZero" + val methodName2 = "testF_IGreaterThanZero" + + val displayName1 = "i > 0 : False -> return 0" + val displayName2 = "i > 0 : True -> return 10" + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + val method = DeepNested.Nested1.Nested2::f + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/objects/SummarySimpleClassExampleTest.kt b/utbot-summary-tests/src/test/kotlin/examples/objects/SummarySimpleClassExampleTest.kt new file mode 100644 index 0000000000..0a2e69bb95 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/objects/SummarySimpleClassExampleTest.kt @@ -0,0 +1,55 @@ +package examples.objects + +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.utbot.examples.objects.SimpleClassExample +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate + +class SummarySimpleClassExampleTest : SummaryTestCaseGeneratorTest( + SimpleClassExample::class, +) { + @Test + fun testImmutableFieldAccess() { + val summary1 = "Test \n" + + "throws NullPointerException when: c.b == 10\n" + val summary2 = "Test executes conditions:\n" + + " (c.b == 10): False\n" + + "returns from: return 1;\n" + val summary3 = "Test executes conditions:\n" + + " (c.b == 10): True\n" + + "returns from: return 0;\n" + + val methodName1 = "testImmutableFieldAccess_ThrowNullPointerException" + val methodName2 = "testImmutableFieldAccess_CBNotEquals10" + val methodName3 = "testImmutableFieldAccess_CBEquals10" + + val displayName1 = "c.b == 10 -> ThrowNullPointerException" + val displayName2 = "c.b == 10 : False -> return 1" + val displayName3 = "c.b == 10 : True -> return 0" + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + val method = SimpleClassExample::immutableFieldAccess + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/recursion/SummaryRecursionTest.kt b/utbot-summary-tests/src/test/kotlin/examples/recursion/SummaryRecursionTest.kt index 283d49141a..7fa6019ff4 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/recursion/SummaryRecursionTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/recursion/SummaryRecursionTest.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.utbot.examples.recursion.Recursion import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.tests.infrastructure.DoNotCalculate +import org.utbot.testing.DoNotCalculate @ExtendWith(CustomJavaDocTagsEnabler::class) class SummaryRecursionTest : SummaryTestCaseGeneratorTest( @@ -26,6 +26,8 @@ class SummaryRecursionTest : SummaryTestCaseGeneratorTest( val summary3 = "@utbot.classUnderTest {@link Recursion}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.recursion.Recursion#fib(int)}\n" + "@utbot.executesCondition {@code (n == 1): False}\n" + + "@utbot.invokes {@link org.utbot.examples.recursion.Recursion#fib(int)}\n" + + "@utbot.invokes {@link org.utbot.examples.recursion.Recursion#fib(int)}\n" + "@utbot.triggersRecursion fib, where the test execute conditions:\n" + " {@code (n == 1): True}\n" + "return from: {@code return 1;}" + @@ -33,12 +35,12 @@ class SummaryRecursionTest : SummaryTestCaseGeneratorTest( val summary4 = "@utbot.classUnderTest {@link Recursion}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.recursion.Recursion#fib(int)}\n" + "@utbot.executesCondition {@code (n < 0): True}\n" + - "@utbot.throwsException {@link java.lang.IllegalArgumentException} in: n < 0" + "@utbot.throwsException {@link java.lang.IllegalArgumentException} when: n < 0" val methodName1 = "testFib_Return1" val methodName2 = "testFib_ReturnZero" val methodName3 = "testFib_NNotEquals1" - val methodName4 = "testFib_NLessThanZero" + val methodName4 = "testFib_ThrowIllegalArgumentException" val displayName1 = "n == 0 : False -> return 1" val displayName2 = "n == 0 : True -> return 0" @@ -84,16 +86,17 @@ class SummaryRecursionTest : SummaryTestCaseGeneratorTest( val summary2 = "@utbot.classUnderTest {@link Recursion}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.recursion.Recursion#factorial(int)}\n" + "@utbot.executesCondition {@code (n == 0): False}\n" + + "@utbot.invokes {@link org.utbot.examples.recursion.Recursion#factorial(int)}\n" + "@utbot.triggersRecursion factorial, where the test return from: {@code return 1;}" + "@utbot.returnsFrom {@code return n * factorial(n - 1);}" val summary3 = "@utbot.classUnderTest {@link Recursion}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.recursion.Recursion#factorial(int)}\n" + "@utbot.executesCondition {@code (n < 0): True}\n" + - "@utbot.throwsException {@link java.lang.IllegalArgumentException} after condition: n < 0" + "@utbot.throwsException {@link java.lang.IllegalArgumentException} when: n < 0" val methodName1 = "testFactorial_Return1" val methodName2 = "testFactorial_NNotEqualsZero" - val methodName3 = "testFactorial_NLessThanZero" + val methodName3 = "testFactorial_ThrowIllegalArgumentException" //TODO: Display names are not complete, see [issue-899](https://github.com/UnitTestBot/UTBotJava/issues/899). //they should be equal "n == 0 : True -> return 1" and "n == 0 : False -> return n * factorial(n - 1)" respectively diff --git a/utbot-summary-tests/src/test/kotlin/examples/structures/SummaryMinStackTest.kt b/utbot-summary-tests/src/test/kotlin/examples/structures/SummaryMinStackTest.kt index 0b8d9f35f5..c920c1bf5f 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/structures/SummaryMinStackTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/structures/SummaryMinStackTest.kt @@ -4,10 +4,9 @@ import examples.CustomJavaDocTagsEnabler import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.structures.MinStack import org.utbot.framework.plugin.api.MockStrategyApi - +import org.utbot.testing.DoNotCalculate @ExtendWith(CustomJavaDocTagsEnabler::class) class SummaryMinStackTest : SummaryTestCaseGeneratorTest( MinStack::class @@ -64,13 +63,13 @@ class SummaryMinStackTest : SummaryTestCaseGeneratorTest( val summary1 = "@utbot.classUnderTest {@link MinStack}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#removeValue()}\n" + "@utbot.executesCondition {@code (size <= 0): True}\n" + - "@utbot.throwsException {@link java.lang.RuntimeException} after condition: size <= 0" + "@utbot.throwsException {@link java.lang.RuntimeException} when: size <= 0" val summary2 = "@utbot.classUnderTest {@link MinStack}\n" + "@utbot.methodUnderTest {@link org.utbot.examples.structures.MinStack#removeValue()}\n" + "@utbot.executesCondition {@code (size <= 0): False}\n" - val methodName1 = "testRemoveValue_SizeLessOrEqualZero" + val methodName1 = "testRemoveValue_ThrowRuntimeException" val methodName2 = "testRemoveValue_SizeGreaterThanZero" val displayName1 = "size <= 0 -> ThrowRuntimeException" @@ -146,7 +145,7 @@ class SummaryMinStackTest : SummaryTestCaseGeneratorTest( val methodName4 = "testAddValue_ThrowNullPointerException_1" val methodName5 = "testAddValue_ThrowNullPointerException_2" val methodName6 = "testAddValue_ThrowArrayIndexOutOfBoundsException_2" - val methodName7 = "testAddValue_MathMin" + val methodName7 = "testAddValue_ThrowArrayIndexOutOfBoundsException_3" val methodName8 = "testAddValue_SizeEqualsZero" val methodName9 = "testAddValue_SizeNotEqualsZero" diff --git a/utbot-summary-tests/src/test/kotlin/examples/ternary/SummaryTernaryTest.kt b/utbot-summary-tests/src/test/kotlin/examples/ternary/SummaryTernaryTest.kt index cf8c456096..c6c5b6dadb 100644 --- a/utbot-summary-tests/src/test/kotlin/examples/ternary/SummaryTernaryTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/ternary/SummaryTernaryTest.kt @@ -2,10 +2,9 @@ package examples.ternary import examples.SummaryTestCaseGeneratorTest import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.examples.ternary.Ternary import org.utbot.framework.plugin.api.MockStrategyApi - +import org.utbot.testing.DoNotCalculate class SummaryTernaryTest : SummaryTestCaseGeneratorTest( Ternary::class, ) { @@ -145,7 +144,7 @@ class SummaryTernaryTest : SummaryTestCaseGeneratorTest( val methodName1 = "testParse_InputEqualsNullOrInputEquals" val methodName2 = "testParse_InputNotEqualsNullOrInputEquals" - val methodName3 = "testParse_InputEqualsNullOrInputEquals_1" + val methodName3 = "testParse_ThrowNumberFormatException" val displayName1 = "input == null || input.equals(\"\") : False -> return value" val displayName2 = "input == null || input.equals(\"\") : True -> return value" diff --git a/utbot-summary-tests/src/test/kotlin/examples/unsafe/UnsafeWithFieldTest.kt b/utbot-summary-tests/src/test/kotlin/examples/unsafe/UnsafeWithFieldTest.kt new file mode 100644 index 0000000000..ad0c12c0ff --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/unsafe/UnsafeWithFieldTest.kt @@ -0,0 +1,43 @@ +package examples.unsafe + +import examples.CustomJavaDocTagsEnabler +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.utbot.examples.unsafe.UnsafeWithField +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testing.DoNotCalculate + +@ExtendWith(CustomJavaDocTagsEnabler::class) +class UnsafeWithFieldTest : SummaryTestCaseGeneratorTest( + UnsafeWithField::class +) { + @Test + fun testUnsafeWithField() { + val summary1 = "@utbot.classUnderTest {@link UnsafeWithField}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.unsafe.UnsafeWithField#setField(java.text.NumberFormat.Field)}\n" + + "@utbot.returnsFrom {@code return Field.INTEGER;}" + + val methodName1 = "testSetField_ReturnFieldINTEGER" + + val displayName1 = "-> return Field.INTEGER" + + val summaryKeys = listOf( + summary1 + ) + + val displayNames = listOf( + displayName1 + ) + + val methodNames = listOf( + methodName1 + ) + + val method = UnsafeWithField::setField + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/math/SummaryIntMathLogTest.kt b/utbot-summary-tests/src/test/kotlin/math/SummaryIntMathLogTest.kt new file mode 100644 index 0000000000..bc3b2832c4 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/math/SummaryIntMathLogTest.kt @@ -0,0 +1,71 @@ +package math + +import examples.CustomJavaDocTagsEnabler +import examples.SummaryTestCaseGeneratorTest +import guava.examples.math.IntMath +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.framework.plugin.api.UtClusterInfo +import org.utbot.testing.DoNotCalculate + +@ExtendWith(CustomJavaDocTagsEnabler::class) +class SummaryIntMathLogTest : SummaryTestCaseGeneratorTest( + IntMath::class, +) { + @Test + fun testLog2() { + val summary1 = "@utbot.classUnderTest {@link IntMath}\n" + + "@utbot.methodUnderTest {@link guava.examples.math.IntMath#log2(int,java.math.RoundingMode)}\n" + val summary2 = "@utbot.classUnderTest {@link IntMath}\n" + + "@utbot.methodUnderTest {@link guava.examples.math.IntMath#log2(int,java.math.RoundingMode)}\n" + val summary3 = "@utbot.classUnderTest {@link IntMath}\n" + + "@utbot.methodUnderTest {@link guava.examples.math.IntMath#log2(int,java.math.RoundingMode)}\n" + val summary4 = "@utbot.classUnderTest {@link IntMath}\n" + + "@utbot.methodUnderTest {@link guava.examples.math.IntMath#log2(int,java.math.RoundingMode)}\n" + + "@utbot.invokes {@link java.math.RoundingMode#ordinal()}\n" + + "@utbot.throwsException {@link java.lang.NullPointerException} in: switch(mode)" + + val methodName1 = "testLog2_IntegerNumberOfLeadingZeros" + val methodName2 = "testLog2_IntegerNumberOfLeadingZeros_1" + val methodName3 = "testLog2_IntMathLessThanBranchFree" + val methodName4 = "testLog2_ThrowNullPointerException" + + val displayName1 = "switch(mode) case: FLOOR -> return (Integer.SIZE - 1) - Integer.numberOfLeadingZeros(x)" + val displayName2 = "switch(mode) case: CEILING -> return Integer.SIZE - Integer.numberOfLeadingZeros(x - 1)" + val displayName3 = "switch(mode) case: HALF_EVEN -> return logFloor + lessThanBranchFree(cmp, x)" + val displayName4 = "switch(mode) -> ThrowNullPointerException" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + val clusterInfo = listOf( + Pair(UtClusterInfo("SYMBOLIC EXECUTION: SUCCESSFUL EXECUTIONS for method log2(int, java.math.RoundingMode)", null), 3), + Pair(UtClusterInfo("SYMBOLIC EXECUTION: ERROR SUITE for method log2(int, java.math.RoundingMode)", null), 1) + ) + + val method = IntMath::log2 + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames, clusterInfo) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/math/SummaryIntMathPowTest.kt b/utbot-summary-tests/src/test/kotlin/math/SummaryIntMathPowTest.kt new file mode 100644 index 0000000000..788874ecaa --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/math/SummaryIntMathPowTest.kt @@ -0,0 +1,148 @@ +package math + +import examples.SummaryTestCaseGeneratorTest +import guava.examples.math.IntMath +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.framework.plugin.api.UtClusterInfo +import org.utbot.testing.DoNotCalculate +class SummaryIntMathPowTest : SummaryTestCaseGeneratorTest( + IntMath::class, +) { + @Test + fun testPow() { + val summary1 = "Test activates switch(b) case: 1, 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: 1 -> 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 clusterInfo = listOf( + Pair(UtClusterInfo("SYMBOLIC EXECUTION: SUCCESSFUL EXECUTIONS for method pow(int, int)", null), 14) + ) + + val method = IntMath::pow + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames, clusterInfo) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/math/SummaryIntMathTest.kt b/utbot-summary-tests/src/test/kotlin/math/SummaryIntMathTest.kt deleted file mode 100644 index 6eb31b4504..0000000000 --- a/utbot-summary-tests/src/test/kotlin/math/SummaryIntMathTest.kt +++ /dev/null @@ -1,149 +0,0 @@ -package math - -import examples.SummaryTestCaseGeneratorTest -import guava.examples.math.IntMath -import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate -import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.framework.plugin.api.UtClusterInfo - -class SummaryIntMathTest : 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 clusterInfo = listOf( - Pair(UtClusterInfo("SYMBOLIC EXECUTION: SUCCESSFUL EXECUTIONS for method pow(int, int)", null), 14) - ) - - val method = IntMath::pow - val mockStrategy = MockStrategyApi.NO_MOCKS - val coverage = DoNotCalculate - - summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames, clusterInfo) - } -} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/math/SummaryOfMathTest.kt b/utbot-summary-tests/src/test/kotlin/math/SummaryOfMathTest.kt index 50b84e1c05..a74203cf99 100644 --- a/utbot-summary-tests/src/test/kotlin/math/SummaryOfMathTest.kt +++ b/utbot-summary-tests/src/test/kotlin/math/SummaryOfMathTest.kt @@ -4,9 +4,9 @@ import examples.SummaryTestCaseGeneratorTest import guava.examples.math.Stats import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.utbot.tests.infrastructure.DoNotCalculate import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.UtClusterInfo +import org.utbot.testing.DoNotCalculate /** * It runs test generation for the poor analogue of the Stats.of method ported from the guava-26.0 framework @@ -18,7 +18,7 @@ class SummaryOfMathTest : SummaryTestCaseGeneratorTest( Stats::class, ) { @Test - @Disabled + @Disabled("Test fails, https://github.com/UnitTestBot/UTBotJava/issues/826") fun testOfInts() { val summary1 = "Test calls {@link guava.examples.math.StatsAccumulator#addAll(int[])},\n" + " there it triggers recursion of addAll once, \n" + @@ -86,6 +86,7 @@ class SummaryOfMathTest : SummaryTestCaseGeneratorTest( } @Test + @Disabled("Test is flaky, https://github.com/UnitTestBot/UTBotJava/issues/826") fun testOfDoubles() { val summary1 = "Test calls {@link guava.examples.math.StatsAccumulator#addAll(double[])},\n" + " there it triggers recursion of addAll once, \n" + @@ -232,7 +233,9 @@ class SummaryOfMathTest : SummaryTestCaseGeneratorTest( " {@code (count == 0): True}\n" + " invoke:\n" + " {@link guava.examples.math.StatsAccumulator#isFinite(double)} twice\n" + - "Tests next execute conditions:\n" + + "Tests later invoke:\n" + + " {@link guava.examples.math.StatsAccumulator#add(double)} once\n" + + "execute conditions:\n" + " {@code (null): False}\n" + "call {@link guava.examples.math.StatsAccumulator#isFinite(double)},\n" + " there it invoke:\n" + diff --git a/utbot-summary-tests/src/test/kotlin/math/SummaryOverflowExamples.kt b/utbot-summary-tests/src/test/kotlin/math/SummaryOverflowExamples.kt new file mode 100644 index 0000000000..e67605af5e --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/math/SummaryOverflowExamples.kt @@ -0,0 +1,54 @@ +package math + +import examples.CustomJavaDocTagsEnabler +import examples.SummaryTestCaseGeneratorTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.utbot.examples.math.OverflowExamples +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.testcheckers.withTreatingOverflowAsError +import org.utbot.testing.DoNotCalculate + +@ExtendWith(CustomJavaDocTagsEnabler::class) +class SummaryOverflowExamples : SummaryTestCaseGeneratorTest( + OverflowExamples::class +) { + @Test + fun testShortMulOverflow() { + val summary1 = "@utbot.classUnderTest {@link OverflowExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.math.OverflowExamples#shortMulOverflow(short,short)}\n" + + "@utbot.returnsFrom {@code return (short) (x * y);}\n" + val summary2 = "@utbot.classUnderTest {@link OverflowExamples}\n" + + "@utbot.methodUnderTest {@link org.utbot.examples.math.OverflowExamples#shortMulOverflow(short,short)}\n" + + "@utbot.detectsSuspiciousBehavior in: return (short) (x * y);\n" + + val methodName1 = "testShortMulOverflow_ReturnXy" + val methodName2 = "testShortMulOverflow_DetectOverflow" + + val displayName1 = "-> return (short) (x * y)" + val displayName2 = "return (short) (x * y) : True -> DetectOverflow" + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + val method = OverflowExamples::shortMulOverflow + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + withTreatingOverflowAsError { + summaryCheck(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + } +} \ No newline at end of file diff --git a/utbot-summary/build.gradle.kts b/utbot-summary/build.gradle.kts index 6fe1cdd86a..3c41ca537d 100644 --- a/utbot-summary/build.gradle.kts +++ b/utbot-summary/build.gradle.kts @@ -1,18 +1,20 @@ val kotlinLoggingVersion: String by rootProject val junit4Version: String by rootProject val junit5Version: String by rootProject -val sootCommitHash: String by rootProject +val sootVersion: String by rootProject val mockitoVersion: String by rootProject dependencies { implementation(project(":utbot-framework-api")) - implementation("com.github.UnitTestBot:soot:${sootCommitHash}") - implementation(project(":utbot-fuzzers")) + implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { + exclude(group="com.google.guava", module="guava") + } + implementation(project(":utbot-java-fuzzing")) implementation(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") implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) implementation("com.github.javaparser:javaparser-core:3.22.1") - testImplementation("org.mockito:mockito-core:4.2.0") + testImplementation("org.mockito:mockito-core:$mockitoVersion") testImplementation("org.junit.jupiter:junit-jupiter:$junit5Version") } diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/AbstractTextBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/AbstractTextBuilder.kt index f15ca4f9c2..02c389b6f1 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/AbstractTextBuilder.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/AbstractTextBuilder.kt @@ -5,16 +5,16 @@ import com.github.javaparser.ast.expr.VariableDeclarationExpr import com.github.javaparser.ast.stmt.ExpressionStmt import com.github.javaparser.ast.stmt.ForEachStmt import com.github.javaparser.ast.stmt.ForStmt +import com.github.javaparser.ast.stmt.IfStmt import com.github.javaparser.ast.stmt.Statement -import com.github.javaparser.ast.stmt.SwitchEntry import com.github.javaparser.ast.stmt.SwitchStmt import com.github.javaparser.ast.stmt.WhileStmt import org.utbot.framework.plugin.api.Step import org.utbot.summary.ast.JimpleToASTMap -import org.utbot.summary.comment.IterationDescription -import org.utbot.summary.comment.SimpleSentenceBlock -import org.utbot.summary.comment.StmtDescription -import org.utbot.summary.comment.StmtType +import org.utbot.summary.comment.classic.symbolic.IterationDescription +import org.utbot.summary.comment.classic.symbolic.SimpleSentenceBlock +import org.utbot.summary.comment.classic.symbolic.StmtDescription +import org.utbot.summary.comment.classic.symbolic.StmtType import org.utbot.summary.comment.getTextIterationDescription import org.utbot.summary.comment.getTextTypeIterationDescription import org.utbot.summary.comment.numberWithSuffix @@ -23,8 +23,7 @@ import org.utbot.summary.tag.TraceTagWithoutExecution import soot.SootMethod import soot.jimple.Stmt import soot.jimple.internal.JIfStmt -import soot.jimple.internal.JLookupSwitchStmt -import soot.jimple.internal.JTableSwitchStmt +import soot.jimple.internal.JReturnStmt abstract class AbstractTextBuilder( val traceTag: TraceTagWithoutExecution, @@ -57,7 +56,6 @@ abstract class AbstractTextBuilder( } } - protected fun textReturn( statementTag: StatementTag, sentenceBlock: SimpleSentenceBlock, @@ -77,49 +75,22 @@ abstract class AbstractTextBuilder( returnType = StmtType.Return } jimpleToASTMap[statementTag.step.stmt]?.let { + val description = if (it is IfStmt) it.thenStmt else it sentenceBlock.stmtTexts.add( StmtDescription( returnType, - it.toString(), + description.toString(), prefix = prefixReturnText ) ) } } - - protected fun textSwitchCase(step: Step, jimpleToASTMap: JimpleToASTMap): String? { - val stmt = step.stmt - val astNode = jimpleToASTMap[stmt] - if (stmt is JLookupSwitchStmt) { - val lookup = stmt.lookupValues - val decision = step.decision - val case = ( - if (decision >= lookup.size) { - null - } else { - lookup[step.decision] - } - )?.value - - if (astNode is SwitchStmt) { - return JimpleToASTMap.getSwitchCaseLabel(astNode, case) - } - } - //needed more tests to cover these cases - if (stmt is JTableSwitchStmt && astNode is SwitchStmt) { - val switchCase = JimpleToASTMap.mapSwitchCase(astNode, step.decision) - if (switchCase is SwitchEntry) { - val case = switchCase.labels.first - if (case.isPresent) { - return "${case.get()}" - } else { - return "default" - } + protected fun textSwitchCase(step: Step, jimpleToASTMap: JimpleToASTMap): String? = + (jimpleToASTMap[step.stmt] as? SwitchStmt) + ?.let { switchStmt -> + NodeConverter.convertSwitchStmt(switchStmt, step, removeSpaces = false) } - } - return null - } protected fun textCondition(statementTag: StatementTag, jimpleToASTMap: JimpleToASTMap): String? { var reversed = true diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/DBSCANClusteringConstants.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/DBSCANClusteringConstants.kt new file mode 100644 index 0000000000..29b4a52d70 --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/DBSCANClusteringConstants.kt @@ -0,0 +1,44 @@ +package org.utbot.summary + +import org.utbot.framework.plugin.api.util.IndentUtil + +object DBSCANClusteringConstants { + /** + * Sets minimum number of successful execution + * for applying the clustering algorithm. + */ + internal const val MIN_NUMBER_OF_EXECUTIONS_FOR_CLUSTERING: Int = 4 + + /** + * DBSCAN hyperparameter. + * + * Sets minimum number of executions to form a cluster. + */ + internal const val MIN_EXEC_DBSCAN: Int = 2 + + /** + * DBSCAN hyperparameter. + * + * Sets radius of search for algorithm. + */ + internal const val RADIUS_DBSCAN: Float = 5.0f +} + +object SummarySentenceConstants { + const val SENTENCE_SEPARATION = ",\n" + const val TAB = IndentUtil.TAB + const val NEW_LINE = "\n" + const val DOT_SYMBOL = '.' + const val COMMA_SYMBOL = ',' + const val SEMI_COLON_SYMBOL = ';' + const val CARRIAGE_RETURN = "\r" + + const val FROM_TO_NAMES_TRANSITION = "->" + const val FROM_TO_NAMES_COLON = ":" + const val AT_CODE = "@code" + const val OPEN_BRACKET = "{" + const val CLOSE_BRACKET = "}" + + const val JAVA_CLASS_DELIMITER = "$" + const val JAVA_DOC_CLASS_DELIMITER = "." +} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/NodeConverter.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/NodeConverter.kt new file mode 100644 index 0000000000..a1e02cfaa6 --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/NodeConverter.kt @@ -0,0 +1,438 @@ +package org.utbot.summary + +import com.github.javaparser.ast.Node +import com.github.javaparser.ast.body.VariableDeclarator +import com.github.javaparser.ast.expr.ArrayAccessExpr +import com.github.javaparser.ast.expr.ArrayCreationExpr +import com.github.javaparser.ast.expr.BinaryExpr +import com.github.javaparser.ast.expr.BooleanLiteralExpr +import com.github.javaparser.ast.expr.CastExpr +import com.github.javaparser.ast.expr.CharLiteralExpr +import com.github.javaparser.ast.expr.ClassExpr +import com.github.javaparser.ast.expr.ConditionalExpr +import com.github.javaparser.ast.expr.DoubleLiteralExpr +import com.github.javaparser.ast.expr.EnclosedExpr +import com.github.javaparser.ast.expr.FieldAccessExpr +import com.github.javaparser.ast.expr.InstanceOfExpr +import com.github.javaparser.ast.expr.IntegerLiteralExpr +import com.github.javaparser.ast.expr.LiteralExpr +import com.github.javaparser.ast.expr.LongLiteralExpr +import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.ast.expr.NameExpr +import com.github.javaparser.ast.expr.NullLiteralExpr +import com.github.javaparser.ast.expr.StringLiteralExpr +import com.github.javaparser.ast.expr.UnaryExpr +import com.github.javaparser.ast.expr.VariableDeclarationExpr +import com.github.javaparser.ast.stmt.CatchClause +import com.github.javaparser.ast.stmt.ExpressionStmt +import com.github.javaparser.ast.stmt.ForEachStmt +import com.github.javaparser.ast.stmt.ForStmt +import com.github.javaparser.ast.stmt.IfStmt +import com.github.javaparser.ast.stmt.ReturnStmt +import com.github.javaparser.ast.stmt.SwitchEntry +import com.github.javaparser.ast.stmt.SwitchStmt +import com.github.javaparser.ast.stmt.ThrowStmt +import com.github.javaparser.ast.stmt.WhileStmt +import org.utbot.framework.plugin.api.Step +import org.utbot.summary.SummarySentenceConstants.SEMI_COLON_SYMBOL +import org.utbot.summary.ast.JimpleToASTMap +import org.utbot.summary.comment.getTextIterationDescription +import soot.jimple.internal.JIfStmt +import soot.jimple.internal.JLookupSwitchStmt +import soot.jimple.internal.JReturnStmt +import soot.jimple.internal.JTableSwitchStmt +import kotlin.jvm.optionals.getOrNull + + +private const val STATEMENT_DECISION_TRUE = 1 +private const val STATEMENT_DECISION_FALSE = 0 + +class NodeConverter { + + companion object { + /** + * Converts ASTNode into String + * @return String that can be a javadoc + */ + fun convertNodeToString(ASTNode: Node, step: Step): String? { + var res = "" + var node = ASTNode + if (node is EnclosedExpr) node = JimpleToASTMap.unEncloseExpr(node) + convertNodeToStringRecursively(node, step)?.let { + res += it + } + if (step.decision == STATEMENT_DECISION_TRUE) { + if (node is BooleanLiteralExpr + || node is NameExpr + || node is MethodCallExpr + || node is CastExpr + || node is FieldAccessExpr + || node is InstanceOfExpr + || node is ArrayAccessExpr + ) res = "Not$res" + else if (node is UnaryExpr && node.operator == UnaryExpr.Operator.LOGICAL_COMPLEMENT) { + res = + res.removePrefix(convertUnaryOperator(UnaryExpr.Operator.LOGICAL_COMPLEMENT)) // double negative expression is positive expression + } + } + return res.ifEmpty { null } + } + + /** + * Used in a conversion of Node into javadoc String + */ + private fun convertNodeToStringRecursively(ASTNode: Node, step: Step): String? { + val res = when (ASTNode) { + is EnclosedExpr -> convertNodeToStringRecursively(JimpleToASTMap.unEncloseExpr(ASTNode), step) + is BinaryExpr -> convertBinaryExpr(ASTNode, step) + is UnaryExpr -> + convertUnaryOperator(ASTNode.operator) + + convertNodeToStringRecursively(ASTNode.expression, step) + is NameExpr -> "${ASTNode.name}" + is LiteralExpr -> convertLiteralExpression(ASTNode) + is ArrayAccessExpr -> "${ASTNode.index.toString().capitalize()}Of${ + ASTNode.name.toString().capitalize() + }" + is FieldAccessExpr -> { + if (ASTNode.scope is FieldAccessExpr) "${ + convertNodeToStringRecursively( + ASTNode.scope, + step + ) + }${ASTNode.name.toString().capitalize()}" + else "${ASTNode.scope.toString().capitalize()}${ASTNode.name.toString().capitalize()}" + } + is CastExpr -> ASTNode.expression.toString().capitalize() + is MethodCallExpr -> { + if (ASTNode.scope.isPresent) "${ + ASTNode.scope.get().toString().capitalize() + }${ASTNode.name.toString().capitalize()}" + else ASTNode.name.toString().capitalize() + } + is InstanceOfExpr -> { + if (step.decision == STATEMENT_DECISION_TRUE) "${ + ASTNode.expression.toString().capitalize() + }NotInstanceOf${ASTNode.type.toString().capitalize()}" + else "${ASTNode.expression.toString().capitalize()}InstanceOf${ + ASTNode.type.toString().capitalize() + }" + } + is ClassExpr -> "${ASTNode.type}Class" + is ArrayCreationExpr -> "NewArrayOf${ASTNode.createdType().toString().capitalize()}" + is ReturnStmt -> { + if (ASTNode.expression.isPresent) convertNodeToStringRecursively( + ASTNode.expression.get(), + step + ) + else "" + } + is ConditionalExpr -> "${convertNodeToStringRecursively(ASTNode.condition, step)}" + is VariableDeclarationExpr -> ASTNode.variables.joinToString(separator = "") { variable -> + convertNodeToStringRecursively( + variable, + step + ) ?: "" + } + + is VariableDeclarator -> { + val initializer = ASTNode.initializer + if (initializer.isPresent) "${ASTNode.type.toString().capitalize()}${ + ASTNode.name.toString().capitalize() + }InitializedBy${convertNodeToStringRecursively(ASTNode.initializer.get(), step)}" + else "${ASTNode.type.toString().capitalize()}${ASTNode.name.toString().capitalize()}IsInitialized" + } + is CatchClause -> ASTNode.parameter.type.toString() + .capitalize() //add ${ASTNode.parameter.name.toString().capitalize() to print variable name + + is WhileStmt -> convertNodeToStringRecursively(ASTNode.condition, step) + is IfStmt -> convertNodeToStringRecursively(ASTNode.condition, step) + is SwitchEntry -> convertSwitchEntry(ASTNode, step, removeSpaces = true) + is ThrowStmt -> "Throws${ASTNode.expression.toString().removePrefix("new").capitalize()}" + is SwitchStmt -> convertSwitchStmt(ASTNode, step, removeSpaces = true) + is ExpressionStmt -> convertNodeToStringRecursively(ASTNode.expression, step) + + else -> { + null + } + } ?: return null + return postProcessName(res) + } + + /** + * Converts ASTNode into String + * @return String that can be a DisplayName + */ + fun convertNodeToDisplayNameString(ASTNode: Node, step: Step): String { + var node = ASTNode + if (node is ExpressionStmt) node = node.expression + if (node is EnclosedExpr) node = JimpleToASTMap.unEncloseExpr(node) + var res = convertNodeToDisplayNameStringRecursively(node, step) + if (node is ReturnStmt) node = node.expression.getOrNull() ?: node + if (step.stmt is JReturnStmt) return res + if (nodeContainsBooleanCondition(node)) { + res += if (step.decision == STATEMENT_DECISION_TRUE) { + " : False" + } else { + " : True" + } + } + return res + } + + /** + * Checks if node contain any boolean condition + */ + private fun nodeContainsBooleanCondition(node: Node): Boolean { + if (node is ArrayAccessExpr + || node is FieldAccessExpr + || node is CastExpr + || node is NameExpr + || node is BinaryExpr + || node is MethodCallExpr + || node is InstanceOfExpr + || node is UnaryExpr + || node is BooleanLiteralExpr + || node is VariableDeclarationExpr && node.variables.any { nodeContainsBooleanCondition(it) } + ) return true + if (node is VariableDeclarator) { + val initializer = node.initializer.getOrNull() + if (initializer != null) { + return nodeContainsBooleanCondition(initializer) + } + } + return false + } + + /** + * Used in a conversion of Node into DisplayName String + */ + private fun convertNodeToDisplayNameStringRecursively(ASTNode: Node, step: Step): String { + val res = when (ASTNode) { + is EnclosedExpr -> convertNodeToDisplayNameStringRecursively( + JimpleToASTMap.unEncloseExpr(ASTNode), + step + ) + is WhileStmt -> getTextIterationDescription(ASTNode) + is ForStmt -> getTextIterationDescription(ASTNode) + is ForEachStmt -> getTextIterationDescription(ASTNode) + is IfStmt -> convertNodeToDisplayNameStringRecursively(ASTNode.condition, step) + is SwitchEntry -> convertSwitchEntry(ASTNode, step, removeSpaces = false) + is ThrowStmt -> "Throws ${ASTNode.expression.toString().removePrefix("new").capitalize()}" + is SwitchStmt -> convertSwitchStmt(ASTNode, step, removeSpaces = false) + is ExpressionStmt -> convertNodeToDisplayNameStringRecursively(ASTNode.expression, step) + is VariableDeclarationExpr -> ASTNode.variables.joinToString(separator = " ") { variable -> + convertNodeToDisplayNameStringRecursively( + variable, + step + ) + } + else -> { + ASTNode.toString() + } + } + return displayNamePostprocessor(res) + } + + /** + * Replaces one+ whitespaces with one whitespace + */ + private fun displayNamePostprocessor(displayName: String) = + displayName.replace("\\s+".toRegex(), " ").replace("$SEMI_COLON_SYMBOL", "") + + private fun convertBinaryExpr(binaryExpr: BinaryExpr, step: Step): String { + var res = "" + val left = convertNodeToStringRecursively(binaryExpr.left, step) + if (left != null) res += left.capitalize() + val stmt = step.stmt + res += if (stmt is JIfStmt) { + convertBinaryOperator( + binaryExpr.operator, + JimpleToASTMap.isOperatorReversed(stmt, binaryExpr), + step.decision + ).capitalize() + } else { + convertBinaryOperator(binaryExpr.operator, false, step.decision).capitalize() + } + val right = convertNodeToStringRecursively(binaryExpr.right, step) + if (right != null) res += right.capitalize() + return res + } + + fun convertSwitchStmt(switchStmt: SwitchStmt, step: Step, removeSpaces: Boolean = true): String = + convertSwitchLabel(switchStmt, step) + ?.let { label -> + val selector = switchStmt.selector.toString() + formatSwitchLabel(label, selector, removeSpaces) + } + ?: "switch(${switchStmt.selector})" + + fun convertSwitchEntry(switchEntry: SwitchEntry, step: Step, removeSpaces: Boolean = true): String = + (switchEntry.parentNode.getOrNull() as? SwitchStmt) + ?.let { switchStmt -> + val label = convertSwitchLabel(switchStmt, step) ?: getSwitchLabel(switchEntry) + val selector = switchStmt.selector.toString() + formatSwitchLabel(label, selector, removeSpaces) + } + ?: switchEntry.toString() + + private fun getSwitchLabel(node: SwitchEntry): String { + val case = node.labels.first + return if (case.isPresent) "${case.get()}" else "default" + } + + private fun formatSwitchLabel(label: String, selector: String, removeSpaces: Boolean = true): String { + return if (removeSpaces) "Switch${selector.capitalize()}Case" + label.replace(" ", "") + else "switch($selector) case: $label" + } + + private fun convertSwitchLabel(switchStmt: SwitchStmt, step: Step): String? = + when (val stmt = step.stmt) { + is JLookupSwitchStmt -> { + val lookup = stmt.lookupValues + val case = + if (step.decision >= lookup.size) null + else lookup[step.decision].value + + JimpleToASTMap.getSwitchCaseLabel(switchStmt, case) + } + + is JTableSwitchStmt -> { + JimpleToASTMap + .mapSwitchCase(switchStmt, step) + ?.let { getSwitchLabel(it) } + } + + else -> null + } + + /** + * Converts literal into String + */ + private fun convertLiteralExpression(literal: LiteralExpr): String { + val res = when (literal) { + is StringLiteralExpr -> literal.asString() + is CharLiteralExpr -> if (isLegitSymbolForFunctionName(literal.asChar())) "${literal.asChar()}" else "Char" + is DoubleLiteralExpr -> { + when (val literalAsDouble = literal.asDouble()) { + Double.NaN -> "NaN" + Double.MIN_VALUE -> "MinValue" + Double.MAX_VALUE -> "MaxValue" + Double.NEGATIVE_INFINITY -> "NegativeInfinity" + Double.POSITIVE_INFINITY -> "Infinity" + else -> { + when { + literalAsDouble != literalAsDouble -> "NaN" + literalAsDouble < 0 -> "Negative${literal.asDouble().toInt()}d" + literalAsDouble == 0.0 -> "Zero" + literalAsDouble == -0.0 -> "Zero" + else -> "${ + literal.asDouble().toInt() + }d" //seems kinda wrong, . can be replaced with "dot" or something else + } + } + } + } + is IntegerLiteralExpr -> { + var str = "" + if (literal.asNumber().toInt() < 0) str += "Negative" + str += if (literal.asNumber().toInt() == 0) "Zero" else "$literal" + str + } + is LongLiteralExpr -> { + var str = "" + if (literal.asNumber().toLong() < 0) str += "Negative" + str += if (literal.asNumber().toInt() == 0) "Zero" else "$literal" + str + } + is BooleanLiteralExpr -> literal.toString() + is NullLiteralExpr -> "Null" + else -> "" + } + return res.capitalize() + } + + /** + * Checks if symbol can be used in function name + * @see Java documentation: Identifiers + */ + private fun isLegitSymbolForFunctionName(ch: Char): Boolean { + return (ch in '0'..'9' + || ch in 'a'..'z' + || ch in 'A'..'Z' + || ch == '_' + || ch == '$' + ) + } + + /** + * Capitalizes method name. + * + * It splits the text by delimiters, capitalizes each part, removes special characters and concatenates result. + */ + private fun postProcessName(name: String) = + name.split(".", "(", ")", ",") + .joinToString("") { it -> it.capitalize().filter { isLegitSymbolForFunctionName(it) } } + + /** + * Converts Javaparser BinaryOperator and all of its children into a String + */ + private fun convertBinaryOperator( + binaryOperator: BinaryExpr.Operator, + isOperatorReversed: Boolean, + decision: Int + ): String { + var operator = binaryOperator + if ((isOperatorReversed && decision == STATEMENT_DECISION_TRUE) || (!isOperatorReversed && decision == STATEMENT_DECISION_FALSE)) { + operator = reverseBinaryOperator(operator) ?: binaryOperator + } + return when (operator) { + BinaryExpr.Operator.OR -> "Or" + BinaryExpr.Operator.AND -> "And" + BinaryExpr.Operator.BINARY_OR -> "BitwiseOr" + BinaryExpr.Operator.BINARY_AND -> "BitwiseAnd" + BinaryExpr.Operator.XOR -> "Xor" + BinaryExpr.Operator.EQUALS -> "Equals" + BinaryExpr.Operator.NOT_EQUALS -> "NotEquals" + BinaryExpr.Operator.LESS -> "LessThan" + BinaryExpr.Operator.GREATER -> "GreaterThan" + BinaryExpr.Operator.LESS_EQUALS -> "LessOrEqual" + BinaryExpr.Operator.GREATER_EQUALS -> "GreaterOrEqual" + BinaryExpr.Operator.LEFT_SHIFT -> "LeftShift" + BinaryExpr.Operator.SIGNED_RIGHT_SHIFT -> "RightShift" + BinaryExpr.Operator.UNSIGNED_RIGHT_SHIFT -> "UnsignedRightShift" + BinaryExpr.Operator.PLUS -> "Plus" + BinaryExpr.Operator.MINUS -> "Minus" + BinaryExpr.Operator.MULTIPLY -> "Multiply" + BinaryExpr.Operator.DIVIDE -> "Divide" + BinaryExpr.Operator.REMAINDER -> "RemainderOf" //does it sounds strange? or is it ok + } + } + + /** + * Reverts Javaparser binary operator if possible + */ + private fun reverseBinaryOperator(operator: BinaryExpr.Operator) = when (operator) { + BinaryExpr.Operator.EQUALS -> BinaryExpr.Operator.NOT_EQUALS + BinaryExpr.Operator.NOT_EQUALS -> BinaryExpr.Operator.EQUALS + BinaryExpr.Operator.LESS -> BinaryExpr.Operator.GREATER_EQUALS + BinaryExpr.Operator.GREATER -> BinaryExpr.Operator.LESS_EQUALS + BinaryExpr.Operator.LESS_EQUALS -> BinaryExpr.Operator.GREATER + BinaryExpr.Operator.GREATER_EQUALS -> BinaryExpr.Operator.LESS + else -> null + } + + /** + * Converts Javaparser unary operator to String + */ + private fun convertUnaryOperator(unaryOperator: UnaryExpr.Operator) = when (unaryOperator) { + UnaryExpr.Operator.PLUS -> "Plus" + UnaryExpr.Operator.MINUS -> "Negative" + UnaryExpr.Operator.PREFIX_INCREMENT -> "PrefixIncrement" + UnaryExpr.Operator.PREFIX_DECREMENT -> "PrefixDecrement" + UnaryExpr.Operator.LOGICAL_COMPLEMENT -> "Not" //! or LogicalComplement + UnaryExpr.Operator.BITWISE_COMPLEMENT -> "BitwiseComplement" + UnaryExpr.Operator.POSTFIX_INCREMENT -> "PostfixIncrement" + UnaryExpr.Operator.POSTFIX_DECREMENT -> "PostfixDecrement" + } + } +} \ 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 40922e2845..1583cf2664 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt @@ -1,78 +1,89 @@ package org.utbot.summary import com.github.javaparser.ast.body.MethodDeclaration -import org.utbot.framework.UtSettings import org.utbot.framework.plugin.api.UtClusterInfo import org.utbot.framework.plugin.api.UtSymbolicExecution import org.utbot.framework.plugin.api.UtExecutionCluster import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter import org.utbot.summary.SummarySentenceConstants.NEW_LINE -import org.utbot.summary.UtSummarySettings.GENERATE_CLUSTER_COMMENTS -import org.utbot.summary.UtSummarySettings.GENERATE_COMMENTS -import org.utbot.summary.UtSummarySettings.GENERATE_DISPLAYNAME_FROM_TO_STYLE -import org.utbot.summary.UtSummarySettings.GENERATE_DISPLAY_NAMES -import org.utbot.summary.UtSummarySettings.GENERATE_NAMES import org.utbot.summary.analysis.ExecutionStructureAnalysis import org.utbot.summary.ast.JimpleToASTMap import org.utbot.summary.ast.SourceCodeParser -import org.utbot.summary.comment.SymbolicExecutionClusterCommentBuilder -import org.utbot.summary.comment.SimpleCommentBuilder +import org.utbot.summary.comment.cluster.SymbolicExecutionClusterCommentBuilder +import org.utbot.summary.comment.classic.symbolic.SimpleCommentBuilder import org.utbot.summary.name.SimpleNameBuilder import java.io.File import java.nio.file.Path -import java.nio.file.Paths import mu.KotlinLogging -import org.utbot.framework.plugin.api.UtConcreteExecutionFailure -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.UtOverflowFailure -import org.utbot.framework.plugin.api.UtSandboxFailure -import org.utbot.framework.plugin.api.UtTimeoutException -import org.utbot.framework.plugin.api.util.humanReadableName +import org.utbot.common.measureTime +import org.utbot.common.info +import org.utbot.framework.SummariesGenerationType.* +import org.utbot.framework.UtSettings.enableClusterCommentsGeneration +import org.utbot.framework.UtSettings.enableJavaDocGeneration +import org.utbot.framework.UtSettings.useDisplayNameArrowStyle +import org.utbot.framework.UtSettings.enableDisplayNameGeneration +import org.utbot.framework.UtSettings.enableTestNamesGeneration +import org.utbot.framework.UtSettings.summaryGenerationType +import org.utbot.framework.UtSettings.useCustomJavaDocTags +import org.utbot.framework.plugin.api.util.isConstructor import org.utbot.framework.plugin.api.util.jClass import org.utbot.fuzzer.FuzzedMethodDescription import org.utbot.fuzzer.FuzzedValue import org.utbot.fuzzer.UtFuzzedExecution import org.utbot.summary.fuzzer.names.MethodBasedNameSuggester import org.utbot.summary.fuzzer.names.ModelBasedNameSuggester -import org.utbot.summary.comment.CustomJavaDocCommentBuilder +import org.utbot.summary.comment.customtags.symbolic.CustomJavaDocCommentBuilder import soot.SootMethod private val logger = KotlinLogging.logger {} -fun UtMethodTestSet.summarize(sourceFile: File?, searchDirectory: Path = Paths.get("")): UtMethodTestSet { - if (!UtSettings.enableMachineLearningModule) return this - - return try { - makeDiverseExecutions(this) - val invokeDescriptions = invokeDescriptions(this, searchDirectory) - // every cluster has summary and list of executions - val executionClusters = Summarization(sourceFile, invokeDescriptions).fillSummaries(this) - val updatedExecutions = executionClusters.flatMap { it.executions } - var pos = 0 - val clustersInfo = executionClusters.map { - val clusterSize = it.executions.size - val indices = pos until (pos + clusterSize) - pos += clusterSize - it.clusterInfo to indices +fun Collection.summarizeAll(searchDirectory: Path, sourceFile: File?): List = logger.info().measureTime({ + "----------------------------------------------------------------------------------------\n" + + "-------------------Summarization started for ${this.size} test cases--------------------\n" + + "----------------------------------------------------------------------------------------" + }) { + this.map { + it.summarizeOne(searchDirectory, sourceFile) + } +} + +private fun UtMethodTestSet.summarizeOne(searchDirectory: Path, sourceFile: File?): UtMethodTestSet = logger.info().measureTime({ "Summarization for ${this.method}"} ){ + if (summaryGenerationType == NONE) return this + + val sourceFileToAnalyze = sourceFile + ?: when (summaryGenerationType) { + FULL -> Instrumenter.adapter.computeSourceFileByClass(this.method.classId.jClass, searchDirectory) + LIGHT, + NONE -> null } - this.copy( + + makeDiverseExecutions(this) + + // HACK: we avoid calling [invokeDescriptions] method to save time, it is useless in Contest + val invokeDescriptions = when (summaryGenerationType) { + FULL -> invokeDescriptions(this, searchDirectory) + LIGHT, + NONE -> emptyList() + } + + // every cluster has summary and list of executions + val executionClusters = Summarization(sourceFileToAnalyze, invokeDescriptions).fillSummaries(this) + val updatedExecutions = executionClusters.flatMap { it.executions } + var pos = 0 + val clustersInfo = executionClusters.map { + val clusterSize = it.executions.size + val indices = pos until (pos + clusterSize) + pos += clusterSize + it.clusterInfo to indices + } + return this.copy( executions = updatedExecutions, clustersInfo = clustersInfo ) // TODO: looks weird and don't create the real copy - } catch (e: Throwable) { - logger.info(e) { "Summary generation error: ${e.message}" } - this - } } -fun UtMethodTestSet.summarize(searchDirectory: Path): UtMethodTestSet = - this.summarize(Instrumenter.computeSourceFileByClass(this.method.classId.jClass, searchDirectory), searchDirectory) - - -class Summarization(val sourceFile: File?, val invokeDescriptions: List) { +open class Summarization(val sourceFile: File?, val invokeDescriptions: List) { private val tagGenerator = TagGenerator() private val jimpleBodyAnalysis = ExecutionStructureAnalysis() @@ -85,14 +96,23 @@ class Summarization(val sourceFile: File?, val invokeDescriptions: List() + val executionClusters = mutableListOf() - clustersToReturn += generateSummariesForTestsWithNonEmptyPathsProducedBySymbolicExecutor(testSet) - clustersToReturn += generateSummariesForTestsProducedByFuzzer(testSet) - clustersToReturn += generateSummariesForTestsWithEmptyPathsProducedBySymbolicExecutor(testSet) + when (summaryGenerationType) { + FULL -> { + executionClusters += generateSummariesForTestsWithNonEmptyPathsProducedBySymbolicExecutor(testSet) + executionClusters += generateFuzzerBasedSummariesForTests(testSet) + executionClusters += generateSummariesForTestsWithEmptyPathsProducedBySymbolicExecutor(testSet) + } + LIGHT -> { + executionClusters += generateFuzzerBasedSummariesForTests(testSet, MethodDescriptionSource.SYMBOLIC) + executionClusters += generateFuzzerBasedSummariesForTests(testSet) + } + NONE -> error("We must not fill summaries if SummariesGenerationType is NONE") + } - return if (clustersToReturn.size > 0) - clustersToReturn + return if (enableClusterCommentsGeneration && executionClusters.size > 0) + executionClusters else listOf(UtExecutionCluster(UtClusterInfo(), testSet.executions)) } @@ -115,9 +135,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 ) { @@ -135,8 +155,8 @@ class Summarization(val sourceFile: File?, val invokeDescriptions: List { val clustersToReturn: MutableList = mutableListOf() - val executionsProducedByFuzzer = testSet.executions.filterIsInstance() - val successfulFuzzerExecutions = mutableListOf() - val unsuccessfulFuzzerExecutions = mutableListOf() + val methodTestSet = prepareMethodTestSet(testSet, descriptionSource) - if (executionsProducedByFuzzer.isNotEmpty()) { - executionsProducedByFuzzer.forEach { utExecution -> - - val nameSuggester = sequenceOf(ModelBasedNameSuggester(), MethodBasedNameSuggester()) + if (methodTestSet.executions.isNotEmpty()) { + methodTestSet.executions.forEach { utExecution -> + val nameSuggester = sequenceOf(ModelBasedNameSuggester(), MethodBasedNameSuggester(descriptionSource)) val testMethodName = try { nameSuggester.flatMap { - it.suggest( - utExecution.fuzzedMethodDescription as FuzzedMethodDescription, - utExecution.fuzzingValues as List, - utExecution.result - ) + when (descriptionSource) { + MethodDescriptionSource.FUZZER -> { + with(utExecution as UtFuzzedExecution) { + it.suggest( + utExecution.fuzzedMethodDescription as FuzzedMethodDescription, + utExecution.fuzzingValues as List, + utExecution.result + ) + } + } + + MethodDescriptionSource.SYMBOLIC -> { + val executableId = testSet.method + val description = FuzzedMethodDescription(executableId).apply { + packageName = executableId.classId.packageName + className = executableId.classId.simpleName + compilableName = if (!executableId.isConstructor) executableId.name else null + canonicalName = executableId.classId.canonicalName + isNested = executableId.classId.isNested + } + it.suggest( + description, + utExecution.stateBefore.parameters.map { value -> FuzzedValue(value) }, + utExecution.result + ) + } + } }.firstOrNull() } catch (t: Throwable) { - logger.error(t) { "Cannot create suggested test name for $utExecution" } // TODO: add better explanation or default behavoiur + logger.error(t) { "Cannot create suggested test name for $utExecution" } // TODO: add better explanation or default behaviour null } utExecution.testMethodName = testMethodName?.testName utExecution.displayName = testMethodName?.displayName - - when (utExecution.result) { - is UtConcreteExecutionFailure -> unsuccessfulFuzzerExecutions.add(utExecution) - is UtExplicitlyThrownException -> unsuccessfulFuzzerExecutions.add(utExecution) - is UtImplicitlyThrownException -> unsuccessfulFuzzerExecutions.add(utExecution) - is UtOverflowFailure -> unsuccessfulFuzzerExecutions.add(utExecution) - is UtSandboxFailure -> unsuccessfulFuzzerExecutions.add(utExecution) - is UtTimeoutException -> unsuccessfulFuzzerExecutions.add(utExecution) - is UtExecutionSuccess -> successfulFuzzerExecutions.add(utExecution) - } + utExecution.summary = testMethodName?.javaDoc } - if (successfulFuzzerExecutions.isNotEmpty()) { - val clusterHeader = buildFuzzerClusterHeaderForSuccessfulExecutions(testSet) - - clustersToReturn.add( - UtExecutionCluster( - UtClusterInfo(clusterHeader, null), - successfulFuzzerExecutions - ) - ) - } - - if (unsuccessfulFuzzerExecutions.isNotEmpty()) { - val clusterHeader = buildFuzzerClusterHeaderForUnsuccessfulExecutions(testSet) - - clustersToReturn.add( - UtExecutionCluster( - UtClusterInfo(clusterHeader, null), - unsuccessfulFuzzerExecutions + when(descriptionSource){ + MethodDescriptionSource.FUZZER -> { + val clusteredExecutions = groupFuzzedExecutions(methodTestSet) + clusteredExecutions.forEach { + clustersToReturn.add( + UtExecutionCluster( + UtClusterInfo(it.header), + it.executions + ) + ) + } + } + MethodDescriptionSource.SYMBOLIC -> { + clustersToReturn.add( + UtExecutionCluster( + UtClusterInfo(), + methodTestSet.executions + ) ) - ) + } } } return clustersToReturn.toList() } - private fun buildFuzzerClusterHeaderForSuccessfulExecutions(testSet: UtMethodTestSet): String { - val commentPrefix = "FUZZER:" - val commentPostfix = "for method ${testSet.method.humanReadableName}" - - return "$commentPrefix ${ExecutionGroup.SUCCESSFUL_EXECUTIONS.displayName} $commentPostfix" - } - - private fun buildFuzzerClusterHeaderForUnsuccessfulExecutions(testSet: UtMethodTestSet): String { - val commentPrefix = "FUZZER:" - val commentPostfix = "for method ${testSet.method.humanReadableName}" - - return "$commentPrefix ${ExecutionGroup.EXPLICITLY_THROWN_UNCHECKED_EXCEPTIONS} $commentPostfix" + open fun prepareMethodTestSet(testSet: UtMethodTestSet, descriptionSource: MethodDescriptionSource): UtMethodTestSet{ + return when (descriptionSource) { + MethodDescriptionSource.FUZZER -> prepareTestSetWithFuzzedExecutions(testSet) + MethodDescriptionSource.SYMBOLIC -> prepareTestSetForByteCodeAnalysis(testSet) + } } /** Filter and copies executions with non-empty paths. */ @@ -308,6 +332,19 @@ class Summarization(val sourceFile: File?, val invokeDescriptions: List() + + return UtMethodTestSet( + method = testSet.method, + executions = executions, + jimpleBody = testSet.jimpleBody, + errors = testSet.errors, + clustersInfo = testSet.clustersInfo + ) + } + /** Filter and copies executions with non-empty paths. */ private fun prepareTestSetWithEmptyPaths(testSet: UtMethodTestSet): UtMethodTestSet { val executions = @@ -323,32 +360,34 @@ class Summarization(val sourceFile: File?, val invokeDescriptions: List? { val sootToAST = mutableMapOf() val jimpleBody = testSet.jimpleBody if (jimpleBody == null) { - logger.info { "No jimple body of method under test" } + logger.debug { "No jimple body of method under test ${testSet.method.name}." } return null } - val methodUnderTestAST = sourceFile?.let { - SourceCodeParser(it, testSet).methodAST - } - if (methodUnderTestAST == null) { - logger.info { "Couldn't parse source file of method under test" } - return null - } + if (sourceFile != null && sourceFile.exists()) { + val methodUnderTestAST = SourceCodeParser(sourceFile, testSet).methodAST - sootToAST[jimpleBody.method] = JimpleToASTMap(jimpleBody.units, methodUnderTestAST) - invokeDescriptions.forEach { - sootToAST[it.sootMethod] = JimpleToASTMap(it.sootMethod.jimpleBody().units, it.ast) + if (methodUnderTestAST == null) { + logger.debug { "Couldn't parse source file with path ${sourceFile.absolutePath} of method under test ${testSet.method.name}." } + return null + } + + sootToAST[jimpleBody.method] = JimpleToASTMap(jimpleBody.units, methodUnderTestAST) + invokeDescriptions.forEach { + sootToAST[it.sootMethod] = JimpleToASTMap(it.sootMethod.jimpleBody().units, it.ast) + } + return sootToAST + } else { + logger.debug { "Couldn't find source file of method under test ${testSet.method.name}." } + return null } - return sootToAST } } @@ -383,20 +422,35 @@ private fun makeDiverseExecutions(testSet: UtMethodTestSet) { private fun invokeDescriptions(testSet: UtMethodTestSet, searchDirectory: Path): List { val sootInvokes = testSet.executions.filterIsInstance().flatMap { it.path.invokeJimpleMethods() }.toSet() + return sootInvokes //TODO(SAT-1170) .filterNot { "\$lambda" in it.declaringClass.name } .mapNotNull { sootMethod -> - val methodFile = Instrumenter.computeSourceFileByClass( + val methodFile = Instrumenter.adapter.computeSourceFileByNameAndPackage( sootMethod.declaringClass.name, sootMethod.declaringClass.javaPackageName.replace(".", File.separator), searchDirectory ) - val ast = methodFile?.let { - SourceCodeParser(sootMethod, it).methodAST + + if (methodFile != null && methodFile.exists()) { + val ast = methodFile.let { + SourceCodeParser(sootMethod, it).methodAST + } + if (ast != null) InvokeDescription(sootMethod, ast) else null + } else { + null } - if (ast != null) InvokeDescription(sootMethod, ast) else null } } -data class InvokeDescription(val sootMethod: SootMethod, val ast: MethodDeclaration) \ No newline at end of file +data class InvokeDescription(val sootMethod: SootMethod, val ast: MethodDeclaration) + +/** + * Sometimes, we need to use fuzzer for preparing summaries even for [UtSymbolicExecution]s. + * See [Summarization.generateFuzzerBasedSummariesForTests]. + */ +enum class MethodDescriptionSource { + FUZZER, + SYMBOLIC, +} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/TagGenerator.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/TagGenerator.kt index 486df9a83f..0e5947483c 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/TagGenerator.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/TagGenerator.kt @@ -2,18 +2,23 @@ package org.utbot.summary import org.utbot.framework.plugin.api.Step import org.utbot.framework.plugin.api.UtConcreteExecutionFailure -import org.utbot.framework.plugin.api.UtSymbolicExecution +import org.utbot.framework.plugin.api.UtConcreteExecutionProcessedFailure +import org.utbot.framework.plugin.api.UtExecution import org.utbot.framework.plugin.api.UtExecutionResult import org.utbot.framework.plugin.api.UtExecutionSuccess import org.utbot.framework.plugin.api.UtExplicitlyThrownException import org.utbot.framework.plugin.api.UtImplicitlyThrownException -import org.utbot.framework.plugin.api.UtOverflowFailure import org.utbot.framework.plugin.api.UtMethodTestSet +import org.utbot.framework.plugin.api.UtOverflowFailure import org.utbot.framework.plugin.api.UtSandboxFailure +import org.utbot.framework.plugin.api.UtStreamConsumingFailure +import org.utbot.framework.plugin.api.UtSymbolicExecution import org.utbot.framework.plugin.api.UtTimeoutException +import org.utbot.framework.plugin.api.UtTaintAnalysisFailure import org.utbot.framework.plugin.api.util.humanReadableName import org.utbot.framework.plugin.api.util.isCheckedException -import org.utbot.summary.UtSummarySettings.MIN_NUMBER_OF_EXECUTIONS_FOR_CLUSTERING +import org.utbot.fuzzer.UtFuzzedExecution +import org.utbot.summary.DBSCANClusteringConstants.MIN_NUMBER_OF_EXECUTIONS_FOR_CLUSTERING import org.utbot.summary.clustering.MatrixUniqueness import org.utbot.summary.clustering.SplitSteps import org.utbot.summary.tag.TraceTag @@ -28,7 +33,7 @@ class TagGenerator { if (clusteredExecutions.isNotEmpty()) { val listOfSplitSteps = clusteredExecutions.map { - val mUniqueness = MatrixUniqueness(it.executions) + val mUniqueness = MatrixUniqueness(it.executions as List) mUniqueness.splitSteps() } @@ -63,7 +68,7 @@ class TagGenerator { traceTagClusters.add( TraceTagCluster( cluster.header, - generateExecutionTags(cluster.executions, splitSteps), + generateExecutionTags(cluster.executions as List, splitSteps), TraceTagWithoutExecution( commonStepsInCluster.toList(), cluster.executions.first().result, @@ -87,17 +92,14 @@ private fun generateExecutionTags(executions: List, splitSt /** - * Splits executions into clusters - * By default there is 5 types of clusters: - * Success, UnexpectedFail, ExpectedCheckedThrow, ExpectedUncheckedThrow, UnexpectedUncheckedThrow - * These are split by the type of execution result + * Splits executions with empty paths into clusters. * - * @return clustered executions + * @return clustered executions. */ fun groupExecutionsWithEmptyPaths(testSet: UtMethodTestSet): List { val methodExecutions = testSet.executions.filterIsInstance() val clusters = mutableListOf() - val commentPrefix = "OTHER:" + val commentPrefix = "OTHER:" val commentPostfix = "for method ${testSet.method.humanReadableName}" val grouped = methodExecutions.groupBy { it.result.clusterKind() } @@ -105,33 +107,52 @@ fun groupExecutionsWithEmptyPaths(testSet: UtMethodTestSet): List kind == ExecutionGroup.SUCCESSFUL_EXECUTIONS } - .map { (suffixId, group) -> - FailedExecutionCluster("$commentPrefix ${suffixId.displayName} $commentPostfix", group) - } + clusters += addClustersOfFailedExecutions(grouped, commentPrefix, commentPostfix) + return clusters +} + +/** + * Splits fuzzed executions into clusters. + * + * @return clustered executions. + */ +fun groupFuzzedExecutions(testSet: UtMethodTestSet): List { + val methodExecutions = testSet.executions.filterIsInstance() + val clusters = mutableListOf() + val commentPrefix = "FUZZER:" + val commentPostfix = "for method ${testSet.method.humanReadableName}" + + val grouped = methodExecutions.groupBy { it.result.clusterKind() } + + val successfulExecutions = grouped[ExecutionGroup.SUCCESSFUL_EXECUTIONS] ?: emptyList() + if (successfulExecutions.isNotEmpty()) { + clusters += SuccessfulExecutionCluster( + "$commentPrefix ${ExecutionGroup.SUCCESSFUL_EXECUTIONS.displayName} $commentPostfix", + successfulExecutions.toList() + ) + } + + clusters += addClustersOfFailedExecutions(grouped, commentPrefix, commentPostfix) return clusters } /** - * Splits executions produced by symbolic execution engine into clusters - * By default there is 5 types of clusters: - * Success, UnexpectedFail, ExpectedCheckedThrow, ExpectedUncheckedThrow, UnexpectedUncheckedThrow - * These are split by the type of execution result + * Splits symbolic executions into clusters. * - * If Success cluster has more than MIN_NUMBER_OF_EXECUTIONS_FOR_CLUSTERING execution - * then clustering algorithm splits those into more clusters + * If Success cluster has more than [MIN_NUMBER_OF_EXECUTIONS_FOR_CLUSTERING] execution + * then clustering algorithm splits those into more clusters. * - * @return clustered executions + * @return clustered executions. */ private fun toClusterExecutions(testSet: UtMethodTestSet): List { val methodExecutions = testSet.executions.filterIsInstance() val clusters = mutableListOf() - val commentPrefix = "SYMBOLIC EXECUTION:" + val commentPrefix = "SYMBOLIC EXECUTION:" val commentPostfix = "for method ${testSet.method.humanReadableName}" val grouped = methodExecutions.groupBy { it.result.clusterKind() } @@ -160,11 +181,21 @@ private fun toClusterExecutions(testSet: UtMethodTestSet): List>, + commentPrefix: String, + commentPostfix: String +): List { + val clusters = grouped .filterNot { (kind, _) -> kind == ExecutionGroup.SUCCESSFUL_EXECUTIONS } .map { (suffixId, group) -> - FailedExecutionCluster("$commentPrefix ${suffixId.displayName} $commentPostfix", group) - } + FailedExecutionCluster("$commentPrefix ${suffixId.displayName} $commentPostfix", group) + } + return clusters } @@ -176,7 +207,14 @@ enum class ExecutionGroup { EXPLICITLY_THROWN_UNCHECKED_EXCEPTIONS, OVERFLOWS, TIMEOUTS, + + /** + * Executions that caused by `InstrumentedProcessDeath` exception. + * Generated tests will be disabled du to possible JVM crash. + */ CRASH_SUITE, + + TAINT_ANALYSIS, SECURITY; val displayName: String get() = toString().replace('_', ' ') @@ -186,27 +224,31 @@ private fun UtExecutionResult.clusterKind() = when (this) { is UtExecutionSuccess -> ExecutionGroup.SUCCESSFUL_EXECUTIONS is UtImplicitlyThrownException -> if (this.exception.isCheckedException) ExecutionGroup.CHECKED_EXCEPTIONS else ExecutionGroup.ERROR_SUITE is UtExplicitlyThrownException -> if (this.exception.isCheckedException) ExecutionGroup.CHECKED_EXCEPTIONS else ExecutionGroup.EXPLICITLY_THROWN_UNCHECKED_EXCEPTIONS + is UtStreamConsumingFailure -> ExecutionGroup.ERROR_SUITE is UtOverflowFailure -> ExecutionGroup.OVERFLOWS is UtTimeoutException -> ExecutionGroup.TIMEOUTS is UtConcreteExecutionFailure -> ExecutionGroup.CRASH_SUITE is UtSandboxFailure -> ExecutionGroup.SECURITY + is UtTaintAnalysisFailure -> ExecutionGroup.TAINT_ANALYSIS + is UtConcreteExecutionProcessedFailure -> + error("Processed failure must not be found in generated tests, it can just happen on intermediate phases of tests generation") } /** * Structure used to represent execution cluster with header */ -sealed class ExecutionCluster(var header: String, val executions: List) +sealed class ExecutionCluster(var header: String, val executions: List) /** * Represents successful execution cluster */ -private class SuccessfulExecutionCluster(header: String, executions: List) : +private class SuccessfulExecutionCluster(header: String, executions: List) : ExecutionCluster(header, executions) /** * Represents failed execution cluster */ -private class FailedExecutionCluster(header: String, executions: List) : +private class FailedExecutionCluster(header: String, executions: List) : ExecutionCluster(header, executions) /** diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/UtSummarySettings.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/UtSummarySettings.kt deleted file mode 100644 index 70f318ad39..0000000000 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/UtSummarySettings.kt +++ /dev/null @@ -1,67 +0,0 @@ -package org.utbot.summary - -object UtSummarySettings { - /** - * If True test comments will be generated - */ - var GENERATE_COMMENTS = true - - /** - * If True cluster comments will be generated - */ - var GENERATE_CLUSTER_COMMENTS = true - - /** - * If True names for tests will be generated - */ - var GENERATE_NAMES = true - - /** - * If True display names for tests will be generated - */ - var GENERATE_DISPLAY_NAMES = true - - /** - * generate display name in from -> to style - */ - var GENERATE_DISPLAYNAME_FROM_TO_STYLE = true - - /** - * If True mutation descriptions for tests will be generated - * TODO: implement - */ - var GENERATE_MUTATION_DESCRIPTIONS = true - - /** - * Sets minimum number of successful execution - * for applying the clustering algorithm - */ - const val MIN_NUMBER_OF_EXECUTIONS_FOR_CLUSTERING: Int = 4 - - /** - * DBSCAN hyperparameter - * Sets minimum number of executions to form a cluster - */ - var MIN_EXEC_DBSCAN: Int = 2 - - /** - * DBSCAN hyperparameter - * Sets radius of search for algorithm - */ - var RADIUS_DBSCAN: Float = 5.0f -} - -object SummarySentenceConstants { - const val SENTENCE_SEPARATION = ",\n" - const val TAB = " " - const val NEW_LINE = "\n" - const val DOT_SYMBOL = '.' - const val COMMA_SYMBOL = ',' - const val SEMI_COLON_SYMBOL = ';' - const val CARRIAGE_RETURN = "\r" - - const val FROM_TO_NAMES_TRANSITION = "->" - const val AT_CODE = "@code" - const val OPEN_BRACKET = "{" - const val CLOSE_BRACKET = "}" -} \ No newline at end of file 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 2366df4915..5ccb1440bf 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 @@ -25,12 +25,14 @@ import com.github.javaparser.ast.stmt.Statement import com.github.javaparser.ast.stmt.SwitchEntry import com.github.javaparser.ast.stmt.SwitchStmt import com.github.javaparser.ast.stmt.WhileStmt +import org.utbot.framework.plugin.api.Step import org.utbot.summary.comment.isLoopStatement import java.util.LinkedList import java.util.Queue +import java.util.stream.Collectors +import java.util.stream.Stream import kotlin.Int.Companion.MAX_VALUE import kotlin.math.abs -import kotlin.streams.toList import soot.Unit import soot.Value import soot.jimple.internal.JCaughtExceptionRef @@ -75,11 +77,16 @@ class JimpleToASTMap(stmts: Iterable, methodDeclaration: MethodDeclaration alignNonAlignedReturns() } + /** + * Avoid conflict with java.util.stream.Stream.toList (available since Java 16 only) + */ + private fun Stream.asList(): List = collect(Collectors.toList()) + /** * Function that maps statements inside of ternary conditions to correct AST nodes */ private fun mapTernaryConditional(methodDeclaration: MethodDeclaration, stmts: Iterable) { - for (condExpr in methodDeclaration.stream().toList().filterIsInstance()) { + for (condExpr in methodDeclaration.stream().asList().filterIsInstance()) { val begin = condExpr.begin.orElse(null) val end = condExpr.end.orElse(null) if (begin == null || end == null) continue @@ -122,7 +129,7 @@ class JimpleToASTMap(stmts: Iterable, methodDeclaration: MethodDeclaration * Node is valid if there is only one return statement inside of it */ private fun validateReturnASTNode(returnNode: Node): Node { - val returns = returnNode.stream().filter { it is ReturnStmt }.toList() + val returns = returnNode.stream().filter { it is ReturnStmt }.asList() if (returns.size == 1) return returns[0] return returnNode } @@ -146,17 +153,17 @@ class JimpleToASTMap(stmts: Iterable, methodDeclaration: MethodDeclaration val loopList = mutableListOf() when (loop) { is ForStmt -> { - loopList.addAll(loop.initialization.stream().toList()) - val compare = loop.compare.orElse(null)?.stream()?.toList() + loopList.addAll(loop.initialization.stream().asList()) + val compare = loop.compare.orElse(null)?.stream()?.asList() if (compare != null) loopList.addAll(compare) - loopList.addAll(loop.update.flatMap { it.stream().toList() }) + loopList.addAll(loop.update.flatMap { it.stream().asList() }) } is WhileStmt -> { - loopList.addAll(loop.condition.stream().toList()) + loopList.addAll(loop.condition.stream().asList()) } is ForEachStmt -> { - loopList.addAll(loop.iterable.stream().toList()) - loopList.addAll(loop.variable.stream().toList()) + loopList.addAll(loop.iterable.stream().asList()) + loopList.addAll(loop.variable.stream().asList()) } } for (stmt in stmtToASTNode.filter { it.value in loopList }.map { it.key }) stmtToASTNode[stmt] = loop @@ -188,9 +195,9 @@ class JimpleToASTMap(stmts: Iterable, methodDeclaration: MethodDeclaration val stmts = stmtToASTNode.keys.toTypedArray() for (nonAlignedReturn in nonAlignedReturns) { val index = stmts.indexOf(nonAlignedReturn) - if (index - 1 >= 0 && stmts[index - 1] is JIfStmt) { - stmtToASTNode[nonAlignedReturn] = stmtToASTNode[stmts[index - 1]] - } + val ternaryIfStmtIndex = stmts.indexOfLast { it is JIfStmt && stmts.indexOf(it) <= index } + + stmtToASTNode[nonAlignedReturn] = stmtToASTNode[stmts[ternaryIfStmtIndex]] } } @@ -339,12 +346,14 @@ class JimpleToASTMap(stmts: Iterable, methodDeclaration: MethodDeclaration } /** - * @return SwitchEntry №decision + * @return SwitchEntry */ - fun mapSwitchCase(switchStmt: SwitchStmt, decision: Int): SwitchEntry? { - val switchStmts = switchStmt.childNodes.filterIsInstance() - return if (decision < switchStmts.size) switchStmts[decision] - else null + fun mapSwitchCase(switchStmt: SwitchStmt, step: Step): SwitchEntry? { + val neededLine = step.stmt.unitBoxes[step.decision].unit.javaSourceStartLineNumber + return switchStmt + .childNodes + .filterIsInstance() + .find { neededLine in (it.range.get().begin.line..it.range.get().end.line) } } /** diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/ast/SourceCodeParser.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/ast/SourceCodeParser.kt index d3e59d79a4..8fa3f041e5 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/ast/SourceCodeParser.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/ast/SourceCodeParser.kt @@ -36,7 +36,6 @@ class SourceCodeParser { val methodName = sootMethod.name val className = sootMethod.declaredClassName - val maxLineNumber = if (sootMethod.hasActiveBody()) sootMethod.retrieveActiveBody()?.units?.maxOfOrNull { it.javaSourceStartLineNumber } @@ -56,7 +55,7 @@ class SourceCodeParser { val clazz = it.types.firstOrNull { clazz -> clazz.name.identifier == className } ?: traverseInnerClassDeclarations( - it.types.flatMap { declaration -> declaration.childNodes }, + it.types.flatMap { declaration -> declaration.childNodes }.filterIsInstance(), className ) @@ -85,9 +84,23 @@ class SourceCodeParser { * Identifier is ClassOrInterfaceDeclaration */ private fun traverseInnerClassDeclarations( - nodes: List, className: String - ): TypeDeclaration<*>? = nodes.filterIsInstance() - .firstOrNull { it.name.identifier == className } + nodes: List, className: String + ): TypeDeclaration<*>? { + var result = nodes.firstOrNull { it.name.identifier == className } + + if (result != null) return result + + run childrenSearch@ { + nodes.forEach { + val childNodes = it.childNodes.filterIsInstance() + if (childNodes.isNotEmpty()) { + result = traverseInnerClassDeclarations(childNodes, className) as ClassOrInterfaceDeclaration? + if (result!= null) return@childrenSearch + } + } + } + return result + } } /** diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/clustering/MatrixUniqueness.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/clustering/MatrixUniqueness.kt index e354d920b3..f2977f184d 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/clustering/MatrixUniqueness.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/clustering/MatrixUniqueness.kt @@ -2,7 +2,7 @@ package org.utbot.summary.clustering import org.utbot.framework.plugin.api.Step import org.utbot.framework.plugin.api.UtSymbolicExecution -import org.utbot.summary.UtSummarySettings +import org.utbot.summary.DBSCANClusteringConstants import org.utbot.summary.clustering.dbscan.DBSCANTrainer import org.utbot.summary.clustering.dbscan.neighbor.LinearRangeQuery @@ -81,8 +81,8 @@ class MatrixUniqueness(executions: List) { /** Returns map: cluster identifier, List. */ fun dbscanClusterExecutions( methodExecutions: List, - minPts: Int = UtSummarySettings.MIN_EXEC_DBSCAN, - radius: Float = UtSummarySettings.RADIUS_DBSCAN + minPts: Int = DBSCANClusteringConstants.MIN_EXEC_DBSCAN, + radius: Float = DBSCANClusteringConstants.RADIUS_DBSCAN ): Map> { val executionPaths = methodExecutions.map { it.path.asIterable() }.toTypedArray() diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocComment.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocComment.kt deleted file mode 100644 index fffe9cbf8b..0000000000 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocComment.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.utbot.summary.comment - -/** - * Represents a set of plugin's custom JavaDoc tags. - */ -data class CustomJavaDocComment( - val classUnderTest: String = EMPTY_STRING, - val methodUnderTest: String = EMPTY_STRING, - val expectedResult: String = EMPTY_STRING, - val actualResult: String = EMPTY_STRING, - var executesCondition: List = listOf(), - var invokes: List = listOf(), - var iterates: List = listOf(), - var switchCase: String = EMPTY_STRING, - var recursion: String = EMPTY_STRING, - var returnsFrom: String = EMPTY_STRING, - var countedReturn: String = EMPTY_STRING, - var caughtException: String = EMPTY_STRING, - var throwsException: String = EMPTY_STRING -) \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocTagProvider.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocTagProvider.kt deleted file mode 100644 index 4c98d8a379..0000000000 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocTagProvider.kt +++ /dev/null @@ -1,70 +0,0 @@ -package org.utbot.summary.comment - -import org.utbot.framework.plugin.api.DocRegularStmt - -/** - * Provides a list of supported custom JavaDoc tags. - */ -class CustomJavaDocTagProvider { - // The tags' order is important because plugin builds final JavaDoc comment according to it. - fun getPluginCustomTags(): List = - listOf( - CustomJavaDocTag.ClassUnderTest, - CustomJavaDocTag.MethodUnderTest, - CustomJavaDocTag.ExpectedResult, - CustomJavaDocTag.ActualResult, - CustomJavaDocTag.Executes, - CustomJavaDocTag.Invokes, - CustomJavaDocTag.Iterates, - CustomJavaDocTag.SwitchCase, - CustomJavaDocTag.Recursion, - CustomJavaDocTag.ReturnsFrom, - CustomJavaDocTag.CaughtException, - CustomJavaDocTag.ThrowsException, - ) -} - -sealed class CustomJavaDocTag( - val name: String, - val message: String, - private val valueRetriever: (CustomJavaDocComment) -> Any -) { - object ClassUnderTest : - CustomJavaDocTag("utbot.classUnderTest", "Class under test", CustomJavaDocComment::classUnderTest) - - object MethodUnderTest : - CustomJavaDocTag("utbot.methodUnderTest", "Method under test", CustomJavaDocComment::methodUnderTest) - - object ExpectedResult : - CustomJavaDocTag("utbot.expectedResult", "Expected result", CustomJavaDocComment::expectedResult) - - object ActualResult : CustomJavaDocTag("utbot.actualResult", "Actual result", CustomJavaDocComment::actualResult) - object Executes : - CustomJavaDocTag("utbot.executesCondition", "Executes condition", CustomJavaDocComment::executesCondition) - - object Invokes : CustomJavaDocTag("utbot.invokes", "Invokes", CustomJavaDocComment::invokes) - object Iterates : CustomJavaDocTag("utbot.iterates", "Iterates", CustomJavaDocComment::iterates) - object SwitchCase : CustomJavaDocTag("utbot.activatesSwitch", "Activates switch", CustomJavaDocComment::switchCase) - object Recursion : - CustomJavaDocTag("utbot.triggersRecursion", "Triggers recursion ", CustomJavaDocComment::recursion) - - object ReturnsFrom : CustomJavaDocTag("utbot.returnsFrom", "Returns from", CustomJavaDocComment::returnsFrom) - object CaughtException : - CustomJavaDocTag("utbot.caughtException", "Caught exception", CustomJavaDocComment::caughtException) - - object ThrowsException : - CustomJavaDocTag("utbot.throwsException", "Throws exception", CustomJavaDocComment::throwsException) - - fun generateDocStatement(comment: CustomJavaDocComment): DocRegularStmt? = - when (val value = valueRetriever.invoke(comment)) { - is String -> value.takeIf { it.isNotEmpty() }?.let { - DocRegularStmt("@$name $value\n") - } - is List<*> -> value.takeIf { it.isNotEmpty() }?.let { - val valueToString = value.joinToString(separator = "\n", postfix = "\n") {"@$name $it"} - - DocRegularStmt(valueToString) - } - else -> null - } -} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SentenceUtil.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SentenceUtil.kt index 8057a5e74c..03061d0f80 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SentenceUtil.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SentenceUtil.kt @@ -1,6 +1,7 @@ package org.utbot.summary.comment import com.github.javaparser.ast.Node +import com.github.javaparser.ast.body.MethodDeclaration import com.github.javaparser.ast.stmt.BlockStmt import com.github.javaparser.ast.stmt.ForEachStmt import com.github.javaparser.ast.stmt.ForStmt @@ -14,6 +15,9 @@ import org.utbot.summary.SummarySentenceConstants.COMMA_SYMBOL import org.utbot.summary.SummarySentenceConstants.DOT_SYMBOL import org.utbot.summary.SummarySentenceConstants.NEW_LINE import org.utbot.summary.SummarySentenceConstants.TAB +import org.utbot.summary.comment.classic.symbolic.SquashedStmtTexts +import org.utbot.summary.comment.classic.symbolic.StmtType +import kotlin.jvm.optionals.getOrNull fun numberWithSuffix(number: Int) = when (number % 10) { 1 -> "${number}st" @@ -23,21 +27,41 @@ fun numberWithSuffix(number: Int) = when (number % 10) { } /** - * Returns reason for given ThrowStmt - * It can be parent ast node - * or grandparent node if parent is BlockStmt class, + * Returns a reason for the given ThrowStmt: a condition or ThrowStmt itself. + * + * ThrowStmt is returned when its grandparent is MethodDeclaration (method under test). + * E.g. + * `public void myMethod() throws RuntimeException { throw new RuntimeException("message") }` + * + * Keep in mind, for inner method that throws something the reason will be that inner method. */ -fun getExceptionReason(throwStmt: ThrowStmt): Node? { - val parent = throwStmt.parentNode.orElse(null) - if (parent != null) { - return if (parent is BlockStmt) { - parent.parentNode.orElse(null) - } else { - parent +private fun getExceptionReason(throwStmt: ThrowStmt): Node? = + throwStmt + .parentNode + .getOrNull() + ?.let { parent -> + if (parent is BlockStmt) { + val grandParent = parent.parentNode.getOrNull() + if (grandParent is MethodDeclaration) throwStmt + else grandParent + } else parent + } + +fun getExceptionReasonForComment(throwStmt: ThrowStmt): Node? = + getExceptionReason(throwStmt) + +fun getExceptionReasonForName(throwStmt: ThrowStmt): Node? = + getExceptionReason(throwStmt) + ?.let { + if (it is ThrowStmt) { + /** + * When the node is ThrowStmt it means that we have a deal with a case + * when the method under test just throws an exception with no conditions. + * So instead of rendering the whole method in display name we want to omit it at all. + */ + null + } else it } - } - return null -} fun numberOccurrencesToText(n: Int): String = when (n) { 0 -> "" @@ -89,14 +113,26 @@ val nextSynonyms = arrayOf( "then" ) -val skipInvokes = arrayOf( +val forbiddenMethodInvokes = arrayOf( "", + "", "valueOf", - "getClass" + "getClass", ) -// TODO: SAT-1589 -fun shouldSkipInvoke(invoke: String) = (invoke in skipInvokes) || (invoke.endsWith('$')) +val forbiddenClassInvokes = arrayOf( + "soot.dummy.InvokeDynamic" +) + +/** + * Filters out + * ```soot.dummy.InvokeDynamic#makeConcat```, + * ```soot.dummy.InvokeDynamic#makeConcatWithConstants```, + * constructor calls (``````), ```bootstrap$``` + * and other unwanted things from name and comment text. + */ +fun shouldSkipInvoke(className: String, methodName: String) = + className in forbiddenClassInvokes || methodName in forbiddenMethodInvokes || methodName.endsWith("$") fun squashDocRegularStatements(sentences: List): List { val newStatements = mutableListOf() diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/classic/fuzzer/SimpleCommentForTestProducedByFuzzerBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/classic/fuzzer/SimpleCommentForTestProducedByFuzzerBuilder.kt new file mode 100644 index 0000000000..ee272f2f3a --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/classic/fuzzer/SimpleCommentForTestProducedByFuzzerBuilder.kt @@ -0,0 +1,48 @@ +package org.utbot.summary.comment.classic.fuzzer + +import org.utbot.framework.plugin.api.DocPreTagStatement +import org.utbot.framework.plugin.api.DocRegularStmt +import org.utbot.framework.plugin.api.DocStatement +import org.utbot.framework.plugin.api.UtExecutionResult +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.summary.SummarySentenceConstants.NEW_LINE +import org.utbot.summary.comment.customtags.getClassReference +import org.utbot.summary.comment.customtags.getFullClassName +import org.utbot.summary.comment.customtags.getMethodReferenceForFuzzingTest + +class SimpleCommentForTestProducedByFuzzerBuilder( + val methodDescription: FuzzedMethodDescription, + val values: List, + val result: UtExecutionResult? +) { + fun buildDocStatements(): List { + val packageName = methodDescription.packageName + val className = methodDescription.className + val methodName = methodDescription.compilableName + val canonicalName = methodDescription.canonicalName + val isNested = methodDescription.isNested + + val result = if (packageName != null && className != null && methodName != null) { + val fullClassName = getFullClassName(canonicalName, packageName, className, isNested) + + val methodReference = getMethodReferenceForFuzzingTest( + fullClassName, + methodName, + methodDescription.parameters, + false + ) + + val classReference = getClassReference(fullClassName) + + val docStatements = mutableListOf() + docStatements.add(DocRegularStmt("Class under test: $classReference$NEW_LINE")) + docStatements.add(DocRegularStmt("Method under test: $methodReference$NEW_LINE")) + docStatements + } else { + emptyList() + } + + return listOf(DocPreTagStatement(result)) + } +} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleCommentBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/classic/symbolic/SimpleCommentBuilder.kt similarity index 80% rename from utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleCommentBuilder.kt rename to utbot-summary/src/main/kotlin/org/utbot/summary/comment/classic/symbolic/SimpleCommentBuilder.kt index 8d86ea777e..cab592b7bb 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleCommentBuilder.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/classic/symbolic/SimpleCommentBuilder.kt @@ -1,442 +1,421 @@ -package org.utbot.summary.comment - -import com.github.javaparser.ast.body.MethodDeclaration -import com.github.javaparser.ast.stmt.CatchClause -import com.github.javaparser.ast.stmt.ForStmt -import com.github.javaparser.ast.stmt.IfStmt -import com.github.javaparser.ast.stmt.Statement -import com.github.javaparser.ast.stmt.SwitchStmt -import com.github.javaparser.ast.stmt.ThrowStmt -import org.utbot.framework.plugin.api.ConcreteExecutionFailureException -import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt -import org.utbot.framework.plugin.api.DocStatement -import org.utbot.framework.plugin.api.Step -import org.utbot.framework.plugin.api.exceptionOrNull -import org.utbot.summary.AbstractTextBuilder -import org.utbot.summary.SummarySentenceConstants.CARRIAGE_RETURN -import org.utbot.summary.ast.JimpleToASTMap -import org.utbot.summary.tag.BasicTypeTag -import org.utbot.summary.tag.CallOrderTag -import org.utbot.summary.tag.StatementTag -import org.utbot.summary.tag.TraceTagWithoutExecution -import org.utbot.summary.tag.UniquenessTag -import soot.SootMethod -import soot.Type -import soot.jimple.Stmt -import soot.jimple.internal.JAssignStmt -import soot.jimple.internal.JInvokeStmt -import soot.jimple.internal.JVirtualInvokeExpr - -private const val JVM_CRASH_REASON = "JVM crash" -const val EMPTY_STRING = "" - -open class SimpleCommentBuilder( - traceTag: TraceTagWithoutExecution, - sootToAST: MutableMap, - val stringTemplates: StringsTemplatesInterface = StringsTemplatesSingular() -) : - AbstractTextBuilder(traceTag, sootToAST) { - - /** - * Creates String from SimpleSentenceBlock - */ - open fun buildString(currentMethod: SootMethod): String { - val root = SimpleSentenceBlock(stringTemplates = stringTemplates) - buildThrownExceptionInfo(root, currentMethod) - skippedIterations() - buildSentenceBlock(traceTag.rootStatementTag, root, currentMethod) - var sentence = toSentence(root) - - if (sentence.isEmpty()) { - return EMPTY_STRING - } - - sentence = splitLongSentence(sentence) - sentence = lastCommaToDot(sentence) - - return "

    \n$sentence
    ".replace(CARRIAGE_RETURN, "") - } - - private fun buildThrownExceptionInfo( - root: SimpleSentenceBlock, - currentMethod: SootMethod - ) { - traceTag.result.exceptionOrNull()?.let { - val exceptionName = it.javaClass.simpleName - val reason = findExceptionReason(currentMethod, it) - root.exceptionThrow = "$exceptionName $reason" - } - } - - /** - * Creates List<[DocStatement]> from [SimpleSentenceBlock]. - */ - open fun buildDocStmts(currentMethod: SootMethod): List { - val sentenceBlock = buildSentenceBlock(currentMethod) - val docStmts = toDocStmts(sentenceBlock) - - if (docStmts.isEmpty()) { - return emptyList() - } -// sentence = splitLongSentence(sentence) //TODO SAT-1309 -// sentence = lastCommaToDot(sentence) //TODO SAT-1309 - - return listOf(DocPreTagStatement(docStmts)) - } - - private fun buildSentenceBlock(currentMethod: SootMethod): SimpleSentenceBlock { - val rootSentenceBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) - buildThrownExceptionInfo(rootSentenceBlock, currentMethod) - skippedIterations() - buildSentenceBlock(traceTag.rootStatementTag, rootSentenceBlock, currentMethod) - return rootSentenceBlock - } - - /** - * Transforms rootSentenceBlock into String - */ - protected fun toSentence(rootSentenceBlock: SimpleSentenceBlock): String { - rootSentenceBlock.squashStmtText() - val buildSentence = rootSentenceBlock.toSentence() - if (buildSentence.isEmpty()) return "" - return "${stringTemplates.sentenceBeginning} $buildSentence" - } - - /** - * Transforms rootSentenceBlock into List - */ - protected fun toDocStmts(rootSentenceBlock: SimpleSentenceBlock): List { - val stmts = mutableListOf() - - rootSentenceBlock.squashStmtText() - stmts += rootSentenceBlock.toDocStmt() - if (stmts.isEmpty()) return emptyList() - - stmts.add(0, DocRegularStmt("${stringTemplates.sentenceBeginning} ")) - return stmts - } - - protected fun findExceptionReason(currentMethod: SootMethod, thrownException: Throwable): String { - val path = traceTag.path - if (path.isEmpty()) { - if (thrownException is ConcreteExecutionFailureException) { - return JVM_CRASH_REASON - } - - error("Cannot find last path step for exception $thrownException") - } - - return findExceptionReason(path.last(), currentMethod) - } - - /** - * Tries to find ast node where exception was thrown - * or condition if exception was thrown manually in function body - */ - protected fun findExceptionReason(step: Step, currentMethod: SootMethod): String { - var reason = "" - val exceptionStmt = step.stmt - val jimpleToASTMap = sootToAST[currentMethod] ?: return "" - var exceptionNode = jimpleToASTMap.stmtToASTNode[exceptionStmt] - if (exceptionNode is ThrowStmt) { - exceptionNode = getExceptionReason(exceptionNode) - reason += "after condition: " - } else reason += "in: " - - //special case if reason is MethodDeclaration -> exception was thrown after body execution, not after condition - if (exceptionNode is MethodDeclaration) return "in ${exceptionNode.name} function body" - //node is SwitchStmt only when jimple stmt is inside selector - if (exceptionNode is SwitchStmt) exceptionNode = exceptionNode.selector - - if (exceptionNode == null) return "" - - reason += when { - exceptionNode is IfStmt -> exceptionNode.condition.toString() - isLoopStatement(exceptionNode) -> getTextIterationDescription(exceptionNode) - exceptionNode is SwitchStmt -> textSwitchCase(step, jimpleToASTMap) - else -> exceptionNode.toString() - } - - return reason.replace(CARRIAGE_RETURN, "") - } - - /** - * Sentence blocks are built based on unique and partly unique statement tags. - */ - open fun buildSentenceBlock( - statementTag: StatementTag?, - sentenceBlock: SimpleSentenceBlock, - currentMethod: SootMethod - ) { - val jimpleToASTMap = sootToAST[currentMethod] - if (statementTag == null) return - if (jimpleToASTMap == null) return - val recursion = statementTag.recursion - val stmt = statementTag.step.stmt - val invoke = statementTag.invoke - var createNextBlock = false - - val localNoIterations = statementTagSkippedIteration(statementTag, currentMethod) - if (localNoIterations.isNotEmpty()) { - sentenceBlock.notExecutedIterations = localNoIterations - methodToNoIterationDescription[currentMethod]?.removeAll(localNoIterations) - } - - val invokeSootMethod = statementTag.invokeSootMethod() - var invokeRegistered = false - if (invoke != null && invokeSootMethod != null) { - val className = invokeSootMethod.declaringClass.name - val methodName = invokeSootMethod.name - val methodParameterTypes = invokeSootMethod.parameterTypes - val sentenceInvoke = SimpleSentenceBlock(stringTemplates = sentenceBlock.stringTemplates) - buildSentenceBlock(invoke, sentenceInvoke, invokeSootMethod) - sentenceInvoke.squashStmtText() - if (!sentenceInvoke.isEmpty()) { - sentenceBlock.invokeSentenceBlock = - Pair( - getMethodReference(className, methodName, methodParameterTypes, invokeSootMethod.isPrivate), - sentenceInvoke - ) - createNextBlock = true - invokeRegistered = true - } - } - if (statementTag.basicTypeTag == BasicTypeTag.Invoke && statementTag.uniquenessTag == UniquenessTag.Unique && !invokeRegistered) { - if (statementTag.executionFrequency <= 1) { - addTextInvoke(sentenceBlock, stmt, statementTag.executionFrequency) - } - if (statementTag.executionFrequency > 1 && statementTag.callOrderTag == CallOrderTag.First) { - addTextInvoke(sentenceBlock, stmt, statementTag.executionFrequency) - } - } - - if (statementTag.basicTypeTag == BasicTypeTag.RecursionAssignment && statementTag.uniquenessTag == UniquenessTag.Unique && !invokeRegistered) { - if (statementTag.executionFrequency <= 1) { - addTextRecursion(sentenceBlock, stmt, statementTag.executionFrequency) - } - if (statementTag.executionFrequency > 1 && statementTag.callOrderTag == CallOrderTag.First) { - addTextRecursion(sentenceBlock, stmt, statementTag.executionFrequency) - } - } - - if (jimpleToASTMap[statementTag.step.stmt] !is ForStmt) { - - if (statementTag.basicTypeTag == BasicTypeTag.Condition && statementTag.callOrderTag == CallOrderTag.First) { - if (statementTag.uniquenessTag == UniquenessTag.Unique) { - val conditionText = textCondition(statementTag, jimpleToASTMap) - if (conditionText != null) { - sentenceBlock.stmtTexts.add(StmtDescription(StmtType.Condition, conditionText)) - } - } - if (statementTag.uniquenessTag == UniquenessTag.Partly && statementTag.executionFrequency == 1) { - val conditionText = textCondition(statementTag, jimpleToASTMap) - if (conditionText != null) { - sentenceBlock.stmtTexts.add(StmtDescription(StmtType.Condition, conditionText)) - } - } - } - - if (statementTag.basicTypeTag == BasicTypeTag.SwitchCase && statementTag.uniquenessTag == UniquenessTag.Unique) { - val switchCase = textSwitchCase(statementTag.step, jimpleToASTMap) - if (switchCase != null) { - sentenceBlock.stmtTexts.add(StmtDescription(StmtType.SwitchCase, switchCase)) - } - } - if (statementTag.basicTypeTag == BasicTypeTag.CaughtException) { - jimpleToASTMap[stmt].let { - if (it is CatchClause) { - sentenceBlock.stmtTexts.add(StmtDescription(StmtType.CaughtException, it.parameter.toString())) - } - } - } - if (statementTag.basicTypeTag == BasicTypeTag.Return) { - textReturn(statementTag, sentenceBlock, stmt, jimpleToASTMap) - } - } - - if (statementTag.iterations.isNotEmpty()) { - val iterationSentenceBlock = buildIterationsBlock(statementTag.iterations, statementTag.step, currentMethod) - sentenceBlock.iterationSentenceBlocks.add(iterationSentenceBlock) - createNextBlock = true - } - - if (recursion != null) { - if (stmt is JAssignStmt) { - val name = (stmt.rightOp as JVirtualInvokeExpr).method.name - val sentenceRecursionBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) - buildSentenceBlock(recursion, sentenceRecursionBlock, currentMethod) - sentenceBlock.recursion = Pair(name, sentenceRecursionBlock) - createNextBlock = true - } - if (stmt is JInvokeStmt) { - val name = stmt.invokeExpr.method.name - val sentenceRecursion = SimpleSentenceBlock(stringTemplates = stringTemplates) - buildSentenceBlock(recursion, sentenceRecursion, currentMethod) - sentenceBlock.recursion = Pair(name, sentenceRecursion) - createNextBlock = true - } - } - - if (createNextBlock) { - val nextSentenceBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) - sentenceBlock.nextBlock = nextSentenceBlock - buildSentenceBlock(statementTag.next, nextSentenceBlock, currentMethod) - } else { - buildSentenceBlock(statementTag.next, sentenceBlock, currentMethod) - } - } - - /** - * Adds RecursionAssignment into sentenceBlock.stmtTexts - */ - protected fun addTextRecursion(sentenceBlock: SimpleSentenceBlock, stmt: Stmt, frequency: Int) { - if (stmt is JAssignStmt || stmt is JInvokeStmt) { - val methodName = stmt.invokeExpr.method.name - addTextRecursion(sentenceBlock, methodName, frequency) - } - } - - /** - * Adds Invoke into sentenceBlock.stmtTexts - */ - protected fun addTextInvoke(sentenceBlock: SimpleSentenceBlock, stmt: Stmt, frequency: Int) { - if (stmt is JAssignStmt || stmt is JInvokeStmt) { - val className = stmt.invokeExpr.methodRef.declaringClass.name - val methodName = stmt.invokeExpr.method.name - val methodParameterTypes = stmt.invokeExpr.method.parameterTypes - val isPrivate = stmt.invokeExpr.method.isPrivate - addTextInvoke( - sentenceBlock, - className, - methodName, - methodParameterTypes, - isPrivate, - frequency - ) - } - } - - /** - * Adds Invoke into sentenceBlock.stmtTexts - */ - protected fun addTextInvoke( - sentenceBlock: SimpleSentenceBlock, - className: String, - methodName: String, - methodParameterTypes: List, - isPrivate: Boolean, - frequency: Int - ) { - if (!shouldSkipInvoke(methodName)) - sentenceBlock.stmtTexts.add( - StmtDescription( - StmtType.Invoke, - getMethodReference(className, methodName, methodParameterTypes, isPrivate), - frequency - ) - ) - } - - /** - * Adds RecursionAssignment into sentenceBlock.stmtTexts - */ - protected fun addTextRecursion( - sentenceBlock: SimpleSentenceBlock, - methodName: String, - frequency: Int - ) { - if (!shouldSkipInvoke(methodName)) - sentenceBlock.stmtTexts.add( - StmtDescription( - StmtType.RecursionAssignment, - methodName, - frequency - ) - ) - } - - /** - * Returns a reference to the invoked method. IDE can't resolve references to private methods in comments, - * so we add @link tag only if the invoked method is not private. - * - * It looks like {@link packageName.className#methodName(type1, type2)}. - * - * In case when an enclosing class in nested, we need to replace '$' with '.' - * to render the reference. - */ - fun getMethodReference( - className: String, - methodName: String, - methodParameterTypes: List, - isPrivate: Boolean - ): String { - val prettyClassName: String = className.replace("$", ".") - - val text = if (methodParameterTypes.isEmpty()) { - "$prettyClassName#$methodName()" - } else { - val methodParametersAsString = methodParameterTypes.joinToString(",") - "$prettyClassName#$methodName($methodParametersAsString)" - } - - return if (isPrivate) { - text - } else { - "{@link $text}" - } - } - - /** - * Returns a reference to the class. - * Replaces '$' with '.' in case a class is nested. - */ - fun getClassReference(fullClasName: String): String { - return "{@link ${fullClasName.replace("$", ".")}}" - } - - protected fun buildIterationsBlock( - iterations: List, - activatedStep: Step, - currentMethod: SootMethod - ): Pair> { - val result = mutableListOf() - val jimpleToASTMap = sootToAST[currentMethod] - iterations.forEach { - val sentenceBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) - buildSentenceBlock(it, sentenceBlock, currentMethod) - result.add(sentenceBlock) - } - val firstStmtNode = jimpleToASTMap?.get(iterations.first().step.stmt) - val activatedNode = jimpleToASTMap?.get(activatedStep.stmt) - val line = iterations.first().line - - var iterationDescription = "" - if (firstStmtNode is Statement) { - iterationDescription = getTextIterationDescription(firstStmtNode) - } - // getTextIterationDescription can return empty description, - // that is why if else is not used here. - if (iterationDescription.isEmpty() && activatedNode is Statement) { - iterationDescription = getTextIterationDescription(activatedNode) - } - //heh, we are still looking for loop txt - if (iterationDescription.isEmpty()) { - val nearestNode = jimpleToASTMap?.nearestIterationNode(activatedNode, line) - if (nearestNode != null) { - iterationDescription = getTextIterationDescription(nearestNode) - } - } - - if (iterationDescription.isEmpty()) { - val nearestNode = jimpleToASTMap?.nearestIterationNode(firstStmtNode, line) - if (nearestNode != null) { - iterationDescription = getTextIterationDescription(nearestNode) - } - } - - return Pair(iterationDescription, result) - } -} - -data class IterationDescription(val from: Int, val to: Int, val description: String, val typeDescription: String) +package org.utbot.summary.comment.classic.symbolic + +import com.github.javaparser.ast.stmt.CatchClause +import com.github.javaparser.ast.stmt.ForStmt +import com.github.javaparser.ast.stmt.IfStmt +import com.github.javaparser.ast.stmt.Statement +import com.github.javaparser.ast.stmt.SwitchStmt +import com.github.javaparser.ast.stmt.ThrowStmt +import com.github.javaparser.ast.stmt.SwitchEntry +import org.utbot.framework.plugin.api.ArtificialError +import org.utbot.framework.plugin.api.InstrumentedProcessDeathException +import org.utbot.framework.plugin.api.DocPreTagStatement +import org.utbot.framework.plugin.api.DocRegularStmt +import org.utbot.framework.plugin.api.DocStatement +import org.utbot.framework.plugin.api.Step +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.framework.plugin.api.exceptionOrNull +import org.utbot.summary.AbstractTextBuilder +import org.utbot.summary.NodeConverter +import org.utbot.summary.SummarySentenceConstants.CARRIAGE_RETURN +import org.utbot.summary.ast.JimpleToASTMap +import org.utbot.summary.comment.* +import org.utbot.summary.comment.customtags.getMethodReferenceForSymbolicTest +import org.utbot.summary.tag.BasicTypeTag +import org.utbot.summary.tag.CallOrderTag +import org.utbot.summary.tag.StatementTag +import org.utbot.summary.tag.TraceTagWithoutExecution +import org.utbot.summary.tag.UniquenessTag +import soot.SootMethod +import soot.Type +import soot.jimple.Stmt +import soot.jimple.internal.JAssignStmt +import soot.jimple.internal.JInvokeStmt +import soot.jimple.internal.JVirtualInvokeExpr + +private const val JVM_CRASH_REASON = "JVM crash" +const val EMPTY_STRING = "" + +open class SimpleCommentBuilder( + traceTag: TraceTagWithoutExecution, + sootToAST: MutableMap, + val stringTemplates: StringsTemplatesInterface = StringsTemplatesSingular() +) : + AbstractTextBuilder(traceTag, sootToAST) { + + /** + * Creates String from SimpleSentenceBlock + */ + open fun buildString(currentMethod: SootMethod): String { + val root = SimpleSentenceBlock(stringTemplates = stringTemplates) + buildThrownExceptionInfo(root, currentMethod) + skippedIterations() + buildSentenceBlock(traceTag.rootStatementTag, root, currentMethod) + var sentence = toSentence(root) + + if (sentence.isEmpty()) { + return EMPTY_STRING + } + + sentence = splitLongSentence(sentence) + sentence = lastCommaToDot(sentence) + + return "
    \n$sentence
    ".replace(CARRIAGE_RETURN, "") + } + + private fun buildThrownExceptionInfo( + root: SimpleSentenceBlock, + currentMethod: SootMethod + ) { + traceTag.result.exceptionOrNull()?.let { + val exceptionName = it.javaClass.simpleName + val reason = findExceptionReason(currentMethod, it) + + when (it) { + is TimeoutException, + is ArtificialError -> root.detectedError = reason + else -> root.exceptionThrow = "$exceptionName $reason" + } + } + } + + /** + * Creates List<[DocStatement]> from [SimpleSentenceBlock]. + */ + open fun buildDocStmts(currentMethod: SootMethod): List { + val sentenceBlock = buildSentenceBlock(currentMethod) + val docStmts = toDocStmts(sentenceBlock) + + if (docStmts.isEmpty()) { + return emptyList() + } +// sentence = splitLongSentence(sentence) //TODO SAT-1309 +// sentence = lastCommaToDot(sentence) //TODO SAT-1309 + + return listOf(DocPreTagStatement(docStmts)) + } + + private fun buildSentenceBlock(currentMethod: SootMethod): SimpleSentenceBlock { + val rootSentenceBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) + buildThrownExceptionInfo(rootSentenceBlock, currentMethod) + skippedIterations() + buildSentenceBlock(traceTag.rootStatementTag, rootSentenceBlock, currentMethod) + return rootSentenceBlock + } + + /** + * Transforms rootSentenceBlock into String + */ + protected fun toSentence(rootSentenceBlock: SimpleSentenceBlock): String { + rootSentenceBlock.squashStmtText() + val buildSentence = rootSentenceBlock.toSentence() + if (buildSentence.isEmpty()) return "" + return "${stringTemplates.sentenceBeginning} $buildSentence" + } + + /** + * Transforms rootSentenceBlock into List + */ + protected fun toDocStmts(rootSentenceBlock: SimpleSentenceBlock): List { + val stmts = mutableListOf() + + rootSentenceBlock.squashStmtText() + stmts += rootSentenceBlock.toDocStmt() + if (stmts.isEmpty()) return emptyList() + + stmts.add(0, DocRegularStmt("${stringTemplates.sentenceBeginning} ")) + return stmts + } + + protected fun findExceptionReason(currentMethod: SootMethod, thrownException: Throwable): String { + val path = traceTag.path + if (path.isEmpty()) { + if (thrownException is InstrumentedProcessDeathException) { + return JVM_CRASH_REASON + } + + error("Cannot find last path step for exception $thrownException") + } + + return findExceptionReason(path.last(), currentMethod) + } + + /** + * Tries to find AST node where the exception was thrown + * or AST node that contains a condition which lead to the throw. + */ + private fun findExceptionReason(step: Step, currentMethod: SootMethod): String = + StringBuilder() + .let { stringBuilder -> + val jimpleToASTMap = sootToAST[currentMethod] ?: return "" + + val exceptionNode = + jimpleToASTMap.stmtToASTNode[step.stmt] + .let { node -> + if (node is ThrowStmt) getExceptionReasonForComment(node) + else node + } + ?.also { node -> + stringBuilder.append( + if (node is IfStmt || node is SwitchEntry) "when: " + else "in: " + ) + } + ?: return "" + + stringBuilder + .append( + when { + exceptionNode is IfStmt -> exceptionNode.condition.toString() + exceptionNode is SwitchEntry -> NodeConverter.convertSwitchEntry(exceptionNode, step, removeSpaces = false) + exceptionNode is SwitchStmt -> NodeConverter.convertSwitchStmt(exceptionNode, step, removeSpaces = false) + isLoopStatement(exceptionNode) -> getTextIterationDescription(exceptionNode) + else -> exceptionNode.toString() + } + ) + } + .toString() + .replace(CARRIAGE_RETURN, "") + + /** + * Sentence blocks are built based on unique and partly unique statement tags. + */ + open fun buildSentenceBlock( + statementTag: StatementTag?, + sentenceBlock: SimpleSentenceBlock, + currentMethod: SootMethod + ) { + val jimpleToASTMap = sootToAST[currentMethod] + if (statementTag == null) return + if (jimpleToASTMap == null) return + val recursion = statementTag.recursion + val stmt = statementTag.step.stmt + val invoke = statementTag.invoke + var createNextBlock = false + + val localNoIterations = statementTagSkippedIteration(statementTag, currentMethod) + if (localNoIterations.isNotEmpty()) { + sentenceBlock.notExecutedIterations = localNoIterations + methodToNoIterationDescription[currentMethod]?.removeAll(localNoIterations) + } + + val invokeSootMethod = statementTag.invokeSootMethod() + var invokeRegistered = false + if (invoke != null && invokeSootMethod != null) { + val className = invokeSootMethod.declaringClass.name + val methodName = invokeSootMethod.name + val methodParameterTypes = invokeSootMethod.parameterTypes + val sentenceInvoke = SimpleSentenceBlock(stringTemplates = sentenceBlock.stringTemplates) + buildSentenceBlock(invoke, sentenceInvoke, invokeSootMethod) + sentenceInvoke.squashStmtText() + if (!sentenceInvoke.isEmpty()) { + sentenceBlock.invokeSentenceBlock = + Pair( + getMethodReferenceForSymbolicTest(className, methodName, methodParameterTypes, invokeSootMethod.isPrivate), + sentenceInvoke + ) + createNextBlock = true + invokeRegistered = true + } + } + if (statementTag.basicTypeTag == BasicTypeTag.Invoke && statementTag.uniquenessTag == UniquenessTag.Unique && !invokeRegistered) { + if (statementTag.executionFrequency <= 1) { + addTextInvoke(sentenceBlock, stmt, statementTag.executionFrequency) + } + if (statementTag.executionFrequency > 1 && statementTag.callOrderTag == CallOrderTag.First) { + addTextInvoke(sentenceBlock, stmt, statementTag.executionFrequency) + } + } + + if (statementTag.basicTypeTag == BasicTypeTag.RecursionAssignment && statementTag.uniquenessTag == UniquenessTag.Unique && !invokeRegistered) { + if (statementTag.executionFrequency <= 1) { + addTextRecursion(sentenceBlock, stmt, statementTag.executionFrequency) + } + if (statementTag.executionFrequency > 1 && statementTag.callOrderTag == CallOrderTag.First) { + addTextRecursion(sentenceBlock, stmt, statementTag.executionFrequency) + } + } + + if (jimpleToASTMap[statementTag.step.stmt] !is ForStmt) { + + if (statementTag.basicTypeTag == BasicTypeTag.Condition && statementTag.callOrderTag == CallOrderTag.First) { + if (statementTag.uniquenessTag == UniquenessTag.Unique) { + val conditionText = textCondition(statementTag, jimpleToASTMap) + if (conditionText != null) { + sentenceBlock.stmtTexts.add(StmtDescription(StmtType.Condition, conditionText)) + } + } + if (statementTag.uniquenessTag == UniquenessTag.Partly && statementTag.executionFrequency == 1) { + val conditionText = textCondition(statementTag, jimpleToASTMap) + if (conditionText != null) { + sentenceBlock.stmtTexts.add(StmtDescription(StmtType.Condition, conditionText)) + } + } + } + + if (statementTag.basicTypeTag == BasicTypeTag.SwitchCase && statementTag.uniquenessTag == UniquenessTag.Unique) { + textSwitchCase(statementTag.step, jimpleToASTMap) + ?.let { description -> + sentenceBlock.stmtTexts.add(StmtDescription(StmtType.SwitchCase, description)) + } + } + if (statementTag.basicTypeTag == BasicTypeTag.CaughtException) { + jimpleToASTMap[stmt].let { + if (it is CatchClause) { + sentenceBlock.stmtTexts.add(StmtDescription(StmtType.CaughtException, it.parameter.toString())) + } + } + } + if (statementTag.basicTypeTag == BasicTypeTag.Return) { + textReturn(statementTag, sentenceBlock, stmt, jimpleToASTMap) + } + } + + if (statementTag.iterations.isNotEmpty()) { + val iterationSentenceBlock = buildIterationsBlock(statementTag.iterations, statementTag.step, currentMethod) + sentenceBlock.iterationSentenceBlocks.add(iterationSentenceBlock) + createNextBlock = true + } + + if (recursion != null) { + if (stmt is JAssignStmt) { + val name = (stmt.rightOp as JVirtualInvokeExpr).method.name + val sentenceRecursionBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) + buildSentenceBlock(recursion, sentenceRecursionBlock, currentMethod) + sentenceBlock.recursion = Pair(name, sentenceRecursionBlock) + createNextBlock = true + } + if (stmt is JInvokeStmt) { + val name = stmt.invokeExpr.method.name + val sentenceRecursion = SimpleSentenceBlock(stringTemplates = stringTemplates) + buildSentenceBlock(recursion, sentenceRecursion, currentMethod) + sentenceBlock.recursion = Pair(name, sentenceRecursion) + createNextBlock = true + } + } + + if (createNextBlock) { + val nextSentenceBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) + sentenceBlock.nextBlock = nextSentenceBlock + buildSentenceBlock(statementTag.next, nextSentenceBlock, currentMethod) + } else { + buildSentenceBlock(statementTag.next, sentenceBlock, currentMethod) + } + } + + /** + * Adds RecursionAssignment into sentenceBlock.stmtTexts + */ + protected fun addTextRecursion(sentenceBlock: SimpleSentenceBlock, stmt: Stmt, frequency: Int) { + if (stmt is JAssignStmt || stmt is JInvokeStmt) { + val className = stmt.invokeExpr.methodRef.declaringClass.name + val methodName = stmt.invokeExpr.method.name + addTextRecursion(sentenceBlock, className, methodName, frequency) + } + } + + /** + * Adds Invoke into sentenceBlock.stmtTexts + */ + protected fun addTextInvoke(sentenceBlock: SimpleSentenceBlock, stmt: Stmt, frequency: Int) { + if (stmt is JAssignStmt || stmt is JInvokeStmt) { + val className = stmt.invokeExpr.methodRef.declaringClass.name + val methodName = stmt.invokeExpr.method.name + val methodParameterTypes = stmt.invokeExpr.method.parameterTypes + val isPrivate = stmt.invokeExpr.method.isPrivate + addTextInvoke( + sentenceBlock, + className, + methodName, + methodParameterTypes, + isPrivate, + frequency + ) + } + } + + /** + * Adds Invoke into sentenceBlock.stmtTexts + */ + protected fun addTextInvoke( + sentenceBlock: SimpleSentenceBlock, + className: String, + methodName: String, + methodParameterTypes: List, + isPrivate: Boolean, + frequency: Int + ) { + if (!shouldSkipInvoke(className, methodName)) + sentenceBlock.stmtTexts.add( + StmtDescription( + StmtType.Invoke, + getMethodReferenceForSymbolicTest(className, methodName, methodParameterTypes, isPrivate), + frequency + ) + ) + } + + /** + * Adds RecursionAssignment into sentenceBlock.stmtTexts + */ + protected fun addTextRecursion( + sentenceBlock: SimpleSentenceBlock, + className: String, + methodName: String, + frequency: Int + ) { + if (!shouldSkipInvoke(className, methodName)) + sentenceBlock.stmtTexts.add( + StmtDescription( + StmtType.RecursionAssignment, + methodName, + frequency + ) + ) + } + + protected fun buildIterationsBlock( + iterations: List, + activatedStep: Step, + currentMethod: SootMethod + ): Pair> { + val result = mutableListOf() + val jimpleToASTMap = sootToAST[currentMethod] + iterations.forEach { + val sentenceBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) + buildSentenceBlock(it, sentenceBlock, currentMethod) + result.add(sentenceBlock) + } + val firstStmtNode = jimpleToASTMap?.get(iterations.first().step.stmt) + val activatedNode = jimpleToASTMap?.get(activatedStep.stmt) + val line = iterations.first().line + + var iterationDescription = "" + if (firstStmtNode is Statement) { + iterationDescription = getTextIterationDescription(firstStmtNode) + } + // getTextIterationDescription can return empty description, + // that is why if else is not used here. + if (iterationDescription.isEmpty() && activatedNode is Statement) { + iterationDescription = getTextIterationDescription(activatedNode) + } + //heh, we are still looking for loop txt + if (iterationDescription.isEmpty()) { + val nearestNode = jimpleToASTMap?.nearestIterationNode(activatedNode, line) + if (nearestNode != null) { + iterationDescription = getTextIterationDescription(nearestNode) + } + } + + if (iterationDescription.isEmpty()) { + val nearestNode = jimpleToASTMap?.nearestIterationNode(firstStmtNode, line) + if (nearestNode != null) { + iterationDescription = getTextIterationDescription(nearestNode) + } + } + + return Pair(iterationDescription, result) + } +} + +data class IterationDescription(val from: Int, val to: Int, val description: String, val typeDescription: String) diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleSentenceBlock.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/classic/symbolic/SimpleSentenceBlock.kt similarity index 94% rename from utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleSentenceBlock.kt rename to utbot-summary/src/main/kotlin/org/utbot/summary/comment/classic/symbolic/SimpleSentenceBlock.kt index 0caa0eaf42..faa6b2fdd1 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleSentenceBlock.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/classic/symbolic/SimpleSentenceBlock.kt @@ -1,4 +1,4 @@ -package org.utbot.summary.comment +package org.utbot.summary.comment.classic.symbolic import org.utbot.framework.plugin.api.DocCodeStmt import org.utbot.framework.plugin.api.DocRegularStmt @@ -12,12 +12,14 @@ import org.utbot.summary.SummarySentenceConstants.NEW_LINE import org.utbot.summary.SummarySentenceConstants.OPEN_BRACKET import org.utbot.summary.SummarySentenceConstants.SENTENCE_SEPARATION import org.utbot.summary.SummarySentenceConstants.TAB +import org.utbot.summary.comment.* class SimpleSentenceBlock(val stringTemplates: StringsTemplatesInterface) { val iterationSentenceBlocks = mutableListOf>>() var notExecutedIterations: List? = null var recursion: Pair? = null var exceptionThrow: String? = null + var detectedError: String? = null var nextBlock: SimpleSentenceBlock? = null val stmtTexts = mutableListOf() @@ -121,6 +123,15 @@ class SimpleSentenceBlock(val stringTemplates: StringsTemplatesInterface) { result += NEW_LINE } + detectedError?.let { + if (restartSentence) { + result += stringTemplates.sentenceBeginning + " " + restartSentence = false + } + result += stringTemplates.suspiciousBehaviorDetectedSentence.format(it) + result += NEW_LINE + } + return result } @@ -237,6 +248,16 @@ class SimpleSentenceBlock(val stringTemplates: StringsTemplatesInterface) { docStmts += DocRegularStmt(NEW_LINE) } + detectedError?.let { + docStmts += DocRegularStmt(NEW_LINE) + if (restartSentence) { + docStmts += DocRegularStmt(stringTemplates.sentenceBeginning + " ") + restartSentence = false + } + docStmts += DocRegularStmt(stringTemplates.suspiciousBehaviorDetectedSentence.format(it)) + docStmts += DocRegularStmt(NEW_LINE) + } + return docStmts } @@ -322,6 +343,7 @@ class SimpleSentenceBlock(val stringTemplates: StringsTemplatesInterface) { if (nextBlock?.isEmpty() == false) return false if (notExecutedIterations.isNullOrEmpty().not()) return false exceptionThrow?.let { return false } + detectedError?.let { return false } if (invokeSentenceBlock != null) return false return true } @@ -477,6 +499,7 @@ interface StringsTemplatesInterface { val noIteration: String val codeSentence: String val throwSentence: String + val suspiciousBehaviorDetectedSentence: String val conditionLine: String val returnLine: String @@ -497,13 +520,14 @@ open class StringsTemplatesSingular : StringsTemplatesInterface { override val noIteration: String = "does not iterate" override val codeSentence: String = "$OPEN_BRACKET$AT_CODE %s$CLOSE_BRACKET" override val throwSentence: String = "throws %s" + override val suspiciousBehaviorDetectedSentence: String = "detects suspicious behavior %s" //used in Squashing override val conditionLine: String = "executes conditions:$NEW_LINE" override val returnLine: String = "returns from: " override val countedReturnLine: String = "returns from:$NEW_LINE" override val invokeLine: String = "invokes:$NEW_LINE" - override val switchCaseLine: String = "activates switch case: " + override val switchCaseLine: String = "activates " override val caughtExceptionLine: String = "catches exception:$NEW_LINE" override val recursionAssignmentLine: String = "triggers recursion of " } @@ -518,13 +542,14 @@ class StringsTemplatesPlural : StringsTemplatesSingular() { override val noIteration: String = "does not iterate" override val codeSentence: String = "$OPEN_BRACKET$AT_CODE %s$CLOSE_BRACKET" override val throwSentence: String = "throw %s" + override val suspiciousBehaviorDetectedSentence: String = "detect suspicious behavior %s" //used in Squashing override val conditionLine: String = "execute conditions:$NEW_LINE" override val returnLine: String = "return from: " override val countedReturnLine: String = "return from:$NEW_LINE" override val invokeLine: String = "invoke:$NEW_LINE" - override val switchCaseLine: String = "activate switch case: " + override val switchCaseLine: String = "activate " override val caughtExceptionLine: String = "catches exception:$NEW_LINE" override val recursionAssignmentLine: String = "trigger recursion of " } diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SymbolicExecutionClusterCommentBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/cluster/SymbolicExecutionClusterCommentBuilder.kt similarity index 93% rename from utbot-summary/src/main/kotlin/org/utbot/summary/comment/SymbolicExecutionClusterCommentBuilder.kt rename to utbot-summary/src/main/kotlin/org/utbot/summary/comment/cluster/SymbolicExecutionClusterCommentBuilder.kt index 76f760d66f..7ea53fbab4 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SymbolicExecutionClusterCommentBuilder.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/cluster/SymbolicExecutionClusterCommentBuilder.kt @@ -1,12 +1,14 @@ -package org.utbot.summary.comment +package org.utbot.summary.comment.cluster import com.github.javaparser.ast.stmt.CatchClause import com.github.javaparser.ast.stmt.ForStmt import org.utbot.framework.plugin.api.DocPreTagStatement -import org.utbot.framework.plugin.api.DocRegularStmt import org.utbot.framework.plugin.api.DocStatement import org.utbot.summary.SummarySentenceConstants.CARRIAGE_RETURN import org.utbot.summary.ast.JimpleToASTMap +import org.utbot.summary.comment.* +import org.utbot.summary.comment.classic.symbolic.* +import org.utbot.summary.comment.customtags.getMethodReferenceForSymbolicTest import org.utbot.summary.tag.BasicTypeTag import org.utbot.summary.tag.CallOrderTag import org.utbot.summary.tag.StatementTag @@ -41,7 +43,7 @@ class SymbolicExecutionClusterCommentBuilder( sentence = splitLongSentence(sentence) sentence = lastCommaToDot(sentence) - return "
    \n$sentence
    ".replace(CARRIAGE_RETURN, "") + return "
    \n$sentence
    ".replace(CARRIAGE_RETURN, EMPTY_STRING) } override fun buildDocStmts(currentMethod: SootMethod): List { @@ -97,7 +99,7 @@ class SymbolicExecutionClusterCommentBuilder( if (!sentenceInvoke.isEmpty()) { sentenceBlock.invokeSentenceBlock = Pair( - getMethodReference(className, methodName, methodParameterTypes, isPrivate), + getMethodReferenceForSymbolicTest(className, methodName, methodParameterTypes, isPrivate), sentenceInvoke ) createNextBlock = true @@ -134,10 +136,10 @@ class SymbolicExecutionClusterCommentBuilder( } if (statementTag.basicTypeTag == BasicTypeTag.SwitchCase && statementTag.uniquenessTag == UniquenessTag.Unique) { - val switchCase = textSwitchCase(statementTag.step, jimpleToASTMap) - if (switchCase != null) { - sentenceBlock.stmtTexts.add(StmtDescription(StmtType.SwitchCase, switchCase)) - } + textSwitchCase(statementTag.step, jimpleToASTMap) + ?.let { description -> + sentenceBlock.stmtTexts.add(StmtDescription(StmtType.SwitchCase, description)) + } } if (statementTag.basicTypeTag == BasicTypeTag.CaughtException && statementTag.uniquenessTag == UniquenessTag.Unique) { jimpleToASTMap[stmt].let { diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/CustomJavaDocTagProvider.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/CustomJavaDocTagProvider.kt new file mode 100644 index 0000000000..73819ea445 --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/CustomJavaDocTagProvider.kt @@ -0,0 +1,112 @@ +package org.utbot.summary.comment.customtags.symbolic + +import org.utbot.framework.plugin.api.DocRegularLineStmt +import org.utbot.framework.plugin.api.DocRegularStmt +import org.utbot.framework.plugin.api.DocStatement +import org.utbot.summary.comment.customtags.fuzzer.CommentWithCustomTagForTestProducedByFuzzer + +/** + * Provides a list of supported custom JavaDoc tags. + */ +class CustomJavaDocTagProvider { + // The tags' order is important because plugin builds final JavaDoc comment according to it. + fun getPluginCustomTags(): List = + listOf( + CustomJavaDocTag.ClassUnderTest, + CustomJavaDocTag.MethodUnderTest, + CustomJavaDocTag.ExpectedResult, + CustomJavaDocTag.ActualResult, + CustomJavaDocTag.Executes, + CustomJavaDocTag.Invokes, + CustomJavaDocTag.Iterates, + CustomJavaDocTag.SwitchCase, + CustomJavaDocTag.Recursion, + CustomJavaDocTag.ReturnsFrom, + CustomJavaDocTag.CaughtException, + CustomJavaDocTag.ThrowsException, + CustomJavaDocTag.DetectsSuspiciousBehavior, + ) +} + +sealed class CustomJavaDocTag( + val name: String, + val message: String, + private val valueRetriever: (CustomJavaDocComment) -> Any, + private val valueRetrieverFuzzer: ((CommentWithCustomTagForTestProducedByFuzzer) -> Any)? // TODO: remove after refactoring +) { + object ClassUnderTest : + CustomJavaDocTag( + "utbot.classUnderTest", + "Class under test", + CustomJavaDocComment::classUnderTest, + CommentWithCustomTagForTestProducedByFuzzer::classUnderTest + ) + + object MethodUnderTest : + CustomJavaDocTag( + "utbot.methodUnderTest", + "Method under test", + CustomJavaDocComment::methodUnderTest, + CommentWithCustomTagForTestProducedByFuzzer::methodUnderTest + ) + + object ExpectedResult : + CustomJavaDocTag("utbot.expectedResult", "Expected result", CustomJavaDocComment::expectedResult, null) + + object ActualResult : + CustomJavaDocTag("utbot.actualResult", "Actual result", CustomJavaDocComment::actualResult, null) + + object Executes : + CustomJavaDocTag("utbot.executesCondition", "Executes condition", CustomJavaDocComment::executesCondition, null) + + object Invokes : CustomJavaDocTag("utbot.invokes", "Invokes", CustomJavaDocComment::invokes, null) + object Iterates : CustomJavaDocTag("utbot.iterates", "Iterates", CustomJavaDocComment::iterates, null) + object SwitchCase : + CustomJavaDocTag("utbot.activatesSwitch", "Activates switch", CustomJavaDocComment::switchCase, null) + + object Recursion : + CustomJavaDocTag("utbot.triggersRecursion", "Triggers recursion ", CustomJavaDocComment::recursion, null) + + object ReturnsFrom : CustomJavaDocTag("utbot.returnsFrom", "Returns from", CustomJavaDocComment::returnsFrom, null) + object CaughtException : + CustomJavaDocTag("utbot.caughtException", "Caught exception", CustomJavaDocComment::caughtException, null) + + object ThrowsException : + CustomJavaDocTag("utbot.throwsException", "Throws exception", CustomJavaDocComment::throwsException, null) + + object DetectsSuspiciousBehavior : + CustomJavaDocTag("utbot.detectsSuspiciousBehavior", "Detects suspicious behavior", CustomJavaDocComment::detectsSuspiciousBehavior, null) + + fun generateDocStatement(comment: CustomJavaDocComment): DocRegularStmt? = + when (val value = valueRetriever.invoke(comment)) { + is String -> value.takeIf { it.isNotEmpty() }?.let { + DocRegularStmt("@$name $value\n") + } + is List<*> -> value.takeIf { it.isNotEmpty() }?.let { + val valueToString = value.joinToString(separator = "\n", postfix = "\n") { "@$name $it" } + + DocRegularStmt(valueToString) + } + else -> null + } + + // TODO: could be universal with the function above after creation of hierarchy data classes related to the comments + fun generateDocStatementForTestProducedByFuzzer(comment: CommentWithCustomTagForTestProducedByFuzzer): DocStatement? { + if (valueRetrieverFuzzer != null) { //TODO: it required only when we have two different retrievers + return when (val value = valueRetrieverFuzzer!!.invoke(comment)) { // TODO: unsafe !! - resolve + is String -> value.takeIf { it.isNotEmpty() }?.let { + DocRegularLineStmt("@$name $value") + } + + is List<*> -> value.takeIf { it.isNotEmpty() }?.let { + val valueToString = value.joinToString(separator = ",\n", postfix = "\n") + + DocRegularStmt("@$name $valueToString") + } + + else -> null + } + } + return null + } +} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/CustomTagsUtil.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/CustomTagsUtil.kt new file mode 100644 index 0000000000..5d503461f2 --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/CustomTagsUtil.kt @@ -0,0 +1,84 @@ +package org.utbot.summary.comment.customtags + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.summary.SummarySentenceConstants.CARRIAGE_RETURN +import org.utbot.summary.SummarySentenceConstants.JAVA_CLASS_DELIMITER +import org.utbot.summary.SummarySentenceConstants.JAVA_DOC_CLASS_DELIMITER +import org.utbot.summary.comment.classic.symbolic.EMPTY_STRING +import soot.Type + +/** + * Returns a reference to the invoked method. IDE can't resolve references to private methods in comments, + * so we add @link tag only if the invoked method is not private. + * + * It looks like {@link packageName.className#methodName(type1, type2)}. + * + * In case when an enclosing class in nested, we need to replace '$' with '.' + * to render the reference. + */ +fun getMethodReferenceForSymbolicTest( + className: String, + methodName: String, + methodParameterTypes: List, + isPrivate: Boolean +): String { + val methodParametersAsString = if (methodParameterTypes.isNotEmpty()) methodParameterTypes.joinToString(",") else EMPTY_STRING + + return formMethodReferenceForJavaDoc(className, methodName, methodParametersAsString, isPrivate) +} + +/** + * Returns a reference to the invoked method. + * + * It looks like {@link packageName.className#methodName(type1, type2)}. + * + * In case when an enclosing class in nested, we need to replace '$' with '.' + * to render the reference. + */ +fun getMethodReferenceForFuzzingTest(className: String, methodName: String, methodParameterTypes: List, isPrivate: Boolean): String { + val methodParametersAsString = if (methodParameterTypes.isNotEmpty()) methodParameterTypes.joinToString(",") { it.canonicalName } else EMPTY_STRING + + return formMethodReferenceForJavaDoc(className, methodName, methodParametersAsString, isPrivate).replace( + CARRIAGE_RETURN, EMPTY_STRING + ) +} + +private fun formMethodReferenceForJavaDoc( + className: String, + methodName: String, + methodParametersAsString: String, + isPrivate: Boolean +): String { + // to avoid $ in names for static inner classes + val prettyClassName: String = className.replace(JAVA_CLASS_DELIMITER, JAVA_DOC_CLASS_DELIMITER) + val validMethodParameters = methodParametersAsString.replace(JAVA_CLASS_DELIMITER, JAVA_DOC_CLASS_DELIMITER) + + val text = if (validMethodParameters == EMPTY_STRING) { + "$prettyClassName#$methodName()" + } else { + "$prettyClassName#$methodName($validMethodParameters)" + } + + return if (isPrivate) { + text + } else { + "{@link $text}" + } +} + +/** + * Returns a reference to the class. + * Replaces '$' with '.' in case a class is nested. + */ +fun getClassReference(fullClassName: String): String { + return "{@link ${fullClassName.replace(JAVA_CLASS_DELIMITER, JAVA_DOC_CLASS_DELIMITER)}}".replace(CARRIAGE_RETURN, EMPTY_STRING) +} + +/** Returns correct full class name. */ +fun getFullClassName(canonicalName: String?, packageName: String, className: String, isNested: Boolean): String { + return if (isNested && canonicalName != null) { + canonicalName + } else { + if (packageName.isEmpty()) className else "$packageName.$className" + } +} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/fuzzer/CommentWithCustomTagForTestProducedByFuzzer.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/fuzzer/CommentWithCustomTagForTestProducedByFuzzer.kt new file mode 100644 index 0000000000..6cefc2c888 --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/fuzzer/CommentWithCustomTagForTestProducedByFuzzer.kt @@ -0,0 +1,15 @@ +package org.utbot.summary.comment.customtags.fuzzer + +import org.utbot.summary.comment.classic.symbolic.EMPTY_STRING + +/** + * Represents a set of plugin's custom JavaDoc tags. + */ +data class CommentWithCustomTagForTestProducedByFuzzer( + val classUnderTest: String = EMPTY_STRING, + val methodUnderTest: String = EMPTY_STRING, + val expectedResult: String = EMPTY_STRING, + val actualResult: String = EMPTY_STRING, + var returnsFrom: String = EMPTY_STRING, + var throwsException: String = EMPTY_STRING +) \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/fuzzer/CommentWithCustomTagForTestProducedByFuzzerBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/fuzzer/CommentWithCustomTagForTestProducedByFuzzerBuilder.kt new file mode 100644 index 0000000000..ca65e00a8c --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/fuzzer/CommentWithCustomTagForTestProducedByFuzzerBuilder.kt @@ -0,0 +1,59 @@ +package org.utbot.summary.comment.customtags.fuzzer + +import org.utbot.framework.plugin.api.DocCustomTagStatement +import org.utbot.framework.plugin.api.DocStatement +import org.utbot.framework.plugin.api.UtExecutionResult +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.summary.comment.customtags.getClassReference +import org.utbot.summary.comment.customtags.getFullClassName +import org.utbot.summary.comment.customtags.getMethodReferenceForFuzzingTest +import org.utbot.summary.comment.customtags.symbolic.CustomJavaDocTagProvider + +/** + * Builds JavaDoc comments for generated tests using plugin's custom JavaDoc tags. + */ +class CommentWithCustomTagForTestProducedByFuzzerBuilder( + val methodDescription: FuzzedMethodDescription, + val values: List, + val result: UtExecutionResult? +) { + /** + * Collects statements for final JavaDoc comment. + */ + fun buildDocStatements(): List { + val comment = buildCustomJavaDocComment() + val docStatementList = + CustomJavaDocTagProvider().getPluginCustomTags() + .mapNotNull { it.generateDocStatementForTestProducedByFuzzer(comment) } + return listOf(DocCustomTagStatement(docStatementList)) + } + + private fun buildCustomJavaDocComment(): CommentWithCustomTagForTestProducedByFuzzer { + val packageName = methodDescription.packageName + val className = methodDescription.className + val methodName = methodDescription.compilableName + val canonicalName = methodDescription.canonicalName + val isNested = methodDescription.isNested + + return if (packageName != null && className != null && methodName != null) { + val fullClassName = getFullClassName(canonicalName, packageName, className, isNested) + + val methodReference = getMethodReferenceForFuzzingTest( + fullClassName, + methodName, + methodDescription.parameters, + false + ) + + val classReference = getClassReference(fullClassName) + + CommentWithCustomTagForTestProducedByFuzzer( + classUnderTest = classReference, + methodUnderTest = methodReference, + ) + } else { + CommentWithCustomTagForTestProducedByFuzzer() + } + } +} diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/symbolic/CustomJavaDocComment.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/symbolic/CustomJavaDocComment.kt new file mode 100644 index 0000000000..373f101ac1 --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/symbolic/CustomJavaDocComment.kt @@ -0,0 +1,23 @@ +package org.utbot.summary.comment.customtags.symbolic + +import org.utbot.summary.comment.classic.symbolic.EMPTY_STRING + +/** + * Represents a set of plugin's custom JavaDoc tags. + */ +data class CustomJavaDocComment( + val classUnderTest: String = EMPTY_STRING, + val methodUnderTest: String = EMPTY_STRING, + val expectedResult: String = EMPTY_STRING, + val actualResult: String = EMPTY_STRING, + var executesCondition: List = listOf(), + var invokes: List = listOf(), + var iterates: List = listOf(), + var switchCase: String = EMPTY_STRING, + var recursion: String = EMPTY_STRING, + var returnsFrom: String = EMPTY_STRING, + var countedReturn: String = EMPTY_STRING, + var caughtException: String = EMPTY_STRING, + var throwsException: String = EMPTY_STRING, + var detectsSuspiciousBehavior: String = EMPTY_STRING +) \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocCommentBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/symbolic/CustomJavaDocCommentBuilder.kt similarity index 82% rename from utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocCommentBuilder.kt rename to utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/symbolic/CustomJavaDocCommentBuilder.kt index 04e412a3c7..613a9daea6 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/CustomJavaDocCommentBuilder.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/customtags/symbolic/CustomJavaDocCommentBuilder.kt @@ -1,10 +1,16 @@ -package org.utbot.summary.comment +package org.utbot.summary.comment.customtags.symbolic +import org.utbot.framework.plugin.api.ArtificialError import org.utbot.framework.plugin.api.DocCustomTagStatement import org.utbot.framework.plugin.api.DocStatement +import org.utbot.framework.plugin.api.TimeoutException import org.utbot.framework.plugin.api.exceptionOrNull import org.utbot.summary.SummarySentenceConstants.CARRIAGE_RETURN import org.utbot.summary.ast.JimpleToASTMap +import org.utbot.summary.comment.* +import org.utbot.summary.comment.classic.symbolic.* +import org.utbot.summary.comment.customtags.getClassReference +import org.utbot.summary.comment.customtags.getMethodReferenceForSymbolicTest import org.utbot.summary.tag.TraceTagWithoutExecution import soot.SootMethod @@ -27,7 +33,7 @@ class CustomJavaDocCommentBuilder( } private fun buildCustomJavaDocComment(currentMethod: SootMethod): CustomJavaDocComment { - val methodReference = getMethodReference( + val methodReference = getMethodReferenceForSymbolicTest( currentMethod.declaringClass.name, currentMethod.name, currentMethod.parameterTypes, @@ -36,8 +42,8 @@ class CustomJavaDocCommentBuilder( val classReference = getClassReference(currentMethod.declaringClass.javaStyleName) val comment = CustomJavaDocComment( - classUnderTest = classReference.replace(CARRIAGE_RETURN, ""), - methodUnderTest = methodReference.replace(CARRIAGE_RETURN, ""), + classUnderTest = classReference, + methodUnderTest = methodReference, ) val rootSentenceBlock = SimpleSentenceBlock(stringTemplates = stringTemplates) @@ -50,8 +56,13 @@ class CustomJavaDocCommentBuilder( if (thrownException != null) { val exceptionName = thrownException.javaClass.name val reason = findExceptionReason(currentMethod, thrownException) + .replace(CARRIAGE_RETURN, "") - comment.throwsException = "{@link $exceptionName} $reason".replace(CARRIAGE_RETURN, "") + when (thrownException) { + is TimeoutException, + is ArtificialError -> comment.detectsSuspiciousBehavior = reason + else -> comment.throwsException = "{@link $exceptionName} $reason" + } } if (rootSentenceBlock.recursion != null) { @@ -97,7 +108,7 @@ class CustomJavaDocCommentBuilder( StmtType.Condition -> comment.executesCondition += "{@code ${statement.description.replace(CARRIAGE_RETURN, "")}}" StmtType.Return -> comment.returnsFrom = "{@code ${statement.description.replace(CARRIAGE_RETURN, "")}}" StmtType.CaughtException -> comment.caughtException = "{@code ${statement.description.replace(CARRIAGE_RETURN, "")}}" - StmtType.SwitchCase -> comment.switchCase = "{@code case ${statement.description.replace(CARRIAGE_RETURN, "")}}" + StmtType.SwitchCase -> comment.switchCase = "{@code ${statement.description.replace(CARRIAGE_RETURN, "")}}" StmtType.CountedReturn -> comment.countedReturn = "{@code ${statement.description.replace(CARRIAGE_RETURN, "")}}" StmtType.RecursionAssignment -> comment.recursion = "of {@code ${statement.description.replace(CARRIAGE_RETURN, "")}}" } diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/MethodBasedNameSuggester.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/MethodBasedNameSuggester.kt index 80583f7cce..e38f566657 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/MethodBasedNameSuggester.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/MethodBasedNameSuggester.kt @@ -3,9 +3,19 @@ package org.utbot.summary.fuzzer.names import org.utbot.framework.plugin.api.UtExecutionResult import org.utbot.fuzzer.FuzzedMethodDescription import org.utbot.fuzzer.FuzzedValue +import org.utbot.summary.MethodDescriptionSource +import java.util.* -class MethodBasedNameSuggester : NameSuggester { - override fun suggest(description: FuzzedMethodDescription, values: List, result: UtExecutionResult?): Sequence { - return sequenceOf(TestSuggestedInfo("test${description.compilableName?.capitalize() ?: "Created"}ByFuzzer")) +class MethodBasedNameSuggester(private val source: MethodDescriptionSource = MethodDescriptionSource.FUZZER) : NameSuggester { + override fun suggest( + description: FuzzedMethodDescription, + values: List, + result: UtExecutionResult? + ): Sequence { + val compilableName = description.compilableName?.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + ?: "Created" + // See [Summarization.generateSummariesForTests]. + val suffix = if (source == MethodDescriptionSource.FUZZER) "ByFuzzer" else "" + return sequenceOf(TestSuggestedInfo("test${compilableName}${suffix}")) } } \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/ModelBasedNameSuggester.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/ModelBasedNameSuggester.kt index 38160e4ddd..d7390ca2ae 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/ModelBasedNameSuggester.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/ModelBasedNameSuggester.kt @@ -1,16 +1,14 @@ package org.utbot.summary.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.UtSettings +import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.util.voidClassId import org.utbot.fuzzer.FuzzedMethodDescription import org.utbot.fuzzer.FuzzedValue +import org.utbot.summary.SummarySentenceConstants.FROM_TO_NAMES_COLON +import org.utbot.summary.SummarySentenceConstants.FROM_TO_NAMES_TRANSITION +import org.utbot.summary.comment.classic.fuzzer.SimpleCommentForTestProducedByFuzzerBuilder +import org.utbot.summary.comment.customtags.fuzzer.CommentWithCustomTagForTestProducedByFuzzerBuilder import java.util.* class ModelBasedNameSuggester( @@ -36,8 +34,9 @@ class ModelBasedNameSuggester( return sequenceOf( TestSuggestedInfo( - testName = createTestName(description, values, result), - displayName = createDisplayName(description, values, result) + testName = if (UtSettings.enableTestNamesGeneration) createTestName(description, values, result) else null, + displayName = if (UtSettings.enableDisplayNameGeneration) createDisplayName(description, values, result) else null, + javaDoc = if (UtSettings.enableJavaDocGeneration) createJavaDoc(description, values, result) else null ) ) } @@ -106,12 +105,17 @@ class ModelBasedNameSuggester( * 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` + * + * NOTE: The ```:``` symbol is used as a separator instead + * of ```->``` if the [UtSettings.GENERATE_DISPLAYNAME_FROM_TO_STYLE] is false. */ private fun createDisplayName( description: FuzzedMethodDescription, values: List, result: UtExecutionResult? ): String { + val displayNameSeparator = if (UtSettings.useDisplayNameArrowStyle) FROM_TO_NAMES_TRANSITION else FROM_TO_NAMES_COLON + val summaries = values.asSequence() .mapIndexed { index, value -> value.summary?.replace("%var%", description.parameterNameMap(index) ?: "arg_$index") @@ -129,19 +133,32 @@ class ModelBasedNameSuggester( 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" + m is UtPrimitiveModel && m.classId != voidClassId -> "$displayNameSeparator return " + m.value + m is UtNullModel -> "$displayNameSeparator return null" else -> null } } - is UtExplicitlyThrownException, is UtImplicitlyThrownException -> "-> throw ${(result as UtExecutionFailure).exception::class.java.simpleName}" + is UtExplicitlyThrownException, is UtImplicitlyThrownException -> "$displayNameSeparator throw ${(result as UtExecutionFailure).exception::class.java.simpleName}" else -> null } return listOfNotNull(parameters, returnValue).joinToString(separator = " ") } + /** + * Builds the JavaDoc. + */ + private fun createJavaDoc( + description: FuzzedMethodDescription, + values: List, + result: UtExecutionResult? + ): List { + return if (UtSettings.useCustomJavaDocTags) { + CommentWithCustomTagForTestProducedByFuzzerBuilder(description, values, result).buildDocStatements() + } else SimpleCommentForTestProducedByFuzzerBuilder(description, values, result).buildDocStatements() + } + companion object { private fun prettifyNumber(value: T): String? { return when { @@ -165,5 +182,4 @@ class ModelBasedNameSuggester( .filter { it.isUpperCase() } .joinToString(separator = "") } - } \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/TestSuggestedInfo.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/TestSuggestedInfo.kt index e7b47c7ed1..a9ce55d95d 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/TestSuggestedInfo.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/fuzzer/names/TestSuggestedInfo.kt @@ -1,9 +1,12 @@ package org.utbot.summary.fuzzer.names +import org.utbot.framework.plugin.api.DocStatement + /** - * Information that can be used to generate test names. + * Information that can be used to generate test meta-information, including name, display name and JavaDoc. */ class TestSuggestedInfo( - val testName: String, - val displayName: String? = null + val testName: String? = null, + val displayName: String? = null, + val javaDoc: List? = null ) \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/name/NameUtil.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/name/NameUtil.kt index 5370925280..483c3c8a64 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/name/NameUtil.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/name/NameUtil.kt @@ -1,6 +1,9 @@ package org.utbot.summary.name +import org.utbot.framework.plugin.api.ArtificialError import org.utbot.framework.plugin.api.Step +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.framework.plugin.api.util.prettyName import org.utbot.summary.tag.UniquenessTag import soot.SootMethod @@ -23,6 +26,8 @@ data class TestNameDescription( if (this.uniquenessTag == UniquenessTag.Common && other.uniquenessTag == UniquenessTag.Partly) return -1 if (this.uniquenessTag == UniquenessTag.Common && other.uniquenessTag == UniquenessTag.Unique) return -1 + if (this.nameType == NameType.ThrowsException && other.nameType != NameType.ThrowsException) return 1 + if (this.nameType != NameType.ThrowsException && other.nameType == NameType.ThrowsException) return -1 if (this.nameType == NameType.CaughtException && other.nameType != NameType.CaughtException) return 1 if (this.nameType != NameType.CaughtException && other.nameType == NameType.CaughtException) return -1 @@ -44,12 +49,22 @@ data class TestNameDescription( if (this.index > other.index) return 1 if (this.index < other.index) return -1 + return 0 } } enum class NameType { - Condition, Return, Invoke, SwitchCase, CaughtException, NoIteration, ThrowsException, StartIteration + Condition, + Return, + Invoke, + SwitchCase, + CaughtException, + NoIteration, + ThrowsException, + StartIteration, + ArtificialError, + TimeoutError } data class DisplayNameCandidate(val name: String, val uniquenessTag: UniquenessTag, val index: Int) @@ -61,4 +76,23 @@ fun List.returnsToUnique() = this.map { } else { it } +} + +fun buildNameFromThrowable(exception: Throwable): String? { + val exceptionName = exception.prettyName + + if (exceptionName.isNullOrEmpty()) return null + return when (exception) { + is TimeoutException -> "${exception.prettyName}Exceeded" + is ArtificialError -> "Detect${exception.prettyName}" + else -> "Throw$exceptionName" + } +} + +fun getThrowableNameType(exception: Throwable): NameType { + return when (exception) { + is ArtificialError -> NameType.ArtificialError + is TimeoutException -> NameType.TimeoutError + else -> NameType.ThrowsException + } } \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/name/NodeConvertor.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/name/NodeConvertor.kt deleted file mode 100644 index c2033ff369..0000000000 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/name/NodeConvertor.kt +++ /dev/null @@ -1,425 +0,0 @@ -package org.utbot.summary.name - -import com.github.javaparser.ast.Node -import com.github.javaparser.ast.body.VariableDeclarator -import com.github.javaparser.ast.expr.ArrayAccessExpr -import com.github.javaparser.ast.expr.ArrayCreationExpr -import com.github.javaparser.ast.expr.BinaryExpr -import com.github.javaparser.ast.expr.BooleanLiteralExpr -import com.github.javaparser.ast.expr.CastExpr -import com.github.javaparser.ast.expr.CharLiteralExpr -import com.github.javaparser.ast.expr.ClassExpr -import com.github.javaparser.ast.expr.ConditionalExpr -import com.github.javaparser.ast.expr.DoubleLiteralExpr -import com.github.javaparser.ast.expr.EnclosedExpr -import com.github.javaparser.ast.expr.FieldAccessExpr -import com.github.javaparser.ast.expr.InstanceOfExpr -import com.github.javaparser.ast.expr.IntegerLiteralExpr -import com.github.javaparser.ast.expr.LiteralExpr -import com.github.javaparser.ast.expr.LongLiteralExpr -import com.github.javaparser.ast.expr.MethodCallExpr -import com.github.javaparser.ast.expr.NameExpr -import com.github.javaparser.ast.expr.NullLiteralExpr -import com.github.javaparser.ast.expr.StringLiteralExpr -import com.github.javaparser.ast.expr.UnaryExpr -import com.github.javaparser.ast.expr.VariableDeclarationExpr -import com.github.javaparser.ast.stmt.CatchClause -import com.github.javaparser.ast.stmt.ExpressionStmt -import com.github.javaparser.ast.stmt.ForEachStmt -import com.github.javaparser.ast.stmt.ForStmt -import com.github.javaparser.ast.stmt.IfStmt -import com.github.javaparser.ast.stmt.ReturnStmt -import com.github.javaparser.ast.stmt.SwitchEntry -import com.github.javaparser.ast.stmt.SwitchStmt -import com.github.javaparser.ast.stmt.ThrowStmt -import com.github.javaparser.ast.stmt.WhileStmt -import org.utbot.framework.plugin.api.Step -import org.utbot.summary.SummarySentenceConstants.SEMI_COLON_SYMBOL -import org.utbot.summary.ast.JimpleToASTMap -import org.utbot.summary.comment.getTextIterationDescription -import soot.jimple.internal.JIfStmt -import soot.jimple.internal.JLookupSwitchStmt -import soot.jimple.internal.JReturnStmt -import soot.jimple.internal.JTableSwitchStmt - - -private const val STATEMENT_DECISION_TRUE = 1 -private const val STATEMENT_DECISION_FALSE = 0 - -class NodeConvertor { - - companion object { - /** - * Converts ASTNode into String - * @return String that can be a javadoc - */ - fun convertNodeToString(ASTNode: Node, step: Step): String? { - var res = "" - var node = ASTNode - if (node is EnclosedExpr) node = JimpleToASTMap.unEncloseExpr(node) - convertNodeToStringRecursively(node, step)?.let { - res += it - } - if (step.decision == STATEMENT_DECISION_TRUE) { - if (node is BooleanLiteralExpr - || node is NameExpr - || node is MethodCallExpr - || node is CastExpr - || node is FieldAccessExpr - || node is InstanceOfExpr - || node is ArrayAccessExpr - ) res = "Not$res" - else if (node is UnaryExpr && node.operator == UnaryExpr.Operator.LOGICAL_COMPLEMENT) { - res = - res.removePrefix(convertUnaryOperator(UnaryExpr.Operator.LOGICAL_COMPLEMENT)) // double negative expression is positive expression - } - } - return res.ifEmpty { null } - } - - /** - * Used in a conversion of Node into javadoc String - */ - private fun convertNodeToStringRecursively(ASTNode: Node, step: Step): String? { - val res = when (ASTNode) { - is EnclosedExpr -> convertNodeToStringRecursively(JimpleToASTMap.unEncloseExpr(ASTNode), step) - is BinaryExpr -> convertBinaryExpr(ASTNode, step) - is UnaryExpr -> - convertUnaryOperator(ASTNode.operator) + - convertNodeToStringRecursively(ASTNode.expression, step) - is NameExpr -> "${ASTNode.name}" - is LiteralExpr -> convertLiteralExpression(ASTNode) - is ArrayAccessExpr -> "${ASTNode.index.toString().capitalize()}Of${ - ASTNode.name.toString().capitalize() - }" - is FieldAccessExpr -> { - if (ASTNode.scope is FieldAccessExpr) "${ - convertNodeToStringRecursively( - ASTNode.scope, - step - ) - }${ASTNode.name.toString().capitalize()}" - else "${ASTNode.scope.toString().capitalize()}${ASTNode.name.toString().capitalize()}" - } - is CastExpr -> ASTNode.expression.toString().capitalize() - is MethodCallExpr -> { - if (ASTNode.scope.isPresent) "${ - ASTNode.scope.get().toString().capitalize() - }${ASTNode.name.toString().capitalize()}" - else ASTNode.name.toString().capitalize() - } - is InstanceOfExpr -> { - if (step.decision == STATEMENT_DECISION_TRUE) "${ - ASTNode.expression.toString().capitalize() - }NotInstanceOf${ASTNode.type.toString().capitalize()}" - else "${ASTNode.expression.toString().capitalize()}InstanceOf${ - ASTNode.type.toString().capitalize() - }" - } - is ClassExpr -> "${ASTNode.type}Class" - is ArrayCreationExpr -> "NewArrayOf${ASTNode.createdType().toString().capitalize()}" - is ReturnStmt -> { - if (ASTNode.expression.isPresent) convertNodeToStringRecursively( - ASTNode.expression.get(), - step - ) - else "" - } - is ConditionalExpr -> "${convertNodeToStringRecursively(ASTNode.condition, step)}" - is VariableDeclarationExpr -> ASTNode.variables.joinToString(separator = "") { variable -> - convertNodeToStringRecursively( - variable, - step - ) ?: "" - } - - is VariableDeclarator -> { - val initializer = ASTNode.initializer - if (initializer.isPresent) "${ASTNode.type.toString().capitalize()}${ - ASTNode.name.toString().capitalize() - }InitializedBy${convertNodeToStringRecursively(ASTNode.initializer.get(), step)}" - else "${ASTNode.type.toString().capitalize()}${ASTNode.name.toString().capitalize()}IsInitialized" - } - is CatchClause -> ASTNode.parameter.type.toString() - .capitalize() //add ${ASTNode.parameter.name.toString().capitalize() to print variable name - - is WhileStmt -> convertNodeToStringRecursively(ASTNode.condition, step) - is IfStmt -> convertNodeToStringRecursively(ASTNode.condition, step) - is ThrowStmt -> "Throws${ASTNode.expression.toString().removePrefix("new").capitalize()}" - is SwitchStmt -> convertSwitch(ASTNode, step) - is ExpressionStmt -> convertNodeToStringRecursively(ASTNode.expression, step) - - else -> { - null - } - } ?: return null - return postProcessName(res) - } - - /** - * Converts ASTNode into String - * @return String that can be a DisplayName - */ - fun convertNodeToDisplayNameString(ASTNode: Node, step: Step): String { - var node = ASTNode - if (node is ExpressionStmt) node = node.expression - if (node is EnclosedExpr) node = JimpleToASTMap.unEncloseExpr(node) - var res = convertNodeToDisplayNameStringRecursively(node, step) - if (node is ReturnStmt) node = node.expression.orElse(null) ?: node - if (step.stmt is JReturnStmt) return res - if (nodeContainsBooleanCondition(node)) { - res += if (step.decision == STATEMENT_DECISION_TRUE) { - " : False" - } else { - " : True" - } - } - return res - } - - /** - * Checks if node contain any boolean condition - */ - private fun nodeContainsBooleanCondition(node: Node): Boolean { - if (node is ArrayAccessExpr - || node is FieldAccessExpr - || node is CastExpr - || node is NameExpr - || node is BinaryExpr - || node is MethodCallExpr - || node is InstanceOfExpr - || node is UnaryExpr - || node is BooleanLiteralExpr - || node is VariableDeclarationExpr && node.variables.any { nodeContainsBooleanCondition(it) } - ) return true - if (node is VariableDeclarator) { - val initializer = node.initializer.orElse(null) - if (initializer != null) { - return nodeContainsBooleanCondition(initializer) - } - } - return false - } - - /** - * Used in a conversion of Node into DisplayName String - */ - private fun convertNodeToDisplayNameStringRecursively(ASTNode: Node, step: Step): String { - val res = when (ASTNode) { - is EnclosedExpr -> convertNodeToDisplayNameStringRecursively( - JimpleToASTMap.unEncloseExpr(ASTNode), - step - ) - is WhileStmt -> getTextIterationDescription(ASTNode) - is ForStmt -> getTextIterationDescription(ASTNode) - is ForEachStmt -> getTextIterationDescription(ASTNode) - is IfStmt -> convertNodeToDisplayNameStringRecursively(ASTNode.condition, step) - is ThrowStmt -> "Throws ${ASTNode.expression.toString().removePrefix("new").capitalize()}" - is SwitchStmt -> convertSwitch(ASTNode, step, removeSpaces = false) - is ExpressionStmt -> convertNodeToDisplayNameStringRecursively(ASTNode.expression, step) - is VariableDeclarationExpr -> ASTNode.variables.joinToString(separator = " ") { variable -> - convertNodeToDisplayNameStringRecursively( - variable, - step - ) - } - else -> { - ASTNode.toString() - } - } - return displayNamePostprocessor(res) - } - - /** - * Replaces one+ whitespaces with one whitespace - */ - private fun displayNamePostprocessor(displayName: String) = - displayName.replace("\\s+".toRegex(), " ").replace("$SEMI_COLON_SYMBOL", "") - - private fun convertBinaryExpr(binaryExpr: BinaryExpr, step: Step): String { - var res = "" - val left = convertNodeToStringRecursively(binaryExpr.left, step) - if (left != null) res += left.capitalize() - val stmt = step.stmt - res += if (stmt is JIfStmt) { - convertBinaryOperator( - binaryExpr.operator, - JimpleToASTMap.isOperatorReversed(stmt, binaryExpr), - step.decision - ).capitalize() - } else { - convertBinaryOperator(binaryExpr.operator, false, step.decision).capitalize() - } - val right = convertNodeToStringRecursively(binaryExpr.right, step) - if (right != null) res += right.capitalize() - return res - } - - /** - * Converts switchNode into String - */ - private fun convertSwitch(switchNode: SwitchStmt, step: Step, removeSpaces: Boolean = true): String { - val stmt = step.stmt - var res = "" - if (stmt is JLookupSwitchStmt) { - val lookup = stmt.lookupValues - val decision = step.decision - val case = ( - if (decision >= lookup.size) { - null - } else { - lookup[step.decision] - } - )?.value - res += JimpleToASTMap.getSwitchCaseLabel(switchNode, case).capitalize() - } - if (stmt is JTableSwitchStmt) { - val switchCase = JimpleToASTMap.mapSwitchCase(switchNode, step.decision) - if (switchCase is SwitchEntry) { - val case = switchCase.labels.first - res += if (case.isPresent) { - case.get().toString().capitalize() - } else { - "Default" - } - } - } - res = if (removeSpaces) { - "Switch${switchNode.selector.toString().capitalize()}Case" + res.replace(" ", "") - } else { - "switch(${switchNode.selector}) case: " + res - } - return res - } - - /** - * Converts literal into String - */ - private fun convertLiteralExpression(literal: LiteralExpr): String { - val res = when (literal) { - is StringLiteralExpr -> literal.asString() - is CharLiteralExpr -> if (isLegitSymbolForFunctionName(literal.asChar())) "${literal.asChar()}" else "Char" - is DoubleLiteralExpr -> { - when (val literalAsDouble = literal.asDouble()) { - Double.NaN -> "NaN" - Double.MIN_VALUE -> "MinValue" - Double.MAX_VALUE -> "MaxValue" - Double.NEGATIVE_INFINITY -> "NegativeInfinity" - Double.POSITIVE_INFINITY -> "Infinity" - else -> { - when { - literalAsDouble != literalAsDouble -> "NaN" - literalAsDouble < 0 -> "Negative${literal.asDouble().toInt()}d" - literalAsDouble == 0.0 -> "Zero" - literalAsDouble == -0.0 -> "Zero" - else -> "${ - literal.asDouble().toInt() - }d" //seems kinda wrong, . can be replaced with "dot" or something else - } - } - } - } - is IntegerLiteralExpr -> { - var str = "" - if (literal.asNumber().toInt() < 0) str += "Negative" - str += if (literal.asNumber().toInt() == 0) "Zero" else "$literal" - str - } - is LongLiteralExpr -> { - var str = "" - if (literal.asNumber().toLong() < 0) str += "Negative" - str += if (literal.asNumber().toInt() == 0) "Zero" else "$literal" - str - } - is BooleanLiteralExpr -> literal.toString() - is NullLiteralExpr -> "Null" - else -> "" - } - return res.capitalize() - } - - /** - * Checks if symbol can be used in function name - * @see Java documentation: Identifiers - */ - private fun isLegitSymbolForFunctionName(ch: Char): Boolean { - return (ch in '0'..'9' - || ch in 'a'..'z' - || ch in 'A'..'Z' - || ch == '_' - || ch == '$' - ) - } - - /** - * Capitalizes method name. - * - * It splits the text by delimiters, capitalizes each part, removes special characters and concatenates result. - */ - private fun postProcessName(name: String) = - name.split(".", "(", ")", ",") - .joinToString("") { it -> it.capitalize().filter { isLegitSymbolForFunctionName(it) } } - - /** - * Converts Javaparser BinaryOperator and all of its children into a String - */ - private fun convertBinaryOperator( - binaryOperator: BinaryExpr.Operator, - isOperatorReversed: Boolean, - decision: Int - ): String { - var operator = binaryOperator - if ((isOperatorReversed && decision == STATEMENT_DECISION_TRUE) || (!isOperatorReversed && decision == STATEMENT_DECISION_FALSE)) { - operator = reverseBinaryOperator(operator) ?: binaryOperator - } - return when (operator) { - BinaryExpr.Operator.OR -> "Or" - BinaryExpr.Operator.AND -> "And" - BinaryExpr.Operator.BINARY_OR -> "BitwiseOr" - BinaryExpr.Operator.BINARY_AND -> "BitwiseAnd" - BinaryExpr.Operator.XOR -> "Xor" - BinaryExpr.Operator.EQUALS -> "Equals" - BinaryExpr.Operator.NOT_EQUALS -> "NotEquals" - BinaryExpr.Operator.LESS -> "LessThan" - BinaryExpr.Operator.GREATER -> "GreaterThan" - BinaryExpr.Operator.LESS_EQUALS -> "LessOrEqual" - BinaryExpr.Operator.GREATER_EQUALS -> "GreaterOrEqual" - BinaryExpr.Operator.LEFT_SHIFT -> "LeftShift" - BinaryExpr.Operator.SIGNED_RIGHT_SHIFT -> "RightShift" - BinaryExpr.Operator.UNSIGNED_RIGHT_SHIFT -> "UnsignedRightShift" - BinaryExpr.Operator.PLUS -> "Plus" - BinaryExpr.Operator.MINUS -> "Minus" - BinaryExpr.Operator.MULTIPLY -> "Multiply" - BinaryExpr.Operator.DIVIDE -> "Divide" - BinaryExpr.Operator.REMAINDER -> "RemainderOf" //does it sounds strange? or is it ok - } - } - - /** - * Reverts Javaparser binary operator if possible - */ - private fun reverseBinaryOperator(operator: BinaryExpr.Operator) = when (operator) { - BinaryExpr.Operator.EQUALS -> BinaryExpr.Operator.NOT_EQUALS - BinaryExpr.Operator.NOT_EQUALS -> BinaryExpr.Operator.EQUALS - BinaryExpr.Operator.LESS -> BinaryExpr.Operator.GREATER_EQUALS - BinaryExpr.Operator.GREATER -> BinaryExpr.Operator.LESS_EQUALS - BinaryExpr.Operator.LESS_EQUALS -> BinaryExpr.Operator.GREATER - BinaryExpr.Operator.GREATER_EQUALS -> BinaryExpr.Operator.LESS - else -> null - } - - /** - * Converts Javaparser unary operator to String - */ - private fun convertUnaryOperator(unaryOperator: UnaryExpr.Operator) = when (unaryOperator) { - UnaryExpr.Operator.PLUS -> "Plus" - UnaryExpr.Operator.MINUS -> "Negative" - UnaryExpr.Operator.PREFIX_INCREMENT -> "PrefixIncrement" - UnaryExpr.Operator.PREFIX_DECREMENT -> "PrefixDecrement" - UnaryExpr.Operator.LOGICAL_COMPLEMENT -> "Not" //! or LogicalComplement - UnaryExpr.Operator.BITWISE_COMPLEMENT -> "BitwiseComplement" - UnaryExpr.Operator.POSTFIX_INCREMENT -> "PostfixIncrement" - UnaryExpr.Operator.POSTFIX_DECREMENT -> "PostfixDecrement" - } - } -} \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/name/SimpleNameBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/name/SimpleNameBuilder.kt index 2f0b2e6c99..4c51cb46a6 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/name/SimpleNameBuilder.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/name/SimpleNameBuilder.kt @@ -1,466 +1,475 @@ -package org.utbot.summary.name - -import com.github.javaparser.ast.stmt.CatchClause -import com.github.javaparser.ast.stmt.ForStmt -import com.github.javaparser.ast.stmt.ThrowStmt -import org.utbot.framework.plugin.api.ConcreteExecutionFailureException -import org.utbot.framework.plugin.api.Step -import org.utbot.framework.plugin.api.exceptionOrNull -import org.utbot.framework.plugin.api.isFailure -import org.utbot.summary.AbstractTextBuilder -import org.utbot.summary.SummarySentenceConstants.FROM_TO_NAMES_TRANSITION -import org.utbot.summary.ast.JimpleToASTMap -import org.utbot.summary.comment.getExceptionReason -import org.utbot.summary.comment.getTextTypeIterationDescription -import org.utbot.summary.comment.shouldSkipInvoke -import org.utbot.summary.name.NodeConvertor.Companion.convertNodeToDisplayNameString -import org.utbot.summary.tag.BasicTypeTag -import org.utbot.summary.tag.CallOrderTag -import org.utbot.summary.tag.StatementTag -import org.utbot.summary.tag.TraceTagWithoutExecution -import org.utbot.summary.tag.UniquenessTag -import soot.SootMethod -import soot.jimple.internal.JAssignStmt -import soot.jimple.internal.JInvokeStmt -import soot.jimple.internal.JVirtualInvokeExpr - -class SimpleNameBuilder( - traceTag: TraceTagWithoutExecution, - sootToAST: MutableMap, - val methodUnderTest: SootMethod -) : - AbstractTextBuilder(traceTag, sootToAST) { - - private var testNameDescription: TestNameDescription? = null - private val testNames: MutableList = collectCandidateNames() - val fromToName = fromToName() - val name = buildMethodName() - val displayName = buildDisplayName() - - - private fun collectCandidateNames(): MutableList { - val testNames = mutableListOf() - collectTags(traceTag.rootStatementTag, testNames, methodUnderTest) - exceptionThrow(testNames) - return testNames - } - - /** - * Collects Tags and chooses node that will be used in name of the test case - * @return name of the test case - */ - private fun buildMethodName(): String { - val methodName = methodUnderTest.name.capitalize() - testNames.sortDescending() - testNameDescription = testNames.firstOrNull() - val subName = testNameDescription?.name - return if (subName != null) { - "test${methodName}_$subName" - } else { - "test$methodName" - } - } - - /** - * Should be run after build function, else testNameDescription is null and displayName will be empty - * - * @return string with the node that is used to create name of the function - * Such description can use special symbols and spaces - */ - private fun buildDisplayName(): String { - val nameDescription = testNameDescription - val sootMethod = testNameDescription?.method - val jimpleToASTMap = sootMethod?.let { sootToAST[it] } - var res = "" - if (nameDescription != null && jimpleToASTMap != null) { - val index = nameDescription.index - val step = traceTag.path[index] - val astNode = jimpleToASTMap[step.stmt] - - if (astNode != null) { - if (traceTag.result.isFailure) { - res += "Throw ${traceTag.result.exceptionOrNull()?.let { it::class.simpleName }}" - res += exceptionPlace(jimpleToASTMap) - } else if (index > 0) { - return convertNodeToDisplayNameString(astNode, step) - } - } - } - return res - } - - private fun exceptionPlace( - jimpleToASTMap: JimpleToASTMap, - placeAfter: String = " after condition: ", - placeIn: String = " in: " - ): String { - if (traceTag.path.isEmpty()) return "" - - if (traceTag.result.isFailure) { - val lastStep = traceTag.path.last() - val lastNode = jimpleToASTMap[lastStep.stmt] - if (lastNode is ThrowStmt) { - val exceptionReason = getExceptionReason(lastNode) ?: return "" - return placeAfter + convertNodeToDisplayNameString(exceptionReason, lastStep) - } else if (lastNode != null) { - return placeIn + convertNodeToDisplayNameString(lastNode, lastStep) - } - } - return "" - } - - private fun fromToName(): String { - val jimpleToASTMap = sootToAST[methodUnderTest] - val maxDepth = testNames.maxOfOrNull { it.depth } ?: 0 - - val candidateNames = testNames.returnsToUnique().filter { it.depth == maxDepth } - .filter { - it.nameType != NameType.StartIteration && it.nameType != NameType.NoIteration - }.mapNotNull { nameDescription -> - fromNameDescriptionToCandidateSimpleName(nameDescription) - }.toMutableList() - - if (traceTag.result.isFailure && jimpleToASTMap != null) { - val throwPlace = exceptionPlace(jimpleToASTMap, placeAfter = "", placeIn = "") - candidateNames.add( - 0, - DisplayNameCandidate( - throwPlace, - UniquenessTag.Unique, - traceTag.path.size - ) - ) - } - - val chosenNames = choosePairFromToNames(candidateNames) - if (chosenNames != null) { - val firstName = chosenNames.first - val secondName = chosenNames.second - if (firstName != null) { - return "$firstName $FROM_TO_NAMES_TRANSITION $secondName" - } else { - return "$FROM_TO_NAMES_TRANSITION $secondName" - } - } - return "" - } - - private fun fromNameDescriptionToCandidateSimpleName(nameDescription: TestNameDescription): DisplayNameCandidate? { - if (nameDescription.nameType == NameType.ThrowsException) { - return DisplayNameCandidate( - nameDescription.name, - nameDescription.uniquenessTag, - traceTag.path.size + 1 - ) - } - if (nameDescription.nameType == NameType.Invoke) { - return DisplayNameCandidate( - nameDescription.name, - nameDescription.uniquenessTag, - nameDescription.index - ) - } - val node = sootToAST[nameDescription.method]?.get(nameDescription.step.stmt) - if (node is CatchClause) { - return DisplayNameCandidate( - "Catch (${node.parameter})", - nameDescription.uniquenessTag, - nameDescription.index - ) - } - if (node != null) { - val name = convertNodeToDisplayNameString(node, nameDescription.step) - return DisplayNameCandidate( - name, - nameDescription.uniquenessTag, - nameDescription.index - ) - } else { - return null - } - } - - /* - * First, the method tries to find two unique tags which - * can represented as From unique tag -> To unique tag. - * Example: Unique condition -> Unique return. - * If the method didn't find two different and unique tags then - * it will try to build names in the following priority: - * 2. Partly Unique Tag -> Unique Tag - * 3. Partly Unique Tag -> Partly Unique Tag - * 4. Unique Tag -> Partly Unique Tag - * 4. Unique Tag -> Any Tag - * 5. Any Tag -> Unique Tag - * 6. Any Tag -> Partly Unique Tag - * 7. -> Unique Tag - * 8. -> Partly Unique Tag - * 9. -> Any last Tag - * otherwise, returns null - */ - private fun choosePairFromToNames(candidates: List): Pair? { - - val fromNameUnique = candidates.firstOrNull { it.uniquenessTag == UniquenessTag.Unique } - val toNameUnique = candidates.lastOrNull { it.uniquenessTag == UniquenessTag.Unique } - // from unique tag -> to unique tag - buildCandidate(fromNameUnique, toNameUnique, null)?.let { - return it - } - val fromNamePartly = candidates.firstOrNull { it.uniquenessTag == UniquenessTag.Partly } - val toNamePartly = candidates.lastOrNull { it.uniquenessTag == UniquenessTag.Partly } - // from partly unique tag -> to unique tag - // from partly unique tag -> to partly unique tag - buildCandidate(fromNamePartly, toNameUnique, toNamePartly)?.let { - return it - } - val toNameAny = candidates.lastOrNull() - // from unique tag -> to partly unique - // from unique tag -> to any - buildCandidate(fromNameUnique, toNamePartly, toNameAny)?.let { - return it - } - val fromNameAny = candidates.firstOrNull() - // from any tag -> to unique tag - // from any tag -> to partly unique tag - buildCandidate(fromNameAny, toNameUnique, toNamePartly)?.let { - return it - } - - if (toNameUnique != null) { - return Pair(null, toNameUnique.name) - } - if (toNamePartly != null) { - return Pair(null, toNamePartly.name) - } - if (toNameAny != null) { - return Pair(null, toNameAny.name) - } - return null - } - - /** - * The method tries to build a pair name with an attempt order: - * 1. from candidate name -> to candidate name 1 - * 2. from candidate name -> to candidate name 2 - * otherwise, returns null - */ - fun buildCandidate( - fromCandidateName: DisplayNameCandidate?, - toCandidateName1: DisplayNameCandidate?, - toCandidateName2: DisplayNameCandidate? - ): Pair? { - if (fromCandidateName != null && toCandidateName1 != null - && fromCandidateName.name != toCandidateName1.name - && fromCandidateName.index < toCandidateName1.index - ) { - return Pair(fromCandidateName.name, toCandidateName1.name) - } - if (fromCandidateName != null && toCandidateName2 != null - && fromCandidateName.name != toCandidateName2.name - && fromCandidateName.index < toCandidateName2.index - ) { - return Pair(fromCandidateName.name, toCandidateName2.name) - } - return null - } - - /** - * [TraceTagWithoutExecution.path] could be empty in case exception is thrown not in source code but in engine - * (for example, [ConcreteExecutionFailureException]). - */ - private fun exceptionThrow(testNames: MutableList) { - val throwsException = traceTag.result.exceptionOrNull()?.let { it::class.simpleName } - if (!(throwsException.isNullOrEmpty() || traceTag.path.isEmpty())) { - testNames.add(TestNameDescription( - "Throw$throwsException", - testNames.maxOfOrNull { it.depth } ?: 0, - testNames.maxOfOrNull { it.line } ?: 0, - UniquenessTag.Unique, - NameType.ThrowsException, - testNames.maxOfOrNull { it.index } ?: 0, - traceTag.path.last(), - methodUnderTest - )) - } - } - - private fun collectTags( - statementTag: StatementTag?, - testNames: MutableList, - currentMethod: SootMethod - ) { - val jimpleToASTMap = sootToAST[currentMethod] - if (statementTag == null) return - if (jimpleToASTMap == null) return - val recursion = statementTag.recursion - val stmt = statementTag.step.stmt - val depth = statementTag.step.depth - val line = statementTag.line - val invoke = statementTag.invoke - - val localNoIterations = statementTagSkippedIteration(statementTag, currentMethod) - if (localNoIterations.isNotEmpty()) { - localNoIterations.forEach { - testNames.add( - TestNameDescription( - "NoIteration${it.typeDescription}", - depth, - it.from, - UniquenessTag.Unique, - NameType.NoIteration, - statementTag.index, - statementTag.step, - currentMethod - ) - ) - methodToNoIterationDescription[currentMethod]?.remove(it) - } - } - - val invokeSootMethod = statementTag.invokeSootMethod() - if (invoke != null && invokeSootMethod != null) { - val beforeInvokeNumberNames = testNames.size - collectTags(invoke, testNames, invokeSootMethod) - if (testNames.size != beforeInvokeNumberNames) { - testNames.add( - beforeInvokeNumberNames, - TestNameDescription( - invokeSootMethod.name, - depth + 1, - 0, - UniquenessTag.Common, - NameType.Invoke, - statementTag.index, - statementTag.step, - invokeSootMethod - ) - ) - } - } - - if (jimpleToASTMap[statementTag.step.stmt] !is ForStmt) { - val nodeAST = jimpleToASTMap[stmt] - if (nodeAST != null) { - if (statementTag.basicTypeTag == BasicTypeTag.Condition && statementTag.callOrderTag == CallOrderTag.First) { - var conditionName: String? = null - if (statementTag.uniquenessTag == UniquenessTag.Unique - || (statementTag.uniquenessTag == UniquenessTag.Partly && statementTag.executionFrequency == 1) - ) { - conditionName = NodeConvertor.convertNodeToString(nodeAST, statementTag.step) - } - conditionName?.let { - testNames.add( - TestNameDescription( - it, - depth, - line, - statementTag.uniquenessTag, - NameType.Condition, - statementTag.index, - statementTag.step, - currentMethod - ) - ) - } - } - - - var prefix = "" - var name = NodeConvertor.convertNodeToString(nodeAST, statementTag.step) - var type: NameType? = null - - if (statementTag.basicTypeTag == BasicTypeTag.SwitchCase && statementTag.uniquenessTag == UniquenessTag.Unique) { - type = NameType.SwitchCase - } else if (statementTag.basicTypeTag == BasicTypeTag.CaughtException) { - type = NameType.CaughtException - prefix = "Catch" - } else if (statementTag.basicTypeTag == BasicTypeTag.Return) { - type = NameType.Return - prefix = "Return" - name = name ?: "" - } else if (statementTag.basicTypeTag == BasicTypeTag.Invoke && statementTag.uniquenessTag == UniquenessTag.Unique) { - val methodName = stmt.invokeExpr.method.name.capitalize() - if (!shouldSkipInvoke(methodName)) { - if (stmt is JAssignStmt || stmt is JInvokeStmt) { - type = NameType.Invoke - prefix += stmt.invokeExpr.methodRef.declaringClass.javaStyleName.substringBefore('$') //class name - prefix += methodName - name = - "" //todo change var name to val name, everything should be mapped through .convertNodeToString - } - } - } - - if (type != null) { - testNames.add( - TestNameDescription( - prefix + name, - depth, - line, - statementTag.uniquenessTag, - type, - statementTag.index, - statementTag.step, - currentMethod - ) - ) - } - } - } - - if (statementTag.iterations.isNotEmpty()) { - val firstNode = jimpleToASTMap[statementTag.iterations.first().step.stmt] - - val iterationDescription = if (firstNode != null) getTextTypeIterationDescription(firstNode) else null - - if (iterationDescription != null && iterationDescription.isNotEmpty()) { - testNames.add( - TestNameDescription( - "Iterate$iterationDescription", - depth, - line, - UniquenessTag.Partly, - NameType.StartIteration, - statementTag.index, - statementTag.step, - currentMethod - ) - ) - } - - statementTag.iterations.forEach { - collectTags(it, testNames, currentMethod) - } - } - - if (recursion != null) { - val name = when (stmt) { - is JAssignStmt -> "Recursion" + (stmt.rightOp as JVirtualInvokeExpr).method.name //todo through .convertNodeToString - is JInvokeStmt -> "Recursion" + stmt.invokeExpr.method.name //todo through .convertNodeToString - else -> "" - } - if (name.isNotEmpty()) { - testNames.add( - TestNameDescription( - name, - depth, - line, - statementTag.uniquenessTag, - NameType.Invoke, - statementTag.index, - statementTag.step, - currentMethod - ) - ) - collectTags(recursion, testNames, currentMethod) - } - } - collectTags(statementTag.next, testNames, currentMethod) - } - - override fun conditionStep(step: Step, reversed: Boolean, jimpleToASTMap: JimpleToASTMap): String { - val nodeAST = jimpleToASTMap[step.stmt] - return if (nodeAST != null) { - NodeConvertor.convertNodeToString(nodeAST, step) ?: "" - } else "" - } +package org.utbot.summary.name + +import com.github.javaparser.ast.stmt.CatchClause +import com.github.javaparser.ast.stmt.ForStmt +import com.github.javaparser.ast.stmt.ThrowStmt +import org.utbot.framework.plugin.api.InstrumentedProcessDeathException +import org.utbot.framework.plugin.api.Step +import org.utbot.framework.plugin.api.exceptionOrNull +import org.utbot.framework.plugin.api.isFailure +import org.utbot.summary.AbstractTextBuilder +import org.utbot.summary.NodeConverter +import org.utbot.summary.SummarySentenceConstants.FROM_TO_NAMES_TRANSITION +import org.utbot.summary.ast.JimpleToASTMap +import org.utbot.summary.comment.getExceptionReasonForName +import org.utbot.summary.comment.getTextTypeIterationDescription +import org.utbot.summary.comment.shouldSkipInvoke +import org.utbot.summary.NodeConverter.Companion.convertNodeToDisplayNameString +import org.utbot.summary.tag.BasicTypeTag +import org.utbot.summary.tag.CallOrderTag +import org.utbot.summary.tag.StatementTag +import org.utbot.summary.tag.TraceTagWithoutExecution +import org.utbot.summary.tag.UniquenessTag +import soot.SootMethod +import soot.jimple.internal.JAssignStmt +import soot.jimple.internal.JInvokeStmt +import soot.jimple.internal.JVirtualInvokeExpr + +class SimpleNameBuilder( + traceTag: TraceTagWithoutExecution, + sootToAST: MutableMap, + val methodUnderTest: SootMethod +) : + AbstractTextBuilder(traceTag, sootToAST) { + + private var testNameDescription: TestNameDescription? = null + private val testNames: MutableList = collectCandidateNames() + val fromToName = fromToName() + val name = buildMethodName() + val displayName = buildDisplayName() + + + private fun collectCandidateNames(): MutableList { + val testNames = mutableListOf() + collectTags(traceTag.rootStatementTag, testNames, methodUnderTest) + exceptionThrow(testNames) + return testNames + } + + /** + * Collects Tags and chooses node that will be used in name of the test case + * @return name of the test case + */ + private fun buildMethodName(): String { + val methodName = methodUnderTest.name.capitalize() + testNames.sortDescending() + testNameDescription = testNames.firstOrNull() + val subName = testNameDescription?.name + return if (subName != null) { + "test${methodName}_$subName" + } else { + "test$methodName" + } + } + + /** + * Should be run after build function, else testNameDescription is null and displayName will be empty + * + * @return string with the node that is used to create name of the function + * Such description can use special symbols and spaces + */ + private fun buildDisplayName(): String { + val nameDescription = testNameDescription + val sootMethod = testNameDescription?.method + val jimpleToASTMap = sootMethod?.let { sootToAST[it] } + var res = "" + if (nameDescription != null && jimpleToASTMap != null) { + val index = nameDescription.index + val step = traceTag.path[index] + val astNode = jimpleToASTMap[step.stmt] + + if (astNode != null) { + if (traceTag.result.isFailure) { + res += "Throw ${traceTag.result.exceptionOrNull()?.let { it::class.simpleName }}" + res += exceptionPlace(jimpleToASTMap) + } else if (index > 0) { + return convertNodeToDisplayNameString(astNode, step) + } + } + } + return res + } + + private fun exceptionPlace( + jimpleToASTMap: JimpleToASTMap, + placeAfter: String = " when: ", + placeIn: String = " in: " + ): String { + if (traceTag.path.isEmpty()) return "" + + if (traceTag.result.isFailure) { + val lastStep = traceTag.path.last() + val lastNode = jimpleToASTMap[lastStep.stmt] + if (lastNode is ThrowStmt) { + val exceptionReason = getExceptionReasonForName(lastNode) ?: return "" + return placeAfter + convertNodeToDisplayNameString(exceptionReason, lastStep) + } else if (lastNode != null) { + return placeIn + convertNodeToDisplayNameString(lastNode, lastStep) + } + } + return "" + } + + private fun fromToName(): String { + val jimpleToASTMap = sootToAST[methodUnderTest] + val maxDepth = testNames.maxOfOrNull { it.depth } ?: 0 + + val candidateNames = testNames.returnsToUnique().filter { it.depth == maxDepth } + .filter { + it.nameType != NameType.StartIteration && it.nameType != NameType.NoIteration + }.mapNotNull { nameDescription -> + fromNameDescriptionToCandidateSimpleName(nameDescription) + }.toMutableList() + + if (traceTag.result.isFailure && jimpleToASTMap != null) { + val throwPlace = exceptionPlace(jimpleToASTMap, placeAfter = "", placeIn = "") + candidateNames.add( + 0, + DisplayNameCandidate( + throwPlace, + UniquenessTag.Unique, + traceTag.path.size + ) + ) + } + + val chosenNames = choosePairFromToNames(candidateNames) + if (chosenNames != null) { + val firstName = chosenNames.first + val secondName = chosenNames.second + if (firstName != null) { + return "$firstName $FROM_TO_NAMES_TRANSITION $secondName" + } else { + return "$FROM_TO_NAMES_TRANSITION $secondName" + } + } + return "" + } + + private fun fromNameDescriptionToCandidateSimpleName(nameDescription: TestNameDescription): DisplayNameCandidate? { + if (nameDescription.nameType == NameType.ArtificialError || + nameDescription.nameType == NameType.TimeoutError || + nameDescription.nameType == NameType.ThrowsException + ) { + return DisplayNameCandidate( + nameDescription.name, + nameDescription.uniquenessTag, + traceTag.path.size + 1 + ) + } + if (nameDescription.nameType == NameType.Invoke) { + return DisplayNameCandidate( + nameDescription.name, + nameDescription.uniquenessTag, + nameDescription.index + ) + } + val node = sootToAST[nameDescription.method]?.get(nameDescription.step.stmt) + if (node is CatchClause) { + return DisplayNameCandidate( + "Catch (${node.parameter})", + nameDescription.uniquenessTag, + nameDescription.index + ) + } + if (node != null) { + val name = convertNodeToDisplayNameString(node, nameDescription.step) + return DisplayNameCandidate( + name, + nameDescription.uniquenessTag, + nameDescription.index + ) + } else { + return null + } + } + + /* + * First, the method tries to find two unique tags which + * can represented as From unique tag -> To unique tag. + * Example: Unique condition -> Unique return. + * If the method didn't find two different and unique tags then + * it will try to build names in the following priority: + * 2. Partly Unique Tag -> Unique Tag + * 3. Partly Unique Tag -> Partly Unique Tag + * 4. Unique Tag -> Partly Unique Tag + * 4. Unique Tag -> Any Tag + * 5. Any Tag -> Unique Tag + * 6. Any Tag -> Partly Unique Tag + * 7. -> Unique Tag + * 8. -> Partly Unique Tag + * 9. -> Any last Tag + * otherwise, returns null + */ + private fun choosePairFromToNames(candidates: List): Pair? { + + val fromNameUnique = candidates.firstOrNull { it.uniquenessTag == UniquenessTag.Unique } + val toNameUnique = candidates.lastOrNull { it.uniquenessTag == UniquenessTag.Unique } + // from unique tag -> to unique tag + buildCandidate(fromNameUnique, toNameUnique, null)?.let { + return it + } + val fromNamePartly = candidates.firstOrNull { it.uniquenessTag == UniquenessTag.Partly } + val toNamePartly = candidates.lastOrNull { it.uniquenessTag == UniquenessTag.Partly } + // from partly unique tag -> to unique tag + // from partly unique tag -> to partly unique tag + buildCandidate(fromNamePartly, toNameUnique, toNamePartly)?.let { + return it + } + val toNameAny = candidates.lastOrNull() + // from unique tag -> to partly unique + // from unique tag -> to any + buildCandidate(fromNameUnique, toNamePartly, toNameAny)?.let { + return it + } + val fromNameAny = candidates.firstOrNull() + // from any tag -> to unique tag + // from any tag -> to partly unique tag + buildCandidate(fromNameAny, toNameUnique, toNamePartly)?.let { + return it + } + + if (toNameUnique != null) { + return Pair(null, toNameUnique.name) + } + if (toNamePartly != null) { + return Pair(null, toNamePartly.name) + } + if (toNameAny != null) { + return Pair(null, toNameAny.name) + } + return null + } + + /** + * The method tries to build a pair name with an attempt order: + * 1. from candidate name -> to candidate name 1 + * 2. from candidate name -> to candidate name 2 + * otherwise, returns null + */ + fun buildCandidate( + fromCandidateName: DisplayNameCandidate?, + toCandidateName1: DisplayNameCandidate?, + toCandidateName2: DisplayNameCandidate? + ): Pair? { + if (fromCandidateName != null && toCandidateName1 != null + && fromCandidateName.name != toCandidateName1.name + && fromCandidateName.index < toCandidateName1.index + ) { + return Pair(fromCandidateName.name, toCandidateName1.name) + } + if (fromCandidateName != null && toCandidateName2 != null + && fromCandidateName.name != toCandidateName2.name + && fromCandidateName.index < toCandidateName2.index + ) { + return Pair(fromCandidateName.name, toCandidateName2.name) + } + return null + } + + /** + * [TraceTagWithoutExecution.path] could be empty in case exception is thrown not in source code but in engine + * (for example, [InstrumentedProcessDeathException]). + */ + private fun exceptionThrow(testNames: MutableList) { + val exception = traceTag.result.exceptionOrNull() ?: return + val name = buildNameFromThrowable(exception) + + if (name != null && traceTag.path.isNotEmpty()) { + val nameType = getThrowableNameType(exception) + + testNames.add(TestNameDescription( + name, + testNames.maxOfOrNull { it.depth } ?: 0, + testNames.maxOfOrNull { it.line } ?: 0, + UniquenessTag.Unique, + nameType, + testNames.maxOfOrNull { it.index } ?: 0, + traceTag.path.last(), + methodUnderTest + )) + } + } + + private fun collectTags( + statementTag: StatementTag?, + testNames: MutableList, + currentMethod: SootMethod + ) { + val jimpleToASTMap = sootToAST[currentMethod] + if (statementTag == null) return + if (jimpleToASTMap == null) return + val recursion = statementTag.recursion + val stmt = statementTag.step.stmt + val depth = statementTag.step.depth + val line = statementTag.line + val invoke = statementTag.invoke + + val localNoIterations = statementTagSkippedIteration(statementTag, currentMethod) + if (localNoIterations.isNotEmpty()) { + localNoIterations.forEach { + testNames.add( + TestNameDescription( + "NoIteration${it.typeDescription}", + depth, + it.from, + UniquenessTag.Unique, + NameType.NoIteration, + statementTag.index, + statementTag.step, + currentMethod + ) + ) + methodToNoIterationDescription[currentMethod]?.remove(it) + } + } + + val invokeSootMethod = statementTag.invokeSootMethod() + if (invoke != null && invokeSootMethod != null) { + val beforeInvokeNumberNames = testNames.size + collectTags(invoke, testNames, invokeSootMethod) + if (testNames.size != beforeInvokeNumberNames) { + testNames.add( + beforeInvokeNumberNames, + TestNameDescription( + invokeSootMethod.name, + depth + 1, + 0, + UniquenessTag.Common, + NameType.Invoke, + statementTag.index, + statementTag.step, + invokeSootMethod + ) + ) + } + } + + if (jimpleToASTMap[statementTag.step.stmt] !is ForStmt) { + val nodeAST = jimpleToASTMap[stmt] + if (nodeAST != null) { + if (statementTag.basicTypeTag == BasicTypeTag.Condition && statementTag.callOrderTag == CallOrderTag.First) { + var conditionName: String? = null + if (statementTag.uniquenessTag == UniquenessTag.Unique + || (statementTag.uniquenessTag == UniquenessTag.Partly && statementTag.executionFrequency == 1) + ) { + conditionName = NodeConverter.convertNodeToString(nodeAST, statementTag.step) + } + conditionName?.let { + testNames.add( + TestNameDescription( + it, + depth, + line, + statementTag.uniquenessTag, + NameType.Condition, + statementTag.index, + statementTag.step, + currentMethod + ) + ) + } + } + + + var prefix = "" + var name = NodeConverter.convertNodeToString(nodeAST, statementTag.step) + var type: NameType? = null + + if (statementTag.basicTypeTag == BasicTypeTag.SwitchCase && statementTag.uniquenessTag == UniquenessTag.Unique) { + type = NameType.SwitchCase + } else if (statementTag.basicTypeTag == BasicTypeTag.CaughtException) { + type = NameType.CaughtException + prefix = "Catch" + } else if (statementTag.basicTypeTag == BasicTypeTag.Return) { + type = NameType.Return + prefix = "Return" + name = name ?: "" + } else if (statementTag.basicTypeTag == BasicTypeTag.Invoke && statementTag.uniquenessTag == UniquenessTag.Unique) { + val declaringClass = stmt.invokeExpr.methodRef.declaringClass + val methodName = stmt.invokeExpr.method.name.capitalize() + if (!shouldSkipInvoke(declaringClass.name, methodName)) { + if (stmt is JAssignStmt || stmt is JInvokeStmt) { + type = NameType.Invoke + prefix += declaringClass.javaStyleName.substringBefore('$') + prefix += methodName + name = + "" //todo change var name to val name, everything should be mapped through .convertNodeToString + } + } + } + + if (type != null) { + testNames.add( + TestNameDescription( + prefix + name, + depth, + line, + statementTag.uniquenessTag, + type, + statementTag.index, + statementTag.step, + currentMethod + ) + ) + } + } + } + + if (statementTag.iterations.isNotEmpty()) { + val firstNode = jimpleToASTMap[statementTag.iterations.first().step.stmt] + + val iterationDescription = if (firstNode != null) getTextTypeIterationDescription(firstNode) else null + + if (iterationDescription != null && iterationDescription.isNotEmpty()) { + testNames.add( + TestNameDescription( + "Iterate$iterationDescription", + depth, + line, + UniquenessTag.Partly, + NameType.StartIteration, + statementTag.index, + statementTag.step, + currentMethod + ) + ) + } + + statementTag.iterations.forEach { + collectTags(it, testNames, currentMethod) + } + } + + if (recursion != null) { + val name = when (stmt) { + is JAssignStmt -> "Recursion" + (stmt.rightOp as JVirtualInvokeExpr).method.name //todo through .convertNodeToString + is JInvokeStmt -> "Recursion" + stmt.invokeExpr.method.name //todo through .convertNodeToString + else -> "" + } + if (name.isNotEmpty()) { + testNames.add( + TestNameDescription( + name, + depth, + line, + statementTag.uniquenessTag, + NameType.Invoke, + statementTag.index, + statementTag.step, + currentMethod + ) + ) + collectTags(recursion, testNames, currentMethod) + } + } + collectTags(statementTag.next, testNames, currentMethod) + } + + override fun conditionStep(step: Step, reversed: Boolean, jimpleToASTMap: JimpleToASTMap): String { + val nodeAST = jimpleToASTMap[step.stmt] + return if (nodeAST != null) { + NodeConverter.convertNodeToString(nodeAST, step) ?: "" + } else "" + } } \ No newline at end of file diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/tag/StatementTreeBuilder.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/tag/StatementTreeBuilder.kt index dff44ea997..230e56ec10 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/tag/StatementTreeBuilder.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/tag/StatementTreeBuilder.kt @@ -43,7 +43,7 @@ class StatementTreeBuilder(private val splitSteps: SplitSteps, private val trace * @see ExecutionStructureAnalysis * as they require initial tree tag structure. Otherwise it would be unreadable code here. */ - private fun buildStatementTree(startIndex: Int = 0, statementTag: StatementTag? = null, depth: Int = 0): Int { + private fun buildStatementTree(startIndex: Int = 0, statementTag: StatementTag? = null, depth: Int = 0, isInvokedRecursively: Boolean = false): Int { var currentIndex = startIndex var previousStatementTag = statementTag while (currentIndex < traceSteps.size) { @@ -54,14 +54,28 @@ class StatementTreeBuilder(private val splitSteps: SplitSteps, private val trace nextStatementTag?.let { when { it.step.depth < depth -> { - return currentIndex-- + // When analyzing a steps' path, we can accidentally return before we process the whole path, + // this leads to missing info in summaries. + // In order to solve it, we track whether we called the method from [StatementTreeBuilder.build()] + // or called it here recursively. + // In the first case we do not return, + // in the second case we return an index. + currentIndex-- + if (isInvokedRecursively) return currentIndex + else return@let } it.step.depth > depth -> { currentStatement.invoke = nextStatementTag + + // We save the nextStatementTag in .next as well at the end of the step path + // in order to recover info from it + if (currentIndex == traceSteps.lastIndex) currentStatement.next = nextStatementTag + currentIndex = buildStatementTree( currentIndex + 1, nextStatementTag, - nextStatementTag.step.depth + nextStatementTag.step.depth, + isInvokedRecursively = true ) currentIndex-- } diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/usvm/USummarization.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/usvm/USummarization.kt new file mode 100644 index 0000000000..d4aab53e33 --- /dev/null +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/usvm/USummarization.kt @@ -0,0 +1,77 @@ +package org.utbot.summary.usvm + +import org.utbot.framework.plugin.api.UtMethodTestSet +import mu.KotlinLogging +import org.utbot.common.measureTime +import org.utbot.common.info +import org.utbot.framework.SummariesGenerationType.* +import org.utbot.framework.UtSettings.enableDisplayNameGeneration +import org.utbot.framework.UtSettings.enableJavaDocGeneration +import org.utbot.framework.UtSettings.enableTestNamesGeneration +import org.utbot.framework.UtSettings.summaryGenerationType +import org.utbot.summary.InvokeDescription +import org.utbot.summary.MethodDescriptionSource +import org.utbot.summary.Summarization +import java.io.File + +private val logger = KotlinLogging.logger {} + +/** +USummarization is used to generate summaries for *usvm-sbft*. + +To generate summary, use the following settings: +- *SummariesGenerationType == LIGHT* +- *enableTestNamesGeneration = true* +- *enableDisplayNameGeneration = false* +- *enableJavaDocGeneration = true* + */ + +fun Collection.summarizeAll(): List = + logger.info().measureTime({ + "----------------------------------------------------------------------------------------\n" + + "-------------------Summarization started for ${this.size} test cases--------------------\n" + + "----------------------------------------------------------------------------------------" + }) { + this.map { + it.summarizeOne() + } + } + +private fun UtMethodTestSet.summarizeOne(): UtMethodTestSet = + logger.info().measureTime({ "Summarization for ${this.method}" }) { + + if (summaryGenerationType != LIGHT || !enableTestNamesGeneration || enableDisplayNameGeneration || !enableJavaDocGeneration) { + logger.info { + "Incorrect settings are used to generate Summaries for usvm-sbft" + } + return this + } + + USummarization(sourceFile = null, invokeDescriptions = emptyList()).fillSummaries(this) + return this + } + +class USummarization(sourceFile: File?, invokeDescriptions: List) : + Summarization(sourceFile, invokeDescriptions) { + + /* + * Used to prepare methodTestSet for further generation of summaries. + * In the case of generating tests using USVM, we only need to work with Symbolic tests. + */ + override fun prepareMethodTestSet( + testSet: UtMethodTestSet, + descriptionSource: MethodDescriptionSource + ): UtMethodTestSet { + return when (descriptionSource) { + MethodDescriptionSource.FUZZER -> UtMethodTestSet( + method = testSet.method, + executions = emptyList(), + jimpleBody = testSet.jimpleBody, + errors = testSet.errors, + clustersInfo = testSet.clustersInfo + ) + + MethodDescriptionSource.SYMBOLIC -> testSet + } + } +} diff --git a/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SimpleCommentBuilderTest.kt b/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SimpleCommentBuilderTest.kt index 738adb39be..17fa0d3d17 100644 --- a/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SimpleCommentBuilderTest.kt +++ b/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SimpleCommentBuilderTest.kt @@ -9,6 +9,8 @@ import org.mockito.Mockito.`when` import org.utbot.framework.plugin.api.Step import org.utbot.framework.plugin.api.UtOverflowFailure import org.utbot.summary.ast.JimpleToASTMap +import org.utbot.summary.comment.classic.symbolic.SimpleCommentBuilder +import org.utbot.summary.comment.customtags.getMethodReferenceForSymbolicTest import org.utbot.summary.tag.StatementTag import org.utbot.summary.tag.TraceTag import soot.SootMethod @@ -66,19 +68,16 @@ class SimpleCommentBuilderTest { @Test fun `builds inline link for method`() { - val commentBuilder = SimpleCommentBuilder(traceTag, sootToAst) - val methodReference = commentBuilder.getMethodReference("org.utbot.ClassName", "methodName", listOf(), false) + val methodReference = getMethodReferenceForSymbolicTest("org.utbot.ClassName", "methodName", listOf(), false) val expectedMethodReference = "{@link org.utbot.ClassName#methodName()}" assertEquals(methodReference, expectedMethodReference) } @Test fun `builds inline link for method in nested class`() { - val commentBuilder = SimpleCommentBuilder(traceTag, sootToAst) val methodReference = - commentBuilder.getMethodReference("org.utbot.ClassName\$NestedClassName", "methodName", listOf(), false) + getMethodReferenceForSymbolicTest("org.utbot.ClassName\$NestedClassName", "methodName", listOf(), false) val expectedMethodReference = "{@link org.utbot.ClassName.NestedClassName#methodName()}" assertEquals(methodReference, expectedMethodReference) } - } \ No newline at end of file diff --git a/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SymbolicExecutionClusterCommentBuilderTest.kt b/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SymbolicExecutionClusterCommentBuilderTest.kt index ab4c93c06e..5a3b7fe255 100644 --- a/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SymbolicExecutionClusterCommentBuilderTest.kt +++ b/utbot-summary/src/test/kotlin/org/utbot/summary/comment/SymbolicExecutionClusterCommentBuilderTest.kt @@ -9,6 +9,7 @@ import org.mockito.Mockito.`when` import org.utbot.framework.plugin.api.Step import org.utbot.framework.plugin.api.UtOverflowFailure import org.utbot.summary.ast.JimpleToASTMap +import org.utbot.summary.comment.cluster.SymbolicExecutionClusterCommentBuilder import org.utbot.summary.tag.StatementTag import org.utbot.summary.tag.TraceTag import soot.SootMethod diff --git a/utbot-testing/build.gradle b/utbot-testing/build.gradle new file mode 100644 index 0000000000..a66c022c29 --- /dev/null +++ b/utbot-testing/build.gradle @@ -0,0 +1,63 @@ +compileKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + api project(':utbot-framework-api') + + api project(':utbot-java-fuzzing') + api project(':utbot-instrumentation') + api project(':utbot-summary') + + implementation(project(":utbot-framework")) + testImplementation project(':utbot-sample') + testImplementation project(":utbot-framework").sourceSets.test.output + testImplementation project(":utbot-core").sourceSets.test.output + + implementation("org.unittestbot.soot:soot-utbot-fork:${sootVersion}") { + exclude group:'com.google.guava', module:'guava' + } + + implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: jacksonVersion + implementation group: 'com.github.curious-odd-man', name: 'rgxgen', version: rgxgenVersion + implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: log4j2Version + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion + implementation group: 'org.apache.commons', name: 'commons-text', version: apacheCommonsTextVersion + // we need this for construction mocks from composite models + implementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + + // 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' + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.8.1' + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.1' + + // used for testing code generation + testImplementation group: 'commons-io', name: 'commons-io', version: commonsIoVersion + testImplementation group: 'junit', name: 'junit', version: junit4Version + testImplementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4PlatformVersion + testImplementation group: 'org.antlr', name: 'antlr4', version: antlrVersion + testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + testImplementation group: 'org.mockito', name: 'mockito-inline', version: mockitoInlineVersion + testImplementation group: 'com.google.guava', name: 'guava', version: guavaVersion + + testImplementation group: 'org.mockito', name: 'mockito-inline', version: mockitoInlineVersion + testImplementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version + + // TODO sbft-usvm-merge: UtBot engine expects `com.github.UnitTestBot.ksmt` here + implementation group: 'io.ksmt', name: 'ksmt-core', version: ksmtVersion + implementation group: 'io.ksmt', name: 'ksmt-z3', version: ksmtVersion +} \ No newline at end of file diff --git a/utbot-testing/src/main/java/org/utbot/api/java/AbstractUtBotJavaApiTest.java b/utbot-testing/src/main/java/org/utbot/api/java/AbstractUtBotJavaApiTest.java new file mode 100644 index 0000000000..90b81bdb51 --- /dev/null +++ b/utbot-testing/src/main/java/org/utbot/api/java/AbstractUtBotJavaApiTest.java @@ -0,0 +1,114 @@ +package org.utbot.api.java; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.utbot.common.PathUtil; +import org.utbot.external.api.TestMethodInfo; +import org.utbot.external.api.UtModelFactory; +import org.utbot.framework.plugin.api.*; +import org.utbot.framework.plugin.api.util.UtContext; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.utbot.framework.plugin.api.util.IdUtilKt.getExecutableId; + +/** + * Extracted to share between modules + */ +public class AbstractUtBotJavaApiTest { + + public static final String GENERATED_TEST_CLASS_NAME = "GeneratedTest"; + + public static Method getMethodByName(Class clazz, String name, Class... parameters) { + try { + return clazz.getDeclaredMethod(name, parameters); + } catch (NoSuchMethodException ignored) { + Assertions.fail(); + } + throw new RuntimeException(); + } + + private AutoCloseable context; + // Helpful hand to create models for classes + protected UtModelFactory modelFactory; + + @BeforeEach + public void setUp() { + context = UtContext.Companion.setUtContext(new UtContext(UtContext.class.getClassLoader())); + modelFactory = new UtModelFactory(); + } + + @AfterEach + public void tearDown() { + try { + context.close(); + modelFactory = null; + } catch (Exception e) { + Assertions.fail(); + } + } + + protected static TestMethodInfo buildTestMethodInfo( + Method methodUnderTest, + UtCompositeModel classUnderTestModel, + List parametersModels, + Map staticsModels + ) { + + ExecutableId methodExecutableId = getExecutableId(methodUnderTest); + EnvironmentModels methodState = new EnvironmentModels( + classUnderTestModel, + parametersModels, + staticsModels, + methodExecutableId + ); + + return new TestMethodInfo(methodUnderTest, methodState); + } + + /** + * Utility method to get class path of the class + */ + @NotNull + protected static String getClassPath(Class clazz) { + try { + return normalizePath(clazz.getProtectionDomain().getCodeSource().getLocation()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @NotNull + protected static String normalizePath(URL url) throws URISyntaxException { + return new File(url.toURI()).getPath(); + } + + /** + * @return classpath of the current thread. For testing environment only. + */ + @NotNull + protected static String getDependencyClassPath() { + + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + URL[] urls = PathUtil.getUrlsFromClassLoader(contextClassLoader); + + return Arrays.stream(urls).map(url -> + { + try { + return new File(url.toURI()).toString(); + } catch (URISyntaxException e) { + Assertions.fail(e); + } + throw new RuntimeException(); + }).collect(Collectors.joining(File.pathSeparator)); + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CheckersUtil.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/CheckersUtil.kt similarity index 83% rename from utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CheckersUtil.kt rename to utbot-testing/src/main/kotlin/org/utbot/testing/CheckersUtil.kt index a59fa685f3..2779d8a910 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CheckersUtil.kt +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/CheckersUtil.kt @@ -1,14 +1,6 @@ -package org.utbot.tests.infrastructure - -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.Junit4 -import org.utbot.framework.codegen.MockitoStaticMocking -import org.utbot.framework.codegen.NoStaticMocking -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.TestNg +package org.utbot.testing + +import org.utbot.framework.codegen.domain.* import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MockFramework import org.utbot.framework.plugin.api.MockStrategyApi @@ -17,7 +9,8 @@ import org.utbot.framework.plugin.api.MockStrategyApi.OTHER_CLASSES import org.utbot.framework.util.Conflict import org.utbot.framework.util.ConflictTriggers -data class TestFrameworkConfiguration( +data class TestInfrastructureConfiguration( + val projectType: ProjectType, val testFramework: TestFramework, val mockFramework: MockFramework, val mockStrategy: MockStrategyApi, @@ -64,8 +57,8 @@ data class TestFrameworkConfiguration( val conflictTriggers: ConflictTriggers = ConflictTriggers() -val allTestFrameworkConfigurations: List = run { - val possibleConfiguration = mutableListOf() +val allTestInfrastructureConfigurations: List = run { + val possibleConfiguration = mutableListOf() for (mockStrategy in listOf(NO_MOCKS, OTHER_CLASSES)) { for (testFramework in TestFramework.allItems) { @@ -79,7 +72,8 @@ val allTestFrameworkConfigurations: List = run { val resetNonFinalFieldsAfterClinit = parametrizedTestSource == ParametrizedTestSource.DO_NOT_PARAMETRIZE - possibleConfiguration += TestFrameworkConfiguration( + possibleConfiguration += TestInfrastructureConfiguration( + ProjectType.PureJvm, testFramework, mockFramework, mockStrategy, @@ -90,7 +84,8 @@ val allTestFrameworkConfigurations: List = run { resetNonFinalFieldsAfterClinit, generateUtilClassFile = false ) - possibleConfiguration += TestFrameworkConfiguration( + possibleConfiguration += TestInfrastructureConfiguration( + ProjectType.PureJvm, testFramework, mockFramework, mockStrategy, diff --git a/utbot-testing/src/main/kotlin/org/utbot/testing/CodeGenerationIntegrationTest.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/CodeGenerationIntegrationTest.kt new file mode 100644 index 0000000000..021a5d6417 --- /dev/null +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/CodeGenerationIntegrationTest.kt @@ -0,0 +1,199 @@ +package org.utbot.testing + +import org.utbot.common.FileUtil +import org.utbot.common.withAccessibility +import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.UtMethodTestSet +import org.utbot.instrumentation.ConcreteExecutor +import kotlin.reflect.KClass +import mu.KotlinLogging +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInfo +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.fail +import org.junit.jupiter.engine.descriptor.ClassTestDescriptor +import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.context.ApplicationContext +import java.nio.file.Path + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +@ExtendWith(CodeGenerationIntegrationTest.Companion.ReadRunningTestsNumberBeforeAllTestsCallback::class) +abstract class CodeGenerationIntegrationTest( + private val testClass: KClass<*>, + private var testCodeGeneration: Boolean = true, + private val configurationsToTest: List, + private val applicationContext: ApplicationContext = defaultApplicationContext, +) { + private val testSets: MutableList = arrayListOf() + + fun processTestSet(testSet: UtMethodTestSet) { + if (testCodeGeneration) testSets += testSet + } + + 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 addTestScenario(pkg: Package) { + if (testCodeGeneration) { + packageResult.getOrPut(pkg) { mutableListOf() } += TestScenario( + testClass, + testSets, + configurationsToTest + ) + } + } + + private fun cleanAfterProcessingPackage(pkg: Package) { + // clean test cases after cur package processing + packageResult[pkg]?.clear() + + // clean cur package test classes info + testClassesAmountByPackage[pkg] = 0 + processedTestClassesAmountByPackage[pkg] = 0 + } + + @Test + @Order(Int.MAX_VALUE) + fun processTestCases(testInfo: TestInfo) { + val pkg = testInfo.testClass.get().`package` + addTestScenario(pkg) + + // check if tests are inside package and current test is not the last one + if (runningTestsNumber > 1 && !isPackageFullyProcessed(testInfo.testClass.get())) { + logger.info("Package $pkg is not fully processed yet, code generation will be tested later") + return + } + ConcreteExecutor.defaultPool.close() + + FileUtil.clearTempDirectory(UtSettings.daysLimitForTempFiles) + + val testScenarios = packageResult[pkg] ?: return + try { + val pipelineErrors = mutableListOf() + + for (testScenarioForClass in testScenarios) { + for (configuration in testScenarioForClass.checkedConfigurations) { + try { + val classStages = ClassStages( + testScenarioForClass.testClass, + StageStatusCheck( + firstStage = CodeGeneration, + lastStage = configuration.lastStage, + status = ExecutionStatus.SUCCESS, + ), + testScenarioForClass.testSets, + ) + + val pipelineConfig = TestCodeGeneratorPipeline.configurePipeline(configuration) + TestCodeGeneratorPipeline(pipelineConfig, applicationContext).runClassesCodeGenerationTests(classStages) + } catch (e: RuntimeException) { + logger.warn(e) { "error in test pipeline" } + pipelineErrors.add(e.message) + } + } + } + + if (pipelineErrors.isNotEmpty()) + fail { pipelineErrors.joinToString(System.lineSeparator()) } + } finally { + cleanAfterProcessingPackage(pkg) + } + } + + companion object { + + init { + // trigger old temporary file deletion + FileUtil.OldTempFileDeleter + } + + private val packageResult: MutableMap> = mutableMapOf() + + private var allRunningTestClasses: List = mutableListOf() + + data class TestScenario( + val testClass: KClass<*>, + val testSets: List, + val checkedConfigurations: List, + ) + + val standardTestingConfigurations = listOf( + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.DO_NOT_PARAMETRIZE, TestExecution), + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.PARAMETRIZE, TestExecution), + Configuration(CodegenLanguage.KOTLIN, ParametrizedTestSource.DO_NOT_PARAMETRIZE, TestExecution), + ) + + val ignoreKotlinCompilationConfigurations = listOf( + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.DO_NOT_PARAMETRIZE, TestExecution), + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.PARAMETRIZE, TestExecution), + Configuration(CodegenLanguage.KOTLIN, ParametrizedTestSource.DO_NOT_PARAMETRIZE, CodeGeneration), + ) + + class ReadRunningTestsNumberBeforeAllTestsCallback : BeforeAllCallback { + override fun beforeAll(extensionContext: ExtensionContext) { + val clazz = Class.forName("org.junit.jupiter.engine.descriptor.AbstractExtensionContext") + val field = clazz.getDeclaredField("testDescriptor") + runningTestsNumber = field.withAccessibility { + val testDescriptor = field.get(extensionContext.parent.get()) + // get all running tests and filter disabled + allRunningTestClasses = (testDescriptor as JupiterEngineDescriptor).children + .map { it as ClassTestDescriptor } + .filter { it.testClass.getAnnotation(Disabled::class.java) == null } + .toList() + allRunningTestClasses.size + } + } + } + + private var processedTestClassesAmountByPackage: MutableMap = mutableMapOf() + private var testClassesAmountByPackage: MutableMap = mutableMapOf() + + private var runningTestsNumber: Int = 0 + + internal val logger = KotlinLogging.logger { } + + @JvmStatic + protected val testCaseGeneratorCache = mutableMapOf() + + data class BuildInfo(val buildDir: Path, val dependencyPath: String?) + + private fun getTestPackageSize(packageName: String): Int = + // filter all not disabled tests classes + allRunningTestClasses + .filter { it.testClass.`package`.name == packageName } + .distinctBy { it.testClass.name.substringBeforeLast("Kt") } + .size + + private fun isPackageFullyProcessed(testClass: Class<*>): Boolean { + val currentPackage = testClass.`package` + + if (currentPackage !in testClassesAmountByPackage) + testClassesAmountByPackage[currentPackage] = getTestPackageSize(currentPackage.name) + + processedTestClassesAmountByPackage.merge(currentPackage, 1, Int::plus) + + val processed = processedTestClassesAmountByPackage[currentPackage]!! + val total = testClassesAmountByPackage[currentPackage]!! + return processed == total + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CompilationAndRunUtils.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/CompilationAndRunUtils.kt similarity index 93% rename from utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CompilationAndRunUtils.kt rename to utbot-testing/src/main/kotlin/org/utbot/testing/CompilationAndRunUtils.kt index ddf523586a..dcaf335f47 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/CompilationAndRunUtils.kt +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/CompilationAndRunUtils.kt @@ -1,12 +1,12 @@ -package org.utbot.tests.infrastructure +package org.utbot.testing import org.utbot.framework.plugin.api.CodegenLanguage import java.io.File import java.nio.file.Path import org.utbot.common.FileUtil -import org.utbot.engine.logger -import org.utbot.framework.codegen.Junit5 -import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.process.OpenModulesContainer data class ClassUnderTest( val testClassSimpleName: String, @@ -60,9 +60,10 @@ fun runTests( ) { val classpath = System.getProperty("java.class.path") + File.pathSeparator + buildDirectory val executionInvoke = generatedLanguage.executorInvokeCommand - val additionalArguments = listOf( - "-ea", // Enable assertions - ) + val additionalArguments = buildList { + addAll(OpenModulesContainer.javaVersionSpecificArguments) + add("-ea") + } val command = testFramework.getRunTestsCommand( executionInvoke, diff --git a/utbot-testing/src/main/kotlin/org/utbot/testing/Configurations.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/Configurations.kt new file mode 100644 index 0000000000..e45de519d9 --- /dev/null +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/Configurations.kt @@ -0,0 +1,47 @@ +package org.utbot.testing + +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.context.simple.SimpleApplicationContext +import org.utbot.framework.context.simple.SimpleMockerContext +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MockStrategyApi + +interface AbstractConfiguration { + val projectType: ProjectType + val mockStrategy: MockStrategyApi + val language: CodegenLanguage + val parametrizedTestSource: ParametrizedTestSource + val lastStage: Stage +} + +data class Configuration( + override val language: CodegenLanguage, + override val parametrizedTestSource: ParametrizedTestSource, + override val lastStage: Stage, +): AbstractConfiguration { + override val projectType: ProjectType + get() = ProjectType.PureJvm + + override val mockStrategy: MockStrategyApi + get() = MockStrategyApi.defaultItem +} + +data class SpringConfiguration( + override val language: CodegenLanguage, + override val parametrizedTestSource: ParametrizedTestSource, + override val lastStage: Stage, +): AbstractConfiguration { + override val projectType: ProjectType + get() = ProjectType.Spring + + override val mockStrategy: MockStrategyApi + get() = MockStrategyApi.springDefaultItem +} + +val defaultApplicationContext = SimpleApplicationContext( + SimpleMockerContext( + mockFrameworkInstalled = true, + staticsMockingIsConfigured = true, + ) +) diff --git a/utbot-testing/src/main/kotlin/org/utbot/testing/TestCodeGeneratorPipeline.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/TestCodeGeneratorPipeline.kt new file mode 100644 index 0000000000..4e1d16a386 --- /dev/null +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/TestCodeGeneratorPipeline.kt @@ -0,0 +1,434 @@ +package org.utbot.testing + +import mu.KotlinLogging +import org.junit.jupiter.api.Assertions.assertFalse +import org.utbot.common.FileUtil +import org.utbot.common.measureTime +import org.utbot.common.info +import org.utbot.framework.codegen.generator.CodeGeneratorResult +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.generator.CodeGeneratorParams +import org.utbot.framework.codegen.services.language.CgLanguageAssistant +import org.utbot.framework.codegen.tree.ututils.UtilClassKind +import org.utbot.framework.codegen.tree.ututils.UtilClassKind.Companion.UT_UTILS_INSTANCE_NAME +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.description +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.withUtContext +import java.io.File +import java.nio.file.Path +import kotlin.reflect.KClass + +internal val logger = KotlinLogging.logger {} + +class TestCodeGeneratorPipeline( + private val testInfrastructureConfiguration: TestInfrastructureConfiguration, + private val applicationContext: ApplicationContext +) { + + fun runClassesCodeGenerationTests(classesStages: ClassStages) { + val pipeline = with(classesStages) { + ClassPipeline(StageContext(testClass, testSets, testSets.size), check) + } + + checkPipelinesResults(pipeline) + } + + private fun runPipelinesStages(classPipeline: ClassPipeline): CodeGenerationResult { + val classUnderTest = classPipeline.stageContext.classUnderTest + val classPipelineName = classUnderTest.qualifiedName + ?: error("${classUnderTest.simpleName} doesn't have a fqn name") + + logger + .info() + .measureTime({ "Executing code generation tests for [$classPipelineName]" }) { + CodeGeneration.verifyPipeline(classPipeline)?.let { + withUtContext(UtContext(classUnderTest.java.classLoader)) { + processCodeGenerationStage(it) + } + } + + Compilation.verifyPipeline(classPipeline)?.let { + processCompilationStages(it) + } + + TestExecution.verifyPipeline(classPipeline)?.let { + processTestExecutionStages(it) + } + + return with(classPipeline.stageContext) { + CodeGenerationResult(classUnderTest, numberOfTestCases, stages) + } + } + } + + @Suppress("UNCHECKED_CAST") + private fun processCodeGenerationStage(classPipeline: ClassPipeline) { + with(classPipeline.stageContext) { + val information = StageExecutionInformation(CodeGeneration) + val testSets = data as List + + val codegenLanguage = testInfrastructureConfiguration.codegenLanguage + val parametrizedTestSource = testInfrastructureConfiguration.parametrizedTestSource + + val codeGenerationResult = callToCodeGenerator(testSets, classUnderTest) + val testClass = codeGenerationResult.generatedCode + + // actual number of the tests in the generated testClass + val generatedMethodsCount = testClass + .lines() + .count { + val trimmedLine = it.trimStart() + val prefix = when (codegenLanguage) { + CodegenLanguage.JAVA -> + when (parametrizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> "@Test" + ParametrizedTestSource.PARAMETRIZE -> "public void parameterizedTestsFor" + } + + CodegenLanguage.KOTLIN -> + when (parametrizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> "@Test" + ParametrizedTestSource.PARAMETRIZE -> "fun parameterizedTestsFor" + } + } + trimmedLine.startsWith(prefix) + } + // expected number of the tests in the generated testClass + val expectedNumberOfGeneratedMethods = + when (parametrizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> testSets.sumOf { it.executions.size } + ParametrizedTestSource.PARAMETRIZE -> testSets.filter { it.executions.isNotEmpty() }.size + } + + // check for error in the generated file + runCatching { + val separator = System.lineSeparator() + require(ERROR_REGION_BEGINNING !in testClass) { + val lines = testClass.lines().withIndex().toList() + + val errorsRegionsBeginningIndices = lines + .filter { it.value.trimStart().startsWith(ERROR_REGION_BEGINNING) } + + val errorsRegionsEndIndices = lines + .filter { it.value.trimStart().startsWith(ERROR_REGION_END) } + + val errorRegions = errorsRegionsBeginningIndices.map { beginning -> + val endIndex = errorsRegionsEndIndices.indexOfFirst { it.index > beginning.index } + lines.subList(beginning.index + 1, errorsRegionsEndIndices[endIndex].index).map { it.value } + } + + val errorText = errorRegions.joinToString(separator, separator, separator) { errorRegion -> + val text = errorRegion.joinToString(separator = separator) + "Error region in ${classUnderTest.simpleName}: $text" + } + + logger.error(errorText) + + "Errors regions has been generated: $errorText" + } + + // for now, we skip a comparing of generated and expected test methods + // in parametrized test generation mode + // because there are problems with determining expected number of methods, + // due to a feature that generates several separated parametrized tests + // when we have several executions with different result type + if (parametrizedTestSource != ParametrizedTestSource.PARAMETRIZE) { + require(generatedMethodsCount == expectedNumberOfGeneratedMethods) { + "Something went wrong during the code generation for ${classUnderTest.simpleName}. " + + "Expected to generate $expectedNumberOfGeneratedMethods test methods, " + + "but got only $generatedMethodsCount" + } + } + }.onFailure { + val classes = listOf(classPipeline).retrieveClasses() + val buildDirectory = classes.createBuildDirectory() + + val testClassName = classPipeline.retrieveTestClassName("BrokenGeneratedTest") + val generatedTestFile = writeTest(testClass, testClassName, buildDirectory, codegenLanguage) + val generatedUtilClassFile = codeGenerationResult.utilClassKind?.writeUtilClassToFile(buildDirectory, codegenLanguage) + + logger.error("Broken test has been written to the file: [$generatedTestFile]") + if (generatedUtilClassFile != null) { + logger.error("Util class for the broken test has been written to the file: [$generatedUtilClassFile]") + } + logger.error("Failed configuration: $testInfrastructureConfiguration") + + throw it + } + + classPipeline.stageContext = copy(data = codeGenerationResult, stages = stages + information.completeStage()) + } + } + + private fun UtilClassKind.writeUtilClassToFile(buildDirectory: Path, language: CodegenLanguage): File { + val utilClassFile = File(buildDirectory.toFile(), "$UT_UTILS_INSTANCE_NAME${language.extension}") + val utilClassText = getUtilClassText(language) + return writeFile(utilClassText, utilClassFile) + } + + private data class GeneratedTestClassInfo( + val testClassName: String, + val generatedTestFile: File, + val generatedUtilClassFile: File? + ) + + @Suppress("UNCHECKED_CAST") + private fun processCompilationStages(classesPipeline: ClassPipeline) { + val information = StageExecutionInformation(Compilation) + val classes = listOf(classesPipeline).retrieveClasses() + val buildDirectory = classes.createBuildDirectory() + + val codegenLanguage = testInfrastructureConfiguration.codegenLanguage + + val codeGeneratorResult = classesPipeline.stageContext.data as CodeGeneratorResult//String + val testClass = codeGeneratorResult.generatedCode + + val testClassName = classesPipeline.retrieveTestClassName("GeneratedTest") + val generatedTestFile = writeTest(testClass, testClassName, buildDirectory, codegenLanguage) + val generatedUtilClassFile = codeGeneratorResult.utilClassKind?.writeUtilClassToFile(buildDirectory, codegenLanguage) + + logger.info("Test has been written to the file: [$generatedTestFile]") + if (generatedUtilClassFile != null) { + logger.info("Util class for the test has been written to the file: [$generatedUtilClassFile]") + } + + val testClassesNamesToTestGeneratedTests = GeneratedTestClassInfo(testClassName, generatedTestFile, generatedUtilClassFile) + + + val sourceFiles = mutableListOf().apply { + testClassesNamesToTestGeneratedTests.generatedTestFile.absolutePath?.let { this += it } + testClassesNamesToTestGeneratedTests.generatedUtilClassFile?.absolutePath?.let { this += it } + } + compileTests( + "$buildDirectory", + sourceFiles, + codegenLanguage + ) + + classesPipeline.stageContext = classesPipeline.stageContext.copy( + data = CompilationResult("$buildDirectory", testClassesNamesToTestGeneratedTests.testClassName), + stages = classesPipeline.stageContext.stages + information.completeStage() + ) + } + + /** + * For simple CUT equals to its fqn + [testSuffix] suffix, + * for nested CUT is its package + dot + its simple name + [testSuffix] suffix (to avoid outer class mention). + */ + private fun ClassPipeline.retrieveTestClassName(testSuffix: String): String = + stageContext.classUnderTest.let { "${it.java.`package`.name}.${it.simpleName}" } + testSuffix + + private fun List>.createBuildDirectory() = + FileUtil.isolateClassFiles(*toTypedArray()).toPath() + + private fun List.retrieveClasses() = map { it.stageContext.classUnderTest.java } + + @Suppress("UNCHECKED_CAST") + private fun processTestExecutionStages(classesPipeline: ClassPipeline) { + val information = StageExecutionInformation(TestExecution) + val compilationResult = classesPipeline.stageContext.data as CompilationResult + val buildDirectory = compilationResult.buildDirectory + val testClassesNames = listOf(compilationResult.testClassName) + + with(testInfrastructureConfiguration) { + runTests(buildDirectory, testClassesNames, testFramework, codegenLanguage) + } + + classesPipeline.stageContext = classesPipeline.stageContext.copy( + data = Unit, + stages = classesPipeline.stageContext.stages + information.completeStage() + ) + } + + private fun callToCodeGenerator( + testSets: List, + classUnderTest: KClass<*> + ): CodeGeneratorResult { + val params = mutableMapOf>() + + withUtContext(UtContext(classUnderTest.java.classLoader)) { + val codeGenerator = with(testInfrastructureConfiguration) { + applicationContext.createCodeGenerator( + CodeGeneratorParams( + classUnderTest.id, + projectType = projectType, + generateUtilClassFile = generateUtilClassFile, + paramNames = params, + testFramework = testFramework, + staticsMocking = staticsMocking, + forceStaticMocking = forceStaticMocking, + generateWarningsForStaticMocking = false, + codegenLanguage = codegenLanguage, + cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(codegenLanguage), + parameterizedTestSource = parametrizedTestSource, + runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, + enableTestsTimeout = enableTestsTimeout, + ) + ) + } + val testClassCustomName = "${classUnderTest.java.simpleName}GeneratedTest" + + return codeGenerator.generateAsStringWithTestReport(testSets, testClassCustomName) + } + } + + private fun checkPipelinesResults(classesPipeline: ClassPipeline) { + val result = runPipelinesStages(classesPipeline) + val classChecks = classesPipeline.stageContext.classUnderTest to listOf(classesPipeline.check) + processResults(result, classChecks) + } + + private fun processResults( + result: CodeGenerationResult, + classChecks: Pair, List> + ) { + val stageStatusChecks = classChecks.second.mapNotNull { check -> + runCatching { check(result) } + .fold( + onSuccess = { if (it) null else check.description }, + onFailure = { it.description } + ) + } + val transformedResult: Pair, List> = result.classUnderTest to stageStatusChecks + val failedResultExists = transformedResult.second.isNotEmpty() + + assertFalse(failedResultExists) { + "There are failed checks: ${transformedResult.second}" + } + } + + companion object { + private val defaultConfiguration = + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.DO_NOT_PARAMETRIZE, TestExecution) + + //TODO: this variable must be lateinit, without strange default initializer + var currentTestInfrastructureConfiguration: TestInfrastructureConfiguration = configurePipeline(defaultConfiguration) + + fun configurePipeline(configuration: AbstractConfiguration) = + TestInfrastructureConfiguration( + projectType = configuration.projectType, + testFramework = TestFramework.defaultItem, + codegenLanguage = configuration.language, + mockFramework = MockFramework.defaultItem, + mockStrategy = configuration.mockStrategy, + staticsMocking = StaticsMocking.defaultItem, + parametrizedTestSource = configuration.parametrizedTestSource, + forceStaticMocking = ForceStaticMocking.defaultItem, + generateUtilClassFile = false + ).also { + currentTestInfrastructureConfiguration = it + } + + private const val ERROR_REGION_BEGINNING = "///region Errors" + private const val ERROR_REGION_END = "///endregion" + } +} + +enum class ExecutionStatus { + IN_PROCESS, FAILED, SUCCESS +} + +sealed class Stage(private val name: String, val nextStage: Stage?) { + override fun toString() = name + + fun verifyPipeline(classesPipeline: ClassPipeline): ClassPipeline? = + if (classesPipeline.check.firstStage <= this && classesPipeline.check.lastStage >= this) classesPipeline else null + + abstract operator fun compareTo(stage: Stage): Int +} + +object CodeGeneration : Stage("Code Generation", Compilation) { + override fun compareTo(stage: Stage): Int = if (stage is CodeGeneration) 0 else -1 +} + +object Compilation : Stage("Compilation", TestExecution) { + override fun compareTo(stage: Stage): Int = + when (stage) { + is CodeGeneration -> 1 + is Compilation -> 0 + else -> -1 + } +} + +object TestExecution : Stage("Test Execution", null) { + override fun compareTo(stage: Stage): Int = if (stage is TestExecution) 0 else 1 +} + +private fun pipeline(firstStage: Stage = CodeGeneration, lastStage: Stage = TestExecution): Sequence = + generateSequence(firstStage) { if (it == lastStage) null else it.nextStage } + +data class StageExecutionInformation( + val stage: Stage, + val status: ExecutionStatus = ExecutionStatus.IN_PROCESS +) { + fun completeStage(status: ExecutionStatus = ExecutionStatus.SUCCESS) = copy(status = status) +} + +data class CodeGenerationResult( + val classUnderTest: KClass<*>, + val numberOfTestCases: Int, + val stageStatisticInformation: List +) + +sealed class PipelineResultCheck( + val description: String, + private val check: (CodeGenerationResult) -> Boolean +) { + open operator fun invoke(codeGenerationResult: CodeGenerationResult) = check(codeGenerationResult) +} + +/** + * Checks that stage failed and all previous stages are successfully processed. + */ +class StageStatusCheck( + val firstStage: Stage = CodeGeneration, + val lastStage: Stage, + status: ExecutionStatus, +) : PipelineResultCheck( + description = "Expect [$lastStage] to be in [$status] status", + check = constructPipelineResultCheck(firstStage, lastStage, status) +) + +private fun constructPipelineResultCheck( + firstStage: Stage, + lastStage: Stage, + status: ExecutionStatus +): (CodeGenerationResult) -> Boolean = + { result -> + val statuses = result.stageStatisticInformation.associate { it.stage to it.status } + val failedPrevStage = pipeline(firstStage, lastStage) + .takeWhile { it != lastStage } + .firstOrNull { statuses[it] != ExecutionStatus.SUCCESS } + + if (failedPrevStage != null) error("[$lastStage] is not started cause $failedPrevStage has failed") + + statuses[lastStage] == status + } + +data class CompilationResult(val buildDirectory: String, val testClassName: String) + +/** + * Context to run Stage. Contains class under test, data (input of current stage), number of analyzed test cases and + * stage execution information. + */ +data class StageContext( + val classUnderTest: KClass<*>, + val data: Any = Unit, + val numberOfTestCases: Int = 0, + val stages: List = emptyList(), + val status: ExecutionStatus = ExecutionStatus.SUCCESS +) + +data class ClassStages( + val testClass: KClass<*>, + val check: StageStatusCheck, + val testSets: List = emptyList() +) + +data class ClassPipeline(var stageContext: StageContext, val check: StageStatusCheck) \ No newline at end of file diff --git a/utbot-testing/src/main/kotlin/org/utbot/testing/TestSpecificTestCaseGenerator.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/TestSpecificTestCaseGenerator.kt new file mode 100644 index 0000000000..5580b593ee --- /dev/null +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/TestSpecificTestCaseGenerator.kt @@ -0,0 +1,114 @@ +package org.utbot.testing + +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import org.utbot.engine.EngineController +import org.utbot.engine.Mocker +import org.utbot.engine.UtBotSymbolicEngine +import org.utbot.engine.util.mockListeners.ForceMockListener +import org.utbot.engine.util.mockListeners.ForceStaticMockListener +import org.utbot.framework.UtSettings +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.framework.plugin.api.TestCaseGenerator +import org.utbot.framework.plugin.api.UtError +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.UtMethodTestSet +import org.utbot.framework.plugin.api.UtSymbolicExecution +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.services.JdkInfoDefaultProvider +import org.utbot.framework.util.Conflict +import org.utbot.framework.util.jimpleBody +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.taint.TaintConfigurationProvider +import java.nio.file.Path + +/** + * Special [UtMethodTestSet] generator for test methods that has a correct + * wrapper for suspend function [TestCaseGenerator.generateAsync]. + */ +class TestSpecificTestCaseGenerator( + buildDir: Path, + classpath: String?, + dependencyPaths: String, + engineActions: MutableList<(UtBotSymbolicEngine) -> Unit> = mutableListOf(), + isCanceled: () -> Boolean = { false }, + private val taintConfigurationProvider: TaintConfigurationProvider? = null, + applicationContext: ApplicationContext = defaultApplicationContext, +): TestCaseGenerator( + listOf(buildDir), + classpath, + dependencyPaths, + JdkInfoDefaultProvider().info, + engineActions, + isCanceled, + forceSootReload = false, + applicationContext = applicationContext, +) { + + private val logger = KotlinLogging.logger {} + + fun generate( + method: ExecutableId, + mockStrategy: MockStrategyApi, + additionalMockAlwaysClasses: Set = emptySet() + ): UtMethodTestSet { + if (isCanceled()) { + return UtMethodTestSet(method) + } + + logger.trace { "UtSettings:${System.lineSeparator()}" + UtSettings.toString() } + + val executions = mutableListOf() + val errors = mutableMapOf() + + val mockAlwaysDefaults = Mocker.javaDefaultClasses.mapTo(mutableSetOf()) { it.id } + additionalMockAlwaysClasses + val defaultTimeEstimator = ExecutionTimeEstimator(UtSettings.utBotGenerationTimeoutInMillis, 1) + + val forceMockListener = ForceMockListener.create(this, conflictTriggers) + val forceStaticMockListener = ForceStaticMockListener.create(this, conflictTriggers) + + runBlocking { + val controller = EngineController() + controller.job = launch { + super + .generateAsync( + controller, + method, + mockStrategy, + mockAlwaysDefaults, + defaultTimeEstimator, + taintConfigurationProvider + ) + .collect { + when (it) { + is UtExecution -> { + if (it is UtSymbolicExecution && + (conflictTriggers.triggered(Conflict.ForceMockHappened) || + conflictTriggers.triggered(Conflict.ForceStaticMockHappened)) + ) { + it.containsMocking = true + } + executions += it + } + is UtError -> errors.merge(it.description, 1, Int::plus) + } + } + } + } + + conflictTriggers.reset(Conflict.ForceMockHappened, Conflict.ForceStaticMockHappened) + forceMockListener.detach(this, forceMockListener) + forceStaticMockListener.detach(this, forceStaticMockListener) + + val minimizedExecutions = super.minimizeExecutions( + method, + executions, + rerunExecutor = ConcreteExecutor(concreteExecutionContext.instrumentationFactory, classpathForEngine) + ) + return UtMethodTestSet(method, minimizedExecutions, jimpleBody(method), errors) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/UtModelTestCaseChecker.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/UtModelTestCaseChecker.kt similarity index 80% rename from utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/UtModelTestCaseChecker.kt rename to utbot-testing/src/main/kotlin/org/utbot/testing/UtModelTestCaseChecker.kt index fb4399d81a..8297859d49 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/UtModelTestCaseChecker.kt +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/UtModelTestCaseChecker.kt @@ -1,6 +1,6 @@ @file:Suppress("NestedLambdaShadowedImplicitParameter") -package org.utbot.tests.infrastructure +package org.utbot.testing import org.junit.jupiter.api.Assertions.assertTrue import org.utbot.common.ClassLocation @@ -12,7 +12,6 @@ import org.utbot.engine.prettify import org.utbot.framework.UtSettings.checkSolverTimeoutMillis import org.utbot.framework.UtSettings.useFuzzing import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.MockStrategyApi @@ -25,7 +24,6 @@ import org.utbot.framework.plugin.api.UtExecutionResult import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.exceptionOrNull -import org.utbot.framework.plugin.api.getOrThrow import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.declaringClazz import org.utbot.framework.plugin.api.util.defaultValueModel @@ -44,40 +42,60 @@ import kotlin.reflect.KFunction3 abstract class UtModelTestCaseChecker( testClass: KClass<*>, testCodeGeneration: Boolean = true, - languagePipelines: List = listOf( - CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), - CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN) - ) -) : CodeGenerationIntegrationTest(testClass, testCodeGeneration, languagePipelines) { + configurations: List = standardTestingConfigurations, +) : CodeGenerationIntegrationTest(testClass, testCodeGeneration, configurations) { protected fun check( method: KFunction2<*, *, *>, branches: ExecutionsNumberMatcher, vararg matchers: (UtModel, UtExecutionResult) -> Boolean, - mockStrategy: MockStrategyApi = NO_MOCKS - ) = internalCheck(method, mockStrategy, branches, matchers) + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, + mockStrategy, + branches, + matchers, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) protected fun check( method: KFunction3<*, *, *, *>, branches: ExecutionsNumberMatcher, vararg matchers: (UtModel, UtModel, UtExecutionResult) -> Boolean, - mockStrategy: MockStrategyApi = NO_MOCKS - ) = internalCheck(method, mockStrategy, branches, matchers) + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, + mockStrategy, + branches, + matchers, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) protected fun checkStatic( method: KFunction1<*, *>, branches: ExecutionsNumberMatcher, vararg matchers: (UtModel, UtExecutionResult) -> Boolean, - mockStrategy: MockStrategyApi = NO_MOCKS - ) = internalCheck(method, mockStrategy, branches, matchers) + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, + mockStrategy, + branches, + matchers, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) protected fun checkStaticsAfter( method: KFunction2<*, *, *>, branches: ExecutionsNumberMatcher, vararg matchers: (UtModel, StaticsModelType, UtExecutionResult) -> Boolean, - mockStrategy: MockStrategyApi = NO_MOCKS + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalMockAlwaysClasses: Set = emptySet() ) = internalCheck( method, mockStrategy, branches, matchers, - arguments = ::withStaticsAfter + arguments = ::withStaticsAfter, + additionalMockAlwaysClasses = additionalMockAlwaysClasses ) private fun internalCheck( @@ -85,7 +103,8 @@ abstract class UtModelTestCaseChecker( mockStrategy: MockStrategyApi, branches: ExecutionsNumberMatcher, matchers: Array>, - arguments: (UtExecution) -> List = ::withResult + arguments: (UtExecution) -> List = ::withResult, + additionalMockAlwaysClasses: Set = emptySet() ) { workaround(HACK) { // @todo change to the constructor parameter @@ -95,7 +114,7 @@ abstract class UtModelTestCaseChecker( val executableId = method.executableId withUtContext(UtContext(method.declaringClazz.classLoader)) { - val testSet = executions(executableId, mockStrategy) + val testSet = executions(executableId, mockStrategy, additionalMockAlwaysClasses) assertTrue(testSet.errors.isEmpty()) { "We have errors: ${testSet.errors.entries.map { "${it.value}: ${it.key}" }.prettify()}" @@ -103,7 +122,7 @@ abstract class UtModelTestCaseChecker( // if force mocking took place in parametrized test generation, // we do not need to process this [testSet] - if (TestCodeGeneratorPipeline.currentTestFrameworkConfiguration.isParametrizedAndMocked) { + if (TestCodeGeneratorPipeline.currentTestInfrastructureConfiguration.isParametrizedAndMocked) { conflictTriggers.reset(Conflict.ForceMockHappened, Conflict.ForceStaticMockHappened) return } @@ -114,7 +133,7 @@ abstract class UtModelTestCaseChecker( } executions.checkMatchers(matchers, arguments) - processTestCase(testSet) + processTestSet(testSet) } } @@ -139,7 +158,8 @@ abstract class UtModelTestCaseChecker( private fun executions( method: ExecutableId, - mockStrategy: MockStrategyApi + mockStrategy: MockStrategyApi, + additionalMockAlwaysClasses: Set = emptySet() ): UtMethodTestSet { val classLocation = locateClass(method.classId.jClass) if (classLocation != previousClassLocation) { @@ -157,7 +177,7 @@ abstract class UtModelTestCaseChecker( ) } - return testCaseGenerator.generate(method, mockStrategy) + return testCaseGenerator.generate(method, mockStrategy, additionalMockAlwaysClasses) } protected inline fun UtExecutionResult.isException(): Boolean = exceptionOrNull() is T @@ -191,6 +211,7 @@ abstract class UtModelTestCaseChecker( .singleOrNull { it.fieldId == fieldId } fieldAccess?.fieldModel ?: fieldId.type.defaultValueModel() } + else -> error("Can't get ${fieldId.name} from $this") } diff --git a/utbot-testing/src/main/kotlin/org/utbot/testing/UtValueTestCaseChecker.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/UtValueTestCaseChecker.kt new file mode 100644 index 0000000000..bc77d9de81 --- /dev/null +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/UtValueTestCaseChecker.kt @@ -0,0 +1,2376 @@ +@file:Suppress("NestedLambdaShadowedImplicitParameter") + +package org.utbot.testing + +import org.junit.jupiter.api.Assertions.assertTrue +import org.utbot.common.ClassLocation +import org.utbot.common.FileUtil.clearTempDirectory +import org.utbot.common.FileUtil.findPathToClassFiles +import org.utbot.common.FileUtil.locateClass +import org.utbot.engine.prettify +import org.utbot.framework.SummariesGenerationType +import org.utbot.framework.UtSettings +import org.utbot.framework.UtSettings.daysLimitForTempFiles +import org.utbot.framework.context.ApplicationContext +import org.utbot.framework.coverage.Coverage +import org.utbot.framework.coverage.counters +import org.utbot.framework.coverage.methodCoverage +import org.utbot.framework.coverage.toAtLeast +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.FieldMockTarget +import org.utbot.framework.plugin.api.MockId +import org.utbot.framework.plugin.api.MockInfo +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.framework.plugin.api.MockStrategyApi.NO_MOCKS +import org.utbot.framework.plugin.api.ObjectMockTarget +import org.utbot.framework.plugin.api.ParameterMockTarget +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.UtInstrumentation +import org.utbot.framework.plugin.api.UtMethodTestSet +import org.utbot.framework.plugin.api.UtMockValue +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.UtValueExecution +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.declaringClazz +import org.utbot.framework.plugin.api.util.enclosingClass +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.kClass +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.framework.util.Conflict +import org.utbot.framework.util.toValueTestCase +import org.utbot.summary.summarizeAll +import org.utbot.testcheckers.ExecutionsNumberMatcher +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import java.util.stream.Collectors +import java.util.stream.Stream +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KFunction0 +import kotlin.reflect.KFunction1 +import kotlin.reflect.KFunction2 +import kotlin.reflect.KFunction3 +import kotlin.reflect.KFunction4 +import kotlin.reflect.KFunction5 + +abstract class UtValueTestCaseChecker( + testClass: KClass<*>, + testCodeGeneration: Boolean = true, + val configurations: List = standardTestingConfigurations, + val applicationContext: ApplicationContext = defaultApplicationContext, +) : CodeGenerationIntegrationTest(testClass, testCodeGeneration, configurations) { + // contains already analyzed by the engine methods + private val analyzedMethods: MutableMap = mutableMapOf() + + val searchDirectory: Path = Paths.get("../utbot-sample/src/main/java") + + init { + UtSettings.checkSolverTimeoutMillis = 0 + UtSettings.checkNpeInNestedMethods = true + UtSettings.checkNpeInNestedNotPrivateMethods = true + UtSettings.substituteStaticsWithSymbolicVariable = true + UtSettings.useAssembleModelGenerator = true + UtSettings.saveRemainingStatesForConcreteExecution = false + UtSettings.useFuzzing = false + UtSettings.useCustomJavaDocTags = false + UtSettings.summaryGenerationType = SummariesGenerationType.FULL + } + + // checks paramsBefore and result + protected inline fun check( + method: KFunction1<*, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet(), + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun check( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun check( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun check( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun check( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // check paramsBefore and Result, suitable to check exceptions + protected inline fun checkWithException( + method: KFunction1<*, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithException( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithException( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithException( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithException( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // check this, paramsBefore and result value + protected inline fun checkWithThis( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withThisAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithThis( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, + arguments = ::withThisAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithThis( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, + arguments = ::withThisAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithThis( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, + arguments = ::withThisAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithThis( + method: KFunction5, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, T4, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, T4::class, + arguments = ::withThisAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithThisAndException( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withThisAndException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithThisAndException( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, + arguments = ::withThisAndException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithThisAndException( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, + arguments = ::withThisAndException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithThisAndException( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, + arguments = ::withThisAndException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithThisAndException( + method: KFunction5, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, T4, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, T4::class, + arguments = ::withThisAndException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // checks paramsBefore, mocks and result value + protected inline fun checkMocksInStaticMethod( + method: KFunction0, + branches: ExecutionsNumberMatcher, + vararg matchers: (Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksInStaticMethod( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksInStaticMethod( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksInStaticMethod( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksInStaticMethod( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // checks paramsBefore, mocks and result value + protected inline fun checkMocks( + method: KFunction1<*, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocks( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocks( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocks( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocks( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, Mocks, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withMocks, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkThisMocksAndExceptions( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, Mocks, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withThisMocksAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkThisMocksAndExceptions( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, Mocks, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, + arguments = ::withThisMocksAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkThisMocksAndExceptions( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, Mocks, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withThisMocksAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkThisMocksAndExceptions( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, Mocks, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withThisMocksAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkThisMocksAndExceptions( + method: KFunction5, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, T4, Mocks, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withThisMocksAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // check paramsBefore, mocks and instrumentation and result value + protected inline fun checkMocksAndInstrumentation( + method: KFunction1<*, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withMocksAndInstrumentation, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksAndInstrumentation( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withMocksAndInstrumentation, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksAndInstrumentation( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withMocksAndInstrumentation, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksAndInstrumentation( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withMocksAndInstrumentation, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksAndInstrumentation( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withMocksAndInstrumentation, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // check this, paramsBefore, mocks, instrumentation and return value + protected inline fun checkMocksInstrumentationAndThis( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withMocksInstrumentationAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksInstrumentationAndThis( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, + arguments = ::withMocksInstrumentationAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksInstrumentationAndThis( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, + arguments = ::withMocksInstrumentationAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksInstrumentationAndThis( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, + arguments = ::withMocksInstrumentationAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMocksInstrumentationAndThis( + method: KFunction5, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, T4, Mocks, Instrumentation, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, T4::class, + arguments = ::withMocksInstrumentationAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // checks paramsBefore and return value for static methods + protected inline fun checkStaticMethod( + method: KFunction0, + branches: ExecutionsNumberMatcher, + vararg matchers: (R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethod( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethod( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethod( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethod( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // checks paramsBefore and Result, suitable for exceptions check + protected inline fun checkStaticMethodWithException( + method: KFunction0, + branches: ExecutionsNumberMatcher, + vararg matchers: (Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodWithException( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodWithException( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodWithException( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodWithException( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withException, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // check arguments, statics and return value + protected inline fun checkStatics( + method: KFunction1<*, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStatics( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStatics( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStatics( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStatics( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // check arguments, statics and Result for exceptions check + protected inline fun checkStaticsAndException( + method: KFunction1<*, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (StaticsType, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withStaticsBeforeAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsAndException( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withStaticsBeforeAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsAndException( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, StaticsType, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withStaticsBeforeAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsAndException( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, StaticsType, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withStaticsBeforeAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsAndException( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, StaticsType, Result) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withStaticsBeforeAndExceptions, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsAfter( + method: KFunction1<*, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsAfter( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsAfter( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsAfter( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsAfter( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkThisAndStaticsAfter( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + 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(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + 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(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + 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(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + 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(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, T4::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // checks paramsBefore, staticsBefore and return value for static methods + protected inline fun checkStaticsInStaticMethod( + method: KFunction0, + branches: ExecutionsNumberMatcher, + vararg matchers: (StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsInStaticMethod( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsInStaticMethod( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsInStaticMethod( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsInStaticMethod( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsInStaticMethod( + method: KFunction5, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, T5, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withStaticsBefore, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // check this, arguments and result value + protected inline fun checkStaticsWithThis( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withThisStaticsBeforeAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsWithThis( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, + arguments = ::withThisStaticsBeforeAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsWithThis( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, + arguments = ::withThisStaticsBeforeAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsWithThis( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, + arguments = ::withThisStaticsBeforeAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticsWithThis( + method: KFunction5, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, T4, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, T4::class, + arguments = ::withThisStaticsBeforeAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkParamsMutationsAndResult( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withParamsMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkParamsMutationsAndResult( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T1, T2, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withParamsMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkParamsMutationsAndResult( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T1, T2, T3, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withParamsMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkParamsMutationsAndResult( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, T1, T2, T3, T4, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withParamsMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // checks mutations in the parameters + protected inline fun checkParamsMutations( + method: KFunction2<*, T, *>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withParamsMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkParamsMutations( + method: KFunction3<*, T1, T2, *>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T1, T2) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withParamsMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkParamsMutations( + method: KFunction4<*, T1, T2, T3, *>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T1, T2, T3) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withParamsMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkParamsMutations( + method: KFunction5<*, T1, T2, T3, T4, *>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, T1, T2, T3, T4) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withParamsMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // checks mutations in the parameters and statics for static method + protected fun checkStaticMethodMutation( + method: KFunction0<*>, + branches: ExecutionsNumberMatcher, + vararg matchers: (StaticsType, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodMutation( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, T, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodMutation( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, StaticsType, T1, T2, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodMutation( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, StaticsType, T1, T2, T3, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodMutation( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, StaticsType, T1, T2, T3, T4, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodMutationAndResult( + method: KFunction0, + branches: ExecutionsNumberMatcher, + vararg matchers: (StaticsType, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodMutationAndResult( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, T, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodMutationAndResult( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, StaticsType, T1, T2, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodMutationAndResult( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, StaticsType, T1, T2, T3, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkStaticMethodMutationAndResult( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, StaticsType, T1, T2, T3, T4, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + + // checks mutations in the parameters and statics + protected inline fun checkMutations( + method: KFunction2<*, T, *>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, T, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMutations( + method: KFunction3<*, T1, T2, *>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, StaticsType, T1, T2, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMutations( + method: KFunction4<*, T1, T2, T3, *>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, StaticsType, T1, T2, T3, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMutations( + method: KFunction5<*, T1, T2, T3, T4, *>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, StaticsType, T1, T2, T3, T4, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withMutations, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // checks mutations in the parameters and statics + protected inline fun checkMutationsAndResult( + method: KFunction1<*, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (StaticsType, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMutationsAndResult( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, T, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMutationsAndResult( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, StaticsType, T1, T2, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMutationsAndResult( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, StaticsType, T1, T2, T3, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkMutationsAndResult( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, StaticsType, T1, T2, T3, T4, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + arguments = ::withMutationsAndResult, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + // checks mutations in this, parameters and statics + protected inline fun checkAllMutationsWithThis( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, T, StaticsType, R) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withMutationsAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkAllMutationsWithThis( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, StaticsType, T, T1, StaticsType, R) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, + arguments = ::withMutationsAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkAllMutationsWithThis( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, StaticsType, T, T1, T2, StaticsType, R) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, + arguments = ::withMutationsAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkAllMutationsWithThis( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, StaticsType, T, T1, T2, T3, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, + arguments = ::withMutationsAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkAllMutationsWithThis( + method: KFunction5, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, T4, StaticsType, T, T1, T2, T3, T4, StaticsType) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, T4::class, + arguments = ::withMutationsAndThis, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + //region checks substituting statics with symbolic variable or not + protected inline fun checkWithoutStaticsSubstitution( + method: KFunction1<*, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithoutStaticsSubstitution( + method: KFunction2<*, T, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithoutStaticsSubstitution( + method: KFunction3<*, T1, T2, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithoutStaticsSubstitution( + method: KFunction4<*, T1, T2, T3, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + protected inline fun checkWithoutStaticsSubstitution( + method: KFunction5<*, T1, T2, T3, T4, R>, + branches: ExecutionsNumberMatcher, + vararg matchers: (T1, T2, T3, T4, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, + additionalDependencies = additionalDependencies, + additionalMockAlwaysClasses = additionalMockAlwaysClasses + ) + + //endregion + + /** + * @param method method under test + * @param generateWithNested a flag indicating if we need to generate nested test classes + * or just generate one top-level test class + * @see [ClassWithStaticAndInnerClassesTest] + */ + fun checkAllCombinations( + method: KFunction<*>, + generateWithNested: Boolean = false, + additionalMockAlwaysClasses: Set = emptySet() + ) { + val failed = mutableListOf() + val succeeded = mutableListOf() + + allTestInfrastructureConfigurations + .filterNot { it.isDisabled } + .forEach { config -> + runCatching { + internalCheckForCodeGeneration(method, config, generateWithNested, additionalMockAlwaysClasses) + }.onFailure { + failed += config + }.onSuccess { + succeeded += config + } + } + + // TODO check that all generated classes have different content JIRA:1415 + + logger.info { "Total configurations: ${succeeded.size + failed.size}. Failed: ${failed.size}." } + require(failed.isEmpty()) { + val separator = System.lineSeparator() + val failedConfigurations = failed.joinToString(prefix = separator, separator = separator) + logger.error { "Failed configurations: $failedConfigurations" } + "Failed configurations: $failedConfigurations" + } + } + + @Suppress("ControlFlowWithEmptyBody", "UNUSED_VARIABLE") + private fun internalCheckForCodeGeneration( + method: KFunction<*>, + testInfrastructureConfiguration: TestInfrastructureConfiguration, + generateWithNested: Boolean, + additionalMockAlwaysClasses: Set = emptySet() + ) { + withSettingsFromTestFrameworkConfiguration(testInfrastructureConfiguration) { + with(testInfrastructureConfiguration) { + + val executableId = method.executableId + computeAdditionalDependenciesClasspathAndBuildDir(method.declaringClazz, emptyArray()) + val utContext = UtContext(method.declaringClazz.classLoader) + + clearTempDirectory(daysLimitForTempFiles) + + withUtContext(utContext) { + val methodWithStrategy = + MethodWithMockStrategy(executableId, mockStrategy, resetNonFinalFieldsAfterClinit) + + val (testSet, coverage) = analyzedMethods.getOrPut(methodWithStrategy) { + walk(executableId, mockStrategy, additionalMockAlwaysClasses = additionalMockAlwaysClasses) + } + + // if force mocking took place in parametrized test generation, + // we do not need to process this [testSet] + if (TestCodeGeneratorPipeline.currentTestInfrastructureConfiguration.isParametrizedAndMocked) { + conflictTriggers.reset(Conflict.ForceMockHappened, Conflict.ForceStaticMockHappened) + return + } + + val methodUnderTestOwnerId = testSet.method.classId + val classUnderTest = if (generateWithNested) { + generateSequence(methodUnderTestOwnerId) { clazz -> clazz.enclosingClass }.last().kClass + } else { + methodUnderTestOwnerId.kClass + } + + val stageStatusCheck = StageStatusCheck( + firstStage = CodeGeneration, + lastStage = TestExecution, + status = ExecutionStatus.SUCCESS + ) + val classStages = ClassStages(classUnderTest, stageStatusCheck, listOf(testSet)) + + TestCodeGeneratorPipeline(testInfrastructureConfiguration, applicationContext).runClassesCodeGenerationTests(classStages) + } + } + } + } + + inline fun internalCheck( + method: KFunction, + mockStrategy: MockStrategyApi, + branches: ExecutionsNumberMatcher, + matchers: Array>, + coverageMatcher: CoverageMatcher, + vararg classes: KClass<*>, + noinline arguments: (UtValueExecution<*>) -> List = ::withResult, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ) { + if (UtSettings.checkAllCombinationsForEveryTestInSamples) { + checkAllCombinations(method) + } + + val executableId = method.executableId + + withUtContext(UtContext(method.declaringClazz.classLoader)) { + val additionalDependenciesClassPath = + computeAdditionalDependenciesClasspathAndBuildDir(executableId.classId.jClass, additionalDependencies) + + val (testSet, coverage) = if (coverageMatcher is DoNotCalculate) { + val testSet = executions( + executableId, + mockStrategy, + additionalDependenciesClassPath, + additionalMockAlwaysClasses + ) + + MethodResult(testSet, Coverage()) + } else { + walk(executableId, mockStrategy, additionalDependenciesClassPath, additionalMockAlwaysClasses) + } + listOf(testSet).summarizeAll(searchDirectory, sourceFile = null) + val valueTestCase = testSet.toValueTestCase() + + assertTrue(testSet.errors.isEmpty()) { + "We have errors: ${ + testSet.errors.entries.map { "${it.value}: ${it.key}" }.prettify() + }" + } + + // if force mocking took place in parametrized test generation, + // we do not need to process this [testSet] + if (TestCodeGeneratorPipeline.currentTestInfrastructureConfiguration.isParametrizedAndMocked) { + conflictTriggers.reset(Conflict.ForceMockHappened, Conflict.ForceStaticMockHappened) + return + } + + val valueExecutions = valueTestCase.executions + assertTrue(branches(valueExecutions.size)) { + "Branch count matcher '$branches' fails for ${valueExecutions.size}: ${valueExecutions.prettify()}" + } + + valueExecutions.checkTypes(R::class, classes.toList()) + + valueExecutions.checkMatchers(matchers, arguments) + assertTrue(coverageMatcher(coverage)) { + "Coverage matcher '$coverageMatcher' fails for $coverage (at least: ${coverage.toAtLeast()})" + } + + processTestSet(testSet) + } + } + + fun List>.checkTypes( + resultType: KClass<*>, + argumentTypes: List> + ) { + for (execution in this) { + val typesWithArgs = argumentTypes.zip(execution.stateBefore.params.map { it.type }) + + assertTrue(typesWithArgs.all { it.second::class.isInstance(it.first::class) }) { "Param types do not match" } + + execution.returnValue.getOrNull()?.let { returnValue -> + assertTrue(resultType::class.isInstance(returnValue::class)) { "Return type does not match" } + } + } + } + + fun List>.checkMatchers( + matchers: Array>, + arguments: (UtValueExecution<*>) -> List + ) { + val notMatched = matchers.indices.filter { i -> + this.none { ex -> + runCatching { invokeMatcher(matchers[i], arguments(ex)) }.getOrDefault(false) + } + } + + val matchersNumbers = notMatched.map { it + 1 } + + assertTrue(notMatched.isEmpty()) { "Execution matchers $matchersNumbers not match to ${this.prettify()}" } + + // Uncomment if you want to check that each value matches at least one matcher. +// val notMatchedValues = this.filter { ex -> +// matchers.none { matcher -> +// runCatching { +// invokeMatcher(matcher, arguments(ex)) +// }.getOrDefault(false) +// } +// } +// assertTrue(notMatchedValues.isEmpty()) { "Values not match to matchers, ${notMatchedValues.prettify()}" } + } + + fun walk( + method: ExecutableId, + mockStrategy: MockStrategyApi, + additionalDependenciesClassPath: String = "", + additionalMockAlwaysClasses: Set = emptySet() + ): MethodResult { + val testSet = executions(method, mockStrategy, additionalDependenciesClassPath, additionalMockAlwaysClasses) + val methodCoverage = methodCoverage( + method, + testSet.toValueTestCase().executions, + buildDir.toString() + File.pathSeparator + additionalDependenciesClassPath + ) + return MethodResult(testSet, methodCoverage) + } + + open fun executions( + method: ExecutableId, + mockStrategy: MockStrategyApi, + additionalDependenciesClassPath: String, + additionalMockAlwaysClasses: Set = emptySet() + ): UtMethodTestSet { + val buildInfo = CodeGenerationIntegrationTest.Companion.BuildInfo(buildDir, additionalDependenciesClassPath) + + val testCaseGenerator = createTestCaseGenerator(buildInfo) + return testCaseGenerator.generate(method, mockStrategy, additionalMockAlwaysClasses) + } + + // factory method + open fun createTestCaseGenerator(buildInfo: CodeGenerationIntegrationTest.Companion.BuildInfo) = + testCaseGeneratorCache.getOrPut(buildInfo) { + TestSpecificTestCaseGenerator( + buildInfo.buildDir, + buildInfo.dependencyPath, + System.getProperty("java.class.path"), + applicationContext = applicationContext + ) + } + + fun executionsModel( + method: ExecutableId, + mockStrategy: MockStrategyApi, + additionalDependencies: Array> = emptyArray(), + additionalMockAlwaysClasses: Set = emptySet() + ): UtMethodTestSet { + val additionalDependenciesClassPath = + computeAdditionalDependenciesClasspathAndBuildDir(method.classId.jClass, additionalDependencies) + withUtContext(UtContext(method.classId.jClass.classLoader)) { + val buildInfo = CodeGenerationIntegrationTest.Companion.BuildInfo(buildDir, additionalDependenciesClassPath) + val testCaseGenerator = createTestCaseGenerator(buildInfo) + return testCaseGenerator.generate(method, mockStrategy, additionalMockAlwaysClasses) + } + } + + companion object { + private var previousClassLocation: ClassLocation? = null + private lateinit var buildDir: Path + + fun computeAdditionalDependenciesClasspathAndBuildDir( + clazz: Class<*>, + additionalDependencies: Array> + ): String { + val additionalDependenciesClassPath = additionalDependencies + .map { locateClass(it) } + .joinToString(File.pathSeparator) { findPathToClassFiles(it).toAbsolutePath().toString() } + val classLocation = locateClass(clazz) + if (classLocation != previousClassLocation) { + buildDir = findPathToClassFiles(classLocation) + previousClassLocation = classLocation + } + return additionalDependenciesClassPath + } + } + + data class MethodWithMockStrategy( + val method: ExecutableId, + val mockStrategy: MockStrategyApi, + val substituteStatics: Boolean + ) + + data class MethodResult(val testSet: UtMethodTestSet, val coverage: Coverage) +} + +@Suppress("UNCHECKED_CAST") +// TODO please use matcher.reflect().call(...) when it will be ready, currently call isn't supported in kotlin reflect +fun invokeMatcher(matcher: Function, params: List) = when (matcher) { + is Function1<*, *> -> (matcher as Function1).invoke(params[0]) + is Function2<*, *, *> -> (matcher as Function2).invoke(params[0], params[1]) + is Function3<*, *, *, *> -> (matcher as Function3).invoke( + params[0], params[1], params[2] + ) + is Function4<*, *, *, *, *> -> (matcher as Function4).invoke( + params[0], params[1], params[2], params[3] + ) + is Function5<*, *, *, *, *, *> -> (matcher as Function5).invoke( + params[0], params[1], params[2], params[3], params[4], + ) + is Function6<*, *, *, *, *, *, *> -> (matcher as Function6).invoke( + params[0], params[1], params[2], params[3], params[4], params[5], + ) + is Function7<*, *, *, *, *, *, *, *> -> (matcher as Function7).invoke( + params[0], params[1], params[2], params[3], params[4], params[5], params[6], + ) + else -> error("Function with arity > 7 not supported") +} + +fun between(bounds: IntRange) = ExecutionsNumberMatcher("$bounds") { it in bounds } +val ignoreExecutionsNumber = ExecutionsNumberMatcher("Do not calculate") { it > 0 } + +fun atLeast(percents: Int) = AtLeast(percents) + +fun signatureOf(function: Function<*>): String { + function as KFunction + return function.executableId.signature +} + +fun MockInfo.mocksMethod(method: Function<*>) = signatureOf(method) == this.method.signature + +fun List.singleMockOrNull(field: String, method: Function<*>): MockInfo? = + singleOrNull { (it.mock as? FieldMockTarget)?.field == field && it.mocksMethod(method) } + +fun List.singleMock(field: String, method: Function<*>): MockInfo = + single { (it.mock as? FieldMockTarget)?.field == field && it.mocksMethod(method) } + +fun List.singleMock(id: MockId, method: Function<*>): MockInfo = + single { (it.mock as? ObjectMockTarget)?.id == id && it.mocksMethod(method) } + +inline fun MockInfo.value(index: Int = 0): T = + when (val value = (values[index] as? UtConcreteValue<*>)?.value) { + is T -> value + else -> error("Unsupported type: ${values[index]}") + } + +fun MockInfo.mockValue(index: Int = 0): UtMockValue = values[index] as UtMockValue + +fun MockInfo.isParameter(num: Int): Boolean = (mock as? ParameterMockTarget)?.index == num + +inline fun Result<*>.isException(): Boolean = exceptionOrNull() is T + +inline fun UtCompositeModel.mockValues(methodName: String): List = + mocks.filterKeys { it.name == methodName }.values.flatten().map { it.primitiveValue() } + +inline fun UtModel.primitiveValue(): T = + (this as? UtPrimitiveModel)?.value as? T ?: error("Can't transform $this to ${T::class}") + +sealed class CoverageMatcher(private val description: String, private val cmp: (Coverage) -> Boolean) { + operator fun invoke(c: Coverage) = cmp(c) + override fun toString() = description +} + +object Full : CoverageMatcher("full coverage", { it.counters.all { it.total == it.covered } }) + +class AtLeast(percents: Int) : CoverageMatcher("at least $percents% coverage", + { it.counters.all { 100 * it.covered >= percents * it.total } }) + +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 withStaticsBeforeAndExceptions(ex: UtValueExecution<*>) = ex.paramsBefore + ex.staticsBefore + ex.returnValue +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 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<*>) = + ex.paramsBefore + listOf(ex.mocks) + listOf(ex.instrumentation) + ex.evaluatedResult + +fun withThisMocksAndExceptions(ex: UtValueExecution<*>) = + listOf(ex.callerBefore) + ex.paramsBefore + listOf(ex.mocks) + ex.returnValue + +fun withMocksInstrumentationAndThis(ex: UtValueExecution<*>) = + listOf(ex.callerBefore) + ex.paramsBefore + listOf(ex.mocks) + listOf(ex.instrumentation) + ex.evaluatedResult + +// mutations +fun withParamsMutations(ex: UtValueExecution<*>) = ex.paramsBefore + ex.paramsAfter +fun withMutations(ex: UtValueExecution<*>) = ex.paramsBefore + ex.staticsBefore + ex.paramsAfter + ex.staticsAfter +fun withParamsMutationsAndResult(ex: UtValueExecution<*>) = ex.paramsBefore + ex.paramsAfter + ex.evaluatedResult +fun withMutationsAndResult(ex: UtValueExecution<*>) = + ex.paramsBefore + ex.staticsBefore + ex.paramsAfter + ex.staticsAfter + ex.evaluatedResult + +fun withMutationsAndThis(ex: UtValueExecution<*>) = + mutableListOf().apply { + add(ex.callerBefore) + addAll(ex.paramsBefore) + add(ex.staticsBefore) + + add(ex.callerAfter) + addAll(ex.paramsAfter) + add(ex.staticsAfter) + + add(ex.evaluatedResult) + } + +private val UtValueExecution<*>.callerBefore get() = stateBefore.caller!!.value +private val UtValueExecution<*>.paramsBefore get() = stateBefore.params.map { it.value } +private val UtValueExecution<*>.staticsBefore get() = stateBefore.statics + +private val UtValueExecution<*>.callerAfter get() = stateAfter.caller!!.value +private val UtValueExecution<*>.paramsAfter get() = stateAfter.params.map { it.value } +private val UtValueExecution<*>.staticsAfter get() = stateAfter.statics + +private val UtValueExecution<*>.evaluatedResult get() = returnValue.getOrNull() + +fun Map>.findByName(name: String) = entries.single { name == it.key.name }.value.value +fun Map>.singleValue() = values.single().value + +typealias StaticsType = Map> +private typealias Mocks = List +private typealias Instrumentation = List + +inline fun withSettingsFromTestFrameworkConfiguration( + config: TestInfrastructureConfiguration, + block: () -> T +): T { + val substituteStaticsWithSymbolicVariable = UtSettings.substituteStaticsWithSymbolicVariable + UtSettings.substituteStaticsWithSymbolicVariable = config.resetNonFinalFieldsAfterClinit + + val previousConfig = TestCodeGeneratorPipeline.currentTestInfrastructureConfiguration + TestCodeGeneratorPipeline.currentTestInfrastructureConfiguration = config + try { + return block() + } finally { + UtSettings.substituteStaticsWithSymbolicVariable = substituteStaticsWithSymbolicVariable + TestCodeGeneratorPipeline.currentTestInfrastructureConfiguration = previousConfig + } +} + +/** + * Avoid conflict with java.util.stream.Stream.toList (available since Java 16 only) + */ +fun Stream.asList(): List = collect(Collectors.toList()) diff --git a/utbot-testing/src/main/kotlin/org/utbot/testing/UtValueTestCaseCheckerForTaint.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/UtValueTestCaseCheckerForTaint.kt new file mode 100644 index 0000000000..050b26b3af --- /dev/null +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/UtValueTestCaseCheckerForTaint.kt @@ -0,0 +1,37 @@ +package org.utbot.testing + +import org.junit.jupiter.api.AfterAll +import org.utbot.framework.UtSettings +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.plugin.api.* +import org.utbot.taint.TaintConfigurationProvider +import kotlin.reflect.KClass + +open class UtValueTestCaseCheckerForTaint( + testClass: KClass<*>, + testCodeGeneration: Boolean = true, + pipelines: List = listOf( + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.DO_NOT_PARAMETRIZE, Compilation), + Configuration(CodegenLanguage.JAVA, ParametrizedTestSource.PARAMETRIZE, Compilation), + Configuration(CodegenLanguage.KOTLIN, ParametrizedTestSource.DO_NOT_PARAMETRIZE, CodeGeneration), + ), + private val taintConfigurationProvider: TaintConfigurationProvider, +) : UtValueTestCaseChecker(testClass, testCodeGeneration, pipelines) { + + init { + UtSettings.useTaintAnalysis = true + } + + override fun createTestCaseGenerator(buildInfo: CodeGenerationIntegrationTest.Companion.BuildInfo) = + TestSpecificTestCaseGenerator( + buildInfo.buildDir, + buildInfo.dependencyPath, + System.getProperty("java.class.path"), + taintConfigurationProvider = taintConfigurationProvider, + ) + + @AfterAll + fun reset() { + UtSettings.useTaintAnalysis = false + } +} \ No newline at end of file diff --git a/utbot-testing/src/main/kotlin/org/utbot/testing/Utils.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/Utils.kt new file mode 100644 index 0000000000..60ced254e9 --- /dev/null +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/Utils.kt @@ -0,0 +1,11 @@ +package org.utbot.testing + +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.UtModel + +fun UtExecutionResult.getOrThrow(): UtModel = when (this) { + is UtExecutionSuccess -> model + is UtExecutionFailure -> throw exception +} diff --git a/utbot-ui-commons/build.gradle.kts b/utbot-ui-commons/build.gradle.kts new file mode 100644 index 0000000000..10ab87140d --- /dev/null +++ b/utbot-ui-commons/build.gradle.kts @@ -0,0 +1,79 @@ +val kotlinLoggingVersion: String by rootProject +val semVer: String? by rootProject +val slf4jVersion: String by rootProject + +// === IDE settings === +val projectType: String by rootProject +val communityEdition: String by rootProject +val ultimateEdition: String by rootProject + +val ideType: String by rootProject +val androidStudioPath: String? by rootProject + +val ideaVersion: String? by rootProject +val pycharmVersion: String? by rootProject +val golandVersion: String? by rootProject + +val javaIde: String? by rootProject +val pythonIde: String? by rootProject +val jsIde: String? by rootProject +val goIde: String? by rootProject + +val ideVersion = when(ideType) { + "PC", "PY" -> pycharmVersion + "GO" -> golandVersion + else -> ideaVersion +} + +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject +val goPluginVersion: String? by rootProject + +// https://plugins.jetbrains.com/docs/intellij/android-studio.html#configuring-the-plugin-pluginxml-file +val ideTypeOrAndroidStudio = if (androidStudioPath == null) ideType else "IC" + +project.tasks.asMap["runIde"]?.enabled = false +// === IDE settings === + +plugins { + id("org.jetbrains.intellij") version "1.13.1" +} + +intellij { + version.set(ideVersion) + type.set(ideType) +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + runIde { + jvmArgs("-Xmx2048m") + jvmArgs("--add-exports", "java.desktop/sun.awt.windows=ALL-UNNAMED") + } + + patchPluginXml { + sinceBuild.set("223") + untilBuild.set("232.*") + version.set(semVer) + } +} + +dependencies { + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation(group = "org.jetbrains", name = "annotations", version = "16.0.2") + implementation(project(":utbot-api")) + implementation(project(":utbot-framework")) + implementation(group = "org.slf4j", name = "slf4j-api", version = slf4jVersion) +} diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt new file mode 100644 index 0000000000..732feff717 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt @@ -0,0 +1,114 @@ +package org.utbot.intellij.plugin.language.agnostic + +import mu.KotlinLogging +import com.intellij.lang.Language +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileSystemItem +import com.intellij.psi.PsiManager + +private val logger = KotlinLogging.logger {} + +abstract class LanguageAssistant { + + abstract fun update(e: AnActionEvent) + abstract fun actionPerformed(e: AnActionEvent) + + companion object { + private val languages = mutableMapOf() + + fun get(e: AnActionEvent): LanguageAssistant? { + val project = e.project ?: return null + val editor = e.getData(CommonDataKeys.EDITOR) + if (editor != null) { + //The action is being called from editor + e.getData(CommonDataKeys.PSI_FILE)?.language?.let { language -> + updateLanguages(language) + return languages[language.id] + } ?: return null + } else { + // The action is being called from 'Project' tool window + val language = when (val element = e.getData(CommonDataKeys.PSI_ELEMENT)) { + is PsiFileSystemItem -> { + e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { + findLanguageRecursively(project, it) + } + } + is PsiElement -> { + element.containingFile?.let { getLanguageFromFile(it) } + } + else -> { + val someSelection = e.getData(PlatformDataKeys.PSI_ELEMENT_ARRAY)?: return null + someSelection.firstNotNullOfOrNull { + when(it) { + is PsiFileSystemItem -> findLanguageRecursively(project, arrayOf(it.virtualFile)) + is PsiElement -> it.language + else -> { null } + } + } + } + } ?: return null + + updateLanguages(language) + return languages[language.id] + } + } + + private fun updateLanguages(language: Language) { + if (!languages.containsKey(language.id)) { + loadWithException(language)?.let { + languages.put(language.id, it.kotlin.objectInstance as LanguageAssistant) + } + } + } + + private fun getLanguageFromFile(file: PsiFile): Language? { + updateLanguages(file.language) + return if (languages.containsKey(file.language.id)) + file.language + else + null + } + + private fun findLanguageRecursively(directory: PsiDirectory): Language? { + return directory.files + .firstNotNullOfOrNull { getLanguageFromFile(it) } ?: + directory.subdirectories.firstNotNullOfOrNull { findLanguageRecursively(it) } + } + + private fun findLanguageRecursively(project: Project, virtualFiles: Array): Language? { + val psiFiles = virtualFiles.mapNotNull { + PsiManager.getInstance(project).findFile(it) + } + val psiDirectories = virtualFiles.mapNotNull { + PsiManager.getInstance(project).findDirectory(it) + } + + val fileLanguage = psiFiles.firstNotNullOfOrNull { getLanguageFromFile(it) } + return fileLanguage ?: psiDirectories.firstNotNullOfOrNull { findLanguageRecursively(it) } + } + } +} + +private fun loadWithException(language: Language): Class<*>? { + try { + return when (language.id) { + "Python" -> Class.forName("org.utbot.intellij.plugin.python.language.PythonLanguageAssistant") + "ECMAScript 6" -> Class.forName("org.utbot.intellij.plugin.js.language.JsLanguageAssistant") + "go" -> Class.forName("org.utbot.intellij.plugin.go.language.GoLanguageAssistant") + "JAVA", "kotlin" -> Class.forName("org.utbot.intellij.plugin.language.JvmLanguageAssistant") + else -> error("Unknown language id: ${language.id}") + } + } catch (e: ClassNotFoundException) { + logger.info("Language ${language.id} is disabled") + } catch (e: IllegalStateException) { + logger.info("Language ${language.id} is not supported") + } + return null +} \ No newline at end of file diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt new file mode 100644 index 0000000000..c6d22c4530 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt @@ -0,0 +1,17 @@ +package org.utbot.intellij.plugin.models + +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import org.utbot.framework.codegen.domain.ProjectType +import org.utbot.framework.plugin.api.CodegenLanguage + + +open class BaseTestsModel( + val project: Project, +) { + var testSourceRoot: VirtualFile? = null + var testPackageName: String? = null + open var sourceRootHistory : MutableList = mutableListOf() + open lateinit var codegenLanguage: CodegenLanguage + open lateinit var projectType: ProjectType +} diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/BaseSettingsWindow.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/BaseSettingsWindow.kt new file mode 100644 index 0000000000..8e95275aaf --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/BaseSettingsWindow.kt @@ -0,0 +1,85 @@ +package org.utbot.intellij.plugin.settings + +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogPanel +import com.intellij.ui.dsl.builder.* +import javax.swing.* +import kotlin.reflect.KClass +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.plugin.api.CodeGenerationSettingItem +import org.utbot.intellij.plugin.ui.components.CodeGenerationSettingItemRenderer + +class BaseSettingsWindow(val project: Project) { + private val settings = project.service() + + private lateinit var enableExperimentalLanguagesCheckBox: JCheckBox + + private fun Row.createCombo(loader: KClass<*>, values: Array<*>) { + comboBox(DefaultComboBoxModel(values)) + .bindItem( + getter = { settings.providerNameByServiceLoader(loader) }, + setter = { settings.setProviderByLoader(loader, it as CodeGenerationSettingItem) }, + ).component.renderer = CodeGenerationSettingItemRenderer() + } + + val panel: JPanel = panel { + group("Common Settings") { + row("Tests with exceptions:") { + createCombo(RuntimeExceptionTestsBehaviour::class, RuntimeExceptionTestsBehaviour.values()) + } + + row("Hanging test timeout:") { + spinner( + range = IntRange( + HangingTestsTimeout.MIN_TIMEOUT_MS.toInt(), + HangingTestsTimeout.MAX_TIMEOUT_MS.toInt() + ), + step = 50 + ).bindIntValue( + getter = { + settings.hangingTestsTimeout.timeoutMs + .coerceIn(HangingTestsTimeout.MIN_TIMEOUT_MS, HangingTestsTimeout.MAX_TIMEOUT_MS).toInt() + }, + setter = { + settings.hangingTestsTimeout = HangingTestsTimeout(it.toLong()) + } + ) + + label("milliseconds per method") + contextHelp( + "Set this timeout to define which test is \"hanging\". Increase it to test the " + + "time-consuming method or decrease if the execution speed is critical for you." + ) + } + + row { + enableExperimentalLanguagesCheckBox = checkBox("Experimental languages support") + .onApply { + settings.state.enableExperimentalLanguagesSupport = + enableExperimentalLanguagesCheckBox.isSelected + } + .onReset { + enableExperimentalLanguagesCheckBox.isSelected = + settings.experimentalLanguagesSupport == true + } + .onIsModified { enableExperimentalLanguagesCheckBox.isSelected xor settings.experimentalLanguagesSupport } + .component + contextHelp("Enable JavaScript and Python if IDE supports them") + }.bottomGap(BottomGap.MEDIUM) + } + } + + fun isModified(): Boolean { + return (panel as DialogPanel).isModified() + } + + fun apply() { + (panel as DialogPanel).apply() + } + + fun reset() { + (panel as DialogPanel).reset() + } +} diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/CommonSettings.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/CommonSettings.kt new file mode 100644 index 0000000000..e75e865f35 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/CommonSettings.kt @@ -0,0 +1,318 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package org.utbot.intellij.plugin.settings + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +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 java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths +import org.utbot.common.FileUtil +import org.utbot.engine.Mocker +import org.utbot.framework.UtSettings +import org.utbot.framework.codegen.domain.ForceStaticMocking +import org.utbot.framework.codegen.domain.HangingTestsTimeout +import org.utbot.framework.codegen.domain.Junit4 +import org.utbot.framework.codegen.domain.Junit5 +import org.utbot.framework.codegen.domain.MockitoStaticMocking +import org.utbot.framework.codegen.domain.NoStaticMocking +import org.utbot.framework.codegen.domain.ParametrizedTestSource +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.StaticsMocking +import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.TestNg +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodeGenerationSettingItem +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.JavaDocCommentStyle +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.framework.plugin.api.TreatOverflowAsError +import java.util.concurrent.CompletableFuture +import kotlin.reflect.KClass +import org.utbot.common.isWindows +import org.utbot.framework.SummariesGenerationType +import org.utbot.framework.codegen.domain.UnknownTestFramework +import org.utbot.framework.plugin.api.SpringTestType +import org.utbot.framework.plugin.api.isSummarizationCompatible +import org.utbot.framework.plugin.api.SpringProfileNames +import org.utbot.framework.plugin.api.SpringConfig + +@State( + name = "UtBotSettings", + storages = [Storage("utbot-settings.xml")] +) +@Service(Service.Level.PROJECT) +class Settings(val project: Project) : PersistentStateComponent { + data class State( + var sourceRootHistory: MutableList = mutableListOf(), + var codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem, + @OptionTag(converter = TestFrameworkConverter::class) + var testFramework: TestFramework = TestFramework.defaultItem, + var mockStrategy: MockStrategyApi = MockStrategyApi.defaultItem, + var mockFramework: MockFramework = MockFramework.defaultItem, + @OptionTag(converter = StaticsMockingConverter::class) + var staticsMocking: StaticsMocking = StaticsMocking.defaultItem, + var runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, + @OptionTag(converter = HangingTestsTimeoutConverter::class) + var hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), + var useTaintAnalysis: Boolean = false, + var runInspectionAfterTestGeneration: Boolean = true, + var forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem, + var treatOverflowAsError: TreatOverflowAsError = TreatOverflowAsError.defaultItem, + var parametrizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem, + var classesToMockAlways: Array = Mocker.defaultSuperClassesToMockAlwaysNames.toTypedArray(), + var springTestType: SpringTestType = SpringTestType.defaultItem, + var springConfig: String = SpringConfig.defaultItem, + var springProfileNames: String = SpringProfileNames.defaultItem, + var fuzzingValue: Double = 0.05, + var runGeneratedTestsWithCoverage: Boolean = false, + var commentStyle: JavaDocCommentStyle = JavaDocCommentStyle.defaultItem, + var summariesGenerationType: SummariesGenerationType = UtSettings.summaryGenerationType, + var generationTimeoutInMillis: Long = UtSettings.utBotGenerationTimeoutInMillis, + var enableExperimentalLanguagesSupport: Boolean = true, + var isSpringHandled: Boolean = false, + ) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as State + + if (sourceRootHistory != other.sourceRootHistory) return false + if (codegenLanguage != other.codegenLanguage) return false + if (testFramework != other.testFramework) return false + if (mockStrategy != other.mockStrategy) return false + if (mockFramework != other.mockFramework) return false + if (staticsMocking != other.staticsMocking) return false + if (runtimeExceptionTestsBehaviour != other.runtimeExceptionTestsBehaviour) return false + if (hangingTestsTimeout != other.hangingTestsTimeout) return false + if (useTaintAnalysis != other.useTaintAnalysis) return false + if (runInspectionAfterTestGeneration != other.runInspectionAfterTestGeneration) return false + if (forceStaticMocking != other.forceStaticMocking) return false + if (treatOverflowAsError != other.treatOverflowAsError) return false + if (parametrizedTestSource != other.parametrizedTestSource) return false + if (!classesToMockAlways.contentEquals(other.classesToMockAlways)) return false + if (springTestType != other.springTestType) return false + if (springConfig != other.springConfig) return false + if (springProfileNames != other.springProfileNames) return false + if (fuzzingValue != other.fuzzingValue) return false + if (runGeneratedTestsWithCoverage != other.runGeneratedTestsWithCoverage) return false + if (commentStyle != other.commentStyle) return false + if (summariesGenerationType != other.summariesGenerationType) return false + if (generationTimeoutInMillis != other.generationTimeoutInMillis) return false + + return true + } + override fun hashCode(): Int { + var result = sourceRootHistory.hashCode() + result = 31 * result + codegenLanguage.hashCode() + result = 31 * result + testFramework.hashCode() + result = 31 * result + mockStrategy.hashCode() + result = 31 * result + mockFramework.hashCode() + result = 31 * result + staticsMocking.hashCode() + result = 31 * result + runtimeExceptionTestsBehaviour.hashCode() + result = 31 * result + hangingTestsTimeout.hashCode() + result = 31 * result + useTaintAnalysis.hashCode() + result = 31 * result + runInspectionAfterTestGeneration.hashCode() + result = 31 * result + forceStaticMocking.hashCode() + result = 31 * result + treatOverflowAsError.hashCode() + result = 31 * result + parametrizedTestSource.hashCode() + result = 31 * result + classesToMockAlways.contentHashCode() + result = 31 * result + springTestType.hashCode() + result = 31 * result + springConfig.hashCode() + result = 31 * result + springProfileNames.hashCode() + result = 31 * result + fuzzingValue.hashCode() + result = 31 * result + if (runGeneratedTestsWithCoverage) 1 else 0 + result = 31 * result + summariesGenerationType.hashCode() + result = 31 * result + generationTimeoutInMillis.hashCode() + + return result + } + } + + private var state = State() + val sourceRootHistory: MutableList get() = state.sourceRootHistory + + val codegenLanguage: CodegenLanguage get() = state.codegenLanguage + + val testFramework: TestFramework get() = state.testFramework + + val mockStrategy: MockStrategyApi get() = state.mockStrategy + + val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour get() = state.runtimeExceptionTestsBehaviour + + var hangingTestsTimeout: HangingTestsTimeout + get() = state.hangingTestsTimeout + set(value) { + state.hangingTestsTimeout = value + } + + var generationTimeoutInMillis : Long + get() = state.generationTimeoutInMillis + set(value) { + state.generationTimeoutInMillis = value + } + + val staticsMocking: StaticsMocking get() = state.staticsMocking + + val useTaintAnalysis: Boolean get() = state.useTaintAnalysis + + val runInspectionAfterTestGeneration: Boolean get() = state.runInspectionAfterTestGeneration + + val forceStaticMocking: ForceStaticMocking get() = state.forceStaticMocking + + val experimentalLanguagesSupport: Boolean get () = state.enableExperimentalLanguagesSupport + + val treatOverflowAsError: TreatOverflowAsError get() = state.treatOverflowAsError + + val parametrizedTestSource: ParametrizedTestSource get() = state.parametrizedTestSource + + val classesToMockAlways: Set get() = state.classesToMockAlways.toSet() + + val springTestType: SpringTestType get() = state.springTestType + + val springConfig: String get() = state.springConfig + + val springProfileNames: String get() = state.springProfileNames + + val javaDocCommentStyle: JavaDocCommentStyle get() = state.commentStyle + + var fuzzingValue: Double + get() = state.fuzzingValue + set(value) { + state.fuzzingValue = value.coerceIn(0.0, 1.0) + } + var runGeneratedTestsWithCoverage = state.runGeneratedTestsWithCoverage + + var enableSummariesGeneration = state.summariesGenerationType + + /** + * Defaults in Spring are slightly different, so for every Spring project we update settings, but only + * do it once so user is not stuck with defaults, hence this flag is needed to avoid repeated updates. + */ + var isSpringHandled: Boolean + get() = state.isSpringHandled + set(value) { + state.isSpringHandled = value + } + + fun setClassesToMockAlways(classesToMockAlways: List) { + state.classesToMockAlways = classesToMockAlways.distinct().toTypedArray() + } + + override fun getState(): State = state + + override fun initializeComponent() { + super.initializeComponent() + CompletableFuture.runAsync { + FileUtil.clearTempDirectory(UtSettings.daysLimitForTempFiles) + + // Don't replace file with custom user's settings + if (UtSettings.areCustomized()) return@runAsync + // In case settings.properties file is not yet presented + // (or stays with all default template values) in {homeDir}/.utbot folder + // we copy (or re-write) it from plugin resource file + val settingsClass = javaClass + Paths.get(UtSettings.defaultSettingsPath()).toFile().apply { + try { + this.parentFile.apply { + if (this.mkdirs() && isWindows) Files.setAttribute(this.toPath(), "dos:hidden", true) + } + settingsClass.getResource("../../../../../settings.properties")?.let { + this.writeBytes(it.openStream().readBytes()) + } + } catch (ignored: IOException) { + } + } + } + } + + override fun loadState(state: State) { + this.state = state + if (!state.codegenLanguage.isSummarizationCompatible()) { + this.state.summariesGenerationType = SummariesGenerationType.NONE + } + } + + // these classes are all ref types so we can use only names here + fun chosenClassesToMockAlways(): Set = state.classesToMockAlways.mapTo(mutableSetOf()) { ClassId(it) } + + fun setProviderByLoader(loader: KClass<*>, provider: CodeGenerationSettingItem) = + when (loader) { + // TODO: service loaders for test generator and code generator are removed from settings temporarily +// TestGeneratorServiceLoader::class -> setGeneratorName(provider) +// CodeGeneratorServiceLoader::class -> setCodeGeneratorName(provider) + MockStrategyApi::class -> state.mockStrategy = provider as MockStrategyApi + CodegenLanguage::class -> state.codegenLanguage = provider as CodegenLanguage + RuntimeExceptionTestsBehaviour::class -> { + state.runtimeExceptionTestsBehaviour = provider as RuntimeExceptionTestsBehaviour + } + ForceStaticMocking::class -> state.forceStaticMocking = provider as ForceStaticMocking + TreatOverflowAsError::class -> { + // TODO: SAT-1566 + state.treatOverflowAsError = provider as TreatOverflowAsError + UtSettings.treatOverflowAsError = provider == TreatOverflowAsError.AS_ERROR + } + JavaDocCommentStyle::class -> state.commentStyle = provider as JavaDocCommentStyle + // TODO: add error processing + else -> error("Unknown class [$loader] to map value [$provider]") + } + + fun providerNameByServiceLoader(loader: KClass<*>): CodeGenerationSettingItem = + when (loader) { + // TODO: service loaders for test generator and code generator are removed from settings temporarily +// TestGeneratorServiceLoader::class -> generatorName +// CodeGeneratorServiceLoader::class -> codeGeneratorName + MockStrategyApi::class -> mockStrategy + CodegenLanguage::class -> codegenLanguage + RuntimeExceptionTestsBehaviour::class -> runtimeExceptionTestsBehaviour + ForceStaticMocking::class -> forceStaticMocking + TreatOverflowAsError::class -> treatOverflowAsError + JavaDocCommentStyle::class -> javaDocCommentStyle + // TODO: add error processing + else -> error("Unknown service loader: $loader") + } +} + +// use it to serialize testFramework in State +private class TestFrameworkConverter : Converter() { + override fun toString(value: TestFramework): String = value.id + + override fun fromString(value: String): TestFramework = when (value) { + Junit4.id -> Junit4 + Junit5.id -> Junit5 + TestNg.id -> TestNg + else -> UnknownTestFramework(value) + } +} + +// use it to serialize staticsMocking in State +private class StaticsMockingConverter : Converter() { + override fun toString(value: StaticsMocking): String = "$value" + + override fun fromString(value: String): StaticsMocking = when (value) { + NoStaticMocking.id -> NoStaticMocking + MockitoStaticMocking.id -> MockitoStaticMocking + else -> error("Unknown StaticsMocking $value") + } +} + +// TODO is it better to use kotlinx.serialization? +// use it to serialize hangingTestsTimeout in State +private class HangingTestsTimeoutConverter : Converter() { + override fun toString(value: HangingTestsTimeout): String = + "HangingTestsTimeout:${value.timeoutMs}" + + override fun fromString(value: String): HangingTestsTimeout { + val arguments = value.substringAfter("HangingTestsTimeout:") + val timeoutMs = arguments.toLong() + return HangingTestsTimeout(timeoutMs) + } +} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/Configurable.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/Configurable.kt similarity index 92% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/Configurable.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/Configurable.kt index 7a41ff805a..3c09b84ea2 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/Configurable.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/Configurable.kt @@ -7,7 +7,7 @@ import javax.swing.JComponent class Configurable(val project: Project) : SearchableConfigurable { private val displayName: String = "UtBot Configuration" private val id: String = "org.utbot.intellij.plugin.settings.UtBotSettingsConfigurable" - private val settingsWindow = SettingsWindow(project) + private val settingsWindow = BaseSettingsWindow(project) override fun createComponent(): JComponent = settingsWindow.panel diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/TestFrameworkMapper.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/TestFrameworkMapper.kt new file mode 100644 index 0000000000..79c290b82e --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/settings/TestFrameworkMapper.kt @@ -0,0 +1,9 @@ +package org.utbot.intellij.plugin.settings + +import org.utbot.framework.codegen.domain.TestFramework + +interface TestFrameworkMapper { + fun toString(value: TestFramework): String + fun fromString(value: String): TestFramework + fun handleUnknown(testFramework: TestFramework): TestFramework +} \ No newline at end of file diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt new file mode 100644 index 0000000000..be3623d853 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt @@ -0,0 +1,207 @@ +package org.utbot.intellij.plugin.ui + +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.actionSystem.AnAction +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.ProjectActivity +import com.intellij.openapi.wm.WindowManager +import com.intellij.ui.GotItTooltip +import javax.swing.event.HyperlinkEvent +import mu.KotlinLogging + +abstract class Notifier { + protected val logger = KotlinLogging.logger {} + + protected abstract val notificationType: NotificationType + protected abstract val displayId: String + protected open fun content(project: Project?, module: Module?, info: String): String = info + + open fun notify(info: String, project: Project? = null, module: Module? = null) { + notify(info, project, module, AnAction.EMPTY_ARRAY) + } + + open fun notify(info: String, project: Project? = null, module: Module? = null, actions: Array) { + notificationGroup + .createNotification(content(project, module, info), notificationType) + .apply { actions.forEach { this.addAction(it) } } + .notify(project) + } + + protected open val notificationDisplayType = NotificationDisplayType.BALLOON + + protected val notificationGroup: NotificationGroup + get() = NotificationGroup.findRegisteredGroup(displayId) ?: NotificationGroup(displayId, notificationDisplayType) +} + +abstract class WarningNotifier : Notifier() { + override val notificationType: NotificationType = NotificationType.WARNING + final override fun notify(info: String, project: Project?, module: Module?) { + super.notify(info, project, module) + } +} + +abstract class ErrorNotifier : Notifier() { + final override val notificationType: NotificationType = NotificationType.ERROR + + override fun notify(info: String, project: Project?, module: Module?) { + super.notify(info, project, module) + error(content(project, module, info)) + } +} + +object CommonErrorNotifier : ErrorNotifier() { + override val displayId: String = "UTBot plugin errors" +} + +class CommonLoggingNotifier(val type :NotificationType = NotificationType.WARNING) : Notifier() { + override val displayId: String = "UTBot plugin errors" + override val notificationType = type + + override fun notify(info: String, project: Project?, module: Module?) { + super.notify(info, project, module) + when (notificationType) { + NotificationType.WARNING -> logger.warn(content(project, module, info)) + NotificationType.INFORMATION -> logger.info(content(project, module, info)) + else -> logger.error(content(project, module, info)) + } + } +} + +object UnsupportedJdkNotifier : ErrorNotifier() { + override val displayId: String = "Unsupported JDK" + override fun content(project: Project?, module: Module?, info: String): String = + "JDK versions older than 8 are not supported. This project's JDK version is $info" +} + +object InvalidClassNotifier : WarningNotifier() { + override val displayId: String = "Invalid class" + override fun content(project: Project?, module: Module?, info: String): String = + "Generate tests with UnitTestBot for the $info is not supported." +} + +object MissingLibrariesNotifier : WarningNotifier() { + override val displayId: String = "Missing libraries" + override fun content(project: Project?, module: Module?, info: String): String = + "Library $info missing on the test classpath of module ${module?.name}" +} + +@Suppress("unused") +object UnsupportedTestFrameworkNotifier : ErrorNotifier() { + override val displayId: String = "Unsupported test framework" + override fun content(project: Project?, module: Module?, info: String): String = + "Test framework $info is not supported yet" +} + +abstract class UrlNotifier : Notifier() { + + protected abstract val titleText: String + protected abstract val urlOpeningListener: NotificationListener + + override fun notify(info: String, project: Project?, module: Module?) { + notificationGroup.createNotification(content(project, module, info), notificationType) + .setTitle(titleText).setListener(urlOpeningListener).notify(project) + } +} + +abstract class InformationUrlNotifier : UrlNotifier() { + override val notificationType: NotificationType = NotificationType.INFORMATION +} + +abstract class WarningUrlNotifier : UrlNotifier() { + override val notificationType: NotificationType = NotificationType.WARNING +} + +abstract class EventLogNotifier : InformationUrlNotifier() { + override val notificationDisplayType = NotificationDisplayType.NONE +} + +object SarifReportNotifier : EventLogNotifier() { + + override val displayId: String = "SARIF report" + + override val titleText: String = "" // no title + + override val urlOpeningListener: NotificationListener = NotificationListener.UrlOpeningListener(false) +} + +object TestsReportNotifier : InformationUrlNotifier() { + override val displayId: String = "Generated unit tests report" + + override val titleText: String = "UnitTestBot: unit tests generated successfully" + + public override val urlOpeningListener: TestReportUrlOpeningListener = TestReportUrlOpeningListener +} + +// TODO replace inheritance with decorators +object WarningTestsReportNotifier : WarningUrlNotifier() { + override val displayId: String = "Generated unit tests report" + + override val titleText: String = "UnitTestBot: unit tests generated with warnings" + + public override val urlOpeningListener: TestReportUrlOpeningListener = TestReportUrlOpeningListener +} + +object DetailsTestsReportNotifier : EventLogNotifier() { + override val displayId: String = "Test report details" + + override val titleText: String = "Test report details of the unit tests generation via UnitTestBot" + + public override val urlOpeningListener: TestReportUrlOpeningListener = TestReportUrlOpeningListener +} + +/** + * Listener that handles URLs starting with [prefix], like "#utbot/configure-mockito". + */ +object TestReportUrlOpeningListener: NotificationListener.Adapter() { + const val prefix = "#utbot/" + const val mockitoSuffix = "configure-mockito" + const val mockitoInlineSuffix = "mockito-inline" + const val eventLogSuffix = "event-log" + + val callbacks: Map Unit>> = hashMapOf( + Pair(mockitoSuffix, mutableListOf()), + Pair(mockitoInlineSuffix, mutableListOf()), + Pair(eventLogSuffix, mutableListOf()), + ) + + private val defaultListener = NotificationListener.UrlOpeningListener(false) + + 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) = + callbacks[descriptionSuffix]?.map { it() } ?: error("No such command with #utbot prefix: $descriptionSuffix") +} + +object GotItTooltipActivity : ProjectActivity { + private const val KEY = "UTBot.GotItMessageWasShown" + override suspend fun execute(project: Project) { + ApplicationManager.getApplication().invokeLater { + val shortcut = ActionManager.getInstance() + .getKeyboardShortcut("org.utbot.intellij.plugin.ui.actions.GenerateTestsAction")?:return@invokeLater + val shortcutText = KeymapUtil.getShortcutText(shortcut) + val message = GotItTooltip(KEY, + "
    You can get test coverage for methods,
    Java classes, and even for whole source roots
    with $shortcutText
    ") + .withHeader("UnitTestBot is ready!") + if (message.canShow()) { + WindowManager.getInstance().getFrame(project)?.rootPane?.let { + message.show(it, GotItTooltip.BOTTOM_LEFT) + } + } + } + } +} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt similarity index 88% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt index 24b6b0a195..48e46a1e3d 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt @@ -5,7 +5,7 @@ import javax.swing.DefaultListCellRenderer import javax.swing.JList import org.utbot.framework.plugin.api.CodeGenerationSettingItem -internal class CodeGenerationSettingItemRenderer : DefaultListCellRenderer() { +class CodeGenerationSettingItemRenderer : DefaultListCellRenderer() { override fun getListCellRendererComponent( list: JList<*>?, value: Any?, diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestSourceDirectoryChooser.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestSourceDirectoryChooser.kt new file mode 100644 index 0000000000..8876815f6c --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestSourceDirectoryChooser.kt @@ -0,0 +1,57 @@ +package org.utbot.intellij.plugin.ui.components + +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.ui.TextBrowseFolderListener +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.vfs.VirtualFile +import java.nio.file.Paths +import org.utbot.common.PathUtil.replaceSeparator +import org.utbot.intellij.plugin.models.BaseTestsModel + +class TestSourceDirectoryChooser( + val model: BaseTestsModel, + file: VirtualFile? = null +) : TextFieldWithBrowseButton() { + private val projectRoot = file + ?.let { getContentRoot(model.project, file) } + ?: model.project.guessProjectDir() + ?: error("Source file lies outside of a module") + + init { + val descriptor = FileChooserDescriptor( + false, + true, + false, + false, + false, + false + ) + descriptor.setRoots(projectRoot) + addBrowseFolderListener( + TextBrowseFolderListener(descriptor, model.project) + ) + text = replaceSeparator(Paths.get(projectRoot.path, defaultDirectory).toString()) + } + + fun validatePath(): ValidationInfo? { + val typedPath = Paths.get(text).toAbsolutePath() + return if (typedPath.startsWith(replaceSeparator(projectRoot.path))) { + defaultDirectory = Paths.get(projectRoot.path).relativize(typedPath).toString() + null + } else + ValidationInfo("Specified directory lies outside of the project", this) + } + + private fun getContentRoot(project: Project, file: VirtualFile): VirtualFile { + return ProjectFileIndex.getInstance(project) + .getContentRootForFile(file) ?: error("Source file lies outside of a module") + } + + companion object { + private var defaultDirectory = "utbot_tests" + } +} \ No newline at end of file diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/DialogWindowUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/DialogWindowUtils.kt new file mode 100644 index 0000000000..c462a1a7a6 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/DialogWindowUtils.kt @@ -0,0 +1,20 @@ +package org.utbot.intellij.plugin.ui.utils + +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.SimpleTextAttributes +import org.utbot.framework.codegen.domain.TestFramework +import javax.swing.JList + +fun createTestFrameworksRenderer(additionalText: String): ColoredListCellRenderer { + return object : ColoredListCellRenderer() { + override fun customizeCellRenderer( + list: JList, value: TestFramework, + index: Int, selected: Boolean, hasFocus: Boolean + ) { + this.append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES) + if (!value.isInstalled) { + this.append(additionalText, SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } +} diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt new file mode 100644 index 0000000000..05472409d9 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt @@ -0,0 +1,17 @@ +package org.utbot.intellij.plugin.ui.utils + +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages + +fun showErrorDialogLater(project: Project, message: String, title: String) { + invokeLater { + Messages.showErrorDialog(project, message, title) + } +} + +fun showWarningDialogLater(project: Project, message: String, title: String) { + invokeLater { + Messages.showWarningDialog(project, message, title) + } +} \ No newline at end of file diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt new file mode 100644 index 0000000000..98b891ac98 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt @@ -0,0 +1,75 @@ +package org.utbot.intellij.plugin.util + +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.project.Project +import com.intellij.util.PlatformUtils +import com.intellij.util.ReflectionUtil +import com.intellij.util.concurrency.AppExecutorUtil +import mu.KotlinLogging +import org.utbot.framework.CancellationStrategyType.* +import org.utbot.framework.UtSettings + +/** + * This object is required to encapsulate Android API usage and grant safe access to it. + */ +object IntelliJApiHelper { + private val logger = KotlinLogging.logger {} + enum class Target { THREAD_POOL, READ_ACTION, WRITE_ACTION, EDT_LATER } + + fun run(target: Target, indicator: ProgressIndicator? = null, logMessage : String, runnable: Runnable) { + logger.info { "[${target}]: " + logMessage + + if (indicator != null) ", indicator[${indicator.text}; ${(indicator.fraction * 100).toInt()}%]" else "" } + + if (indicator?.isCanceled == true) { + when (UtSettings.cancellationStrategyType) { + NONE, + SAVE_PROCESSED_RESULTS -> {} + CANCEL_EVERYTHING -> { + logger.info { "Indicator is already cancelled" } + return + } + } + } + + val wrapper = Runnable { + try { + runnable.run() + } catch (e: Exception) { + logger.error(e) { target.toString() } + throw e + } + } + when (target) { + Target.THREAD_POOL -> AppExecutorUtil.getAppExecutorService().submit { wrapper.run() } + Target.READ_ACTION -> runReadAction { wrapper.run() } + Target.WRITE_ACTION -> runWriteAction { wrapper.run() } + Target.EDT_LATER -> ApplicationManager.getApplication().invokeLater( wrapper, ModalityState.NON_MODAL ) + } + } + + private val isAndroidPluginAvailable: Boolean = + !PluginManagerCore.isDisabled(PluginId.getId("org.jetbrains.android")) + + fun isAndroidStudio(): Boolean = + isAndroidPluginAvailable && ("AndroidStudio" == PlatformUtils.getPlatformPrefix()) + + fun androidGradleSDK(project: Project): String? { + if (!isAndroidPluginAvailable) return null + try { + val finderClass = Class.forName("com.android.tools.idea.gradle.util.GradleProjectSettingsFinder") + var method = ReflectionUtil.getMethod(finderClass, "findGradleProjectSettings", Project::class.java) ?: return null + val gradleProjectSettings = method.invoke(project) ?: return null + method = ReflectionUtil.getMethod(gradleProjectSettings.javaClass, "getGradleJvm") ?: return null + val gradleJvm = method.invoke(gradleProjectSettings) + return if (gradleJvm is String) gradleJvm else null + } catch (e: Exception) { + return null + } + } +} diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/UtSettingsHelper.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/UtSettingsHelper.kt new file mode 100644 index 0000000000..3f6a400a5c --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/UtSettingsHelper.kt @@ -0,0 +1,31 @@ +package org.utbot.intellij.plugin.util + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VfsUtil +import java.io.File +import java.nio.charset.Charset +import org.utbot.framework.UtSettings + +fun showSettingsEditor(project: Project, key: String? = null) { + val ioFile = File(UtSettings.getPath()) + ApplicationManager.getApplication().executeOnPooledThread { + var logicalLine: Int = -1 + key?.let { + logicalLine = ioFile.readLines(Charset.defaultCharset()) + .indexOfFirst { s -> s.startsWith("$key=") or s.startsWith("#$key=") } + } + VfsUtil.findFileByIoFile(ioFile, true)?.let { + val descriptor = if (logicalLine != -1) { + OpenFileDescriptor(project, it, logicalLine, 0) + } else { + OpenFileDescriptor(project, it) + } + ApplicationManager.getApplication().invokeLater { + FileEditorManager.getInstance(project).openTextEditor(descriptor, true) + } + } + } +} diff --git a/utbot-usvm/build.gradle.kts b/utbot-usvm/build.gradle.kts new file mode 100644 index 0000000000..67c5b6d982 --- /dev/null +++ b/utbot-usvm/build.gradle.kts @@ -0,0 +1,74 @@ +val jacoDbVersion: String by rootProject +val usvmVersion: String by rootProject +val approximationsVersion: String by rootProject + +val approximationsRepo = "com.github.UnitTestBot.java-stdlib-approximations" +val usvmRepo = "com.github.UnitTestBot.usvm" + +repositories { + mavenCentral() + maven("https://jitpack.io") +} + +val approximations: Configuration by configurations.creating {} +val usvmApproximationsApi: Configuration by configurations.creating {} +val usvmInstrumentationCollector: Configuration by configurations.creating {} +val usvmInstrumentationRunner: Configuration by configurations.creating {} + +dependencies { + implementation(project(":utbot-framework-api")) + + implementation(group = "org.jacodb", name = "jacodb-core", version = jacoDbVersion) + implementation(group = "org.jacodb", name = "jacodb-analysis", version = jacoDbVersion) + implementation(group = "org.jacodb", name = "jacodb-approximations", version = jacoDbVersion) + + implementation(group = usvmRepo, name = "usvm-core", version = usvmVersion) + implementation(group = usvmRepo, name = "usvm-jvm", version = usvmVersion) + implementation(group = usvmRepo, name = "usvm-jvm-api", version = usvmVersion) + implementation(group = usvmRepo, name = "usvm-jvm-instrumentation", version = usvmVersion) + implementation(group = usvmRepo, name = "usvm-jvm-instrumentation-collectors", version = usvmVersion) + + approximations("$approximationsRepo:approximations:$approximationsVersion") + + usvmApproximationsApi("$usvmRepo:usvm-jvm-api:$usvmVersion") + usvmInstrumentationCollector("$usvmRepo:usvm-jvm-instrumentation-collectors:$usvmVersion") + usvmInstrumentationRunner("$usvmRepo:usvm-jvm-instrumentation:$usvmVersion") + usvmInstrumentationRunner("$usvmRepo:usvm-jvm-instrumentation-collectors:$usvmVersion") +} + +// TODO replace with runner from usvm (unavailable due to huge jar size) +val usvmInstrumentationRunnerJarTask by tasks.register("usvmInstrumentationRunnerJar") { + archiveBaseName.set("usvm-jvm-instrumentation-runner") + archiveVersion.set("") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + manifest { + attributes( + "Main-Class" to "org.usvm.instrumentation.rd.InstrumentedProcessKt", + "Premain-Class" to "org.usvm.instrumentation.agent.Agent", + "Can-Retransform-Classes" to "true", + "Can-Redefine-Classes" to "true" + ) + } + + from(configurations.named("usvmInstrumentationRunner").get().map { + if (it.isDirectory) it else zipTree(it) + }) +} + +tasks.processResources { + listOf( + approximations, + usvmApproximationsApi, + usvmInstrumentationCollector, + usvmInstrumentationRunnerJarTask, + ).forEach { source -> + from(source) { + into("lib") + rename { + it.replace("-$usvmVersion", "") + .replace("-$approximationsVersion", "") + } + } + } +} diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/ConverterUtils.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/ConverterUtils.kt new file mode 100644 index 0000000000..02d2840044 --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/ConverterUtils.kt @@ -0,0 +1,108 @@ +package org.utbot.usvm.converter + +import org.jacodb.analysis.library.analyzers.thisInstance +import org.jacodb.api.JcArrayType +import org.jacodb.api.JcClassOrInterface +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcField +import org.jacodb.api.JcMethod +import org.jacodb.api.JcPrimitiveType +import org.jacodb.api.JcRefType +import org.jacodb.api.JcType +import org.jacodb.api.TypeName +import org.jacodb.api.ext.boolean +import org.jacodb.api.ext.byte +import org.jacodb.api.ext.char +import org.jacodb.api.ext.double +import org.jacodb.api.ext.float +import org.jacodb.api.ext.int +import org.jacodb.api.ext.long +import org.jacodb.api.ext.short +import org.jacodb.api.ext.void +import org.jacodb.api.ext.toType +import org.usvm.instrumentation.testcase.api.UTestInst +import org.usvm.instrumentation.testcase.descriptor.UTestObjectDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestValueDescriptor +import org.usvm.instrumentation.util.getFieldByName +import org.usvm.instrumentation.util.toJavaClass +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.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.fieldId +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.utContext +import org.utbot.framework.plugin.api.util.voidClassId + +fun JcMethod.toExecutableId(classpath: JcClasspath): ExecutableId { + val type = this.thisInstance.type.classId + val parameters = this.parameters.map { it.type.findClassId(classpath) } + + if (isConstructor) { + return ConstructorId(type, parameters) + } + + val returnClassId = this.returnType.findClassId(classpath) + + return MethodId(type, this.name, returnClassId, parameters) +} + +private fun JcType.replaceToBoundIfGeneric(): JcType { + return when (this) { + is JcArrayType -> this.classpath.arrayTypeOf(elementType.replaceToBoundIfGeneric()) + is JcRefType -> this.jcClass.toType() + else -> this + } +} + +val JcType?.classId: ClassId + get() { + if (this !is JcPrimitiveType) { + return runCatching { + this + ?.replaceToBoundIfGeneric() + ?.toJavaClass(utContext.classLoader, initialize = false) + ?.id + ?: error("Can not construct classId for $this") + }.getOrElse { e -> + throw IllegalStateException("JcType.classId failed on ${this?.typeName}", e) + } + } + + val cp = this.classpath + return when (this) { + cp.boolean -> booleanClassId + cp.byte -> byteClassId + cp.short -> shortClassId + cp.int -> intClassId + cp.long -> longClassId + cp.float -> floatClassId + cp.double -> doubleClassId + cp.char -> charClassId + cp.void -> voidClassId + else -> error("$this is not a primitive type") + } + } + +val JcClassOrInterface.classId: ClassId + get() = this.toJavaClass(utContext.classLoader, initialize = false).id + +fun TypeName.findClassId(classpath: JcClasspath): ClassId = + classpath.findTypeOrNull(this.typeName)?.classId + ?: error("Can not construct classId for $this") + +val JcField.fieldId: FieldId + get() = enclosingClass.toType().toJavaClass(utContext.classLoader, initialize = false).getFieldByName(name)?.fieldId + ?: error("Can not construct fieldId for $this") + +val UTestValueDescriptor.origin: UTestInst? + get() = (this as? UTestObjectDescriptor)?.originUTestExpr \ No newline at end of file diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/InstructionIdProvider.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/InstructionIdProvider.kt new file mode 100644 index 0000000000..196728c769 --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/InstructionIdProvider.kt @@ -0,0 +1,12 @@ +package org.utbot.usvm.converter + +fun interface InstructionIdProvider { + fun provideInstructionId(methodSignature: String, instIndex: Int): Long +} + +class SimpleInstructionIdProvider : InstructionIdProvider { + private val instructionIds = mutableMapOf, Long>() + + override fun provideInstructionId(methodSignature: String, instIndex: Int): Long = + instructionIds.getOrPut(methodSignature to instIndex) { instructionIds.size.toLong() } +} \ No newline at end of file diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/JcToUtExecutionConverter.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/JcToUtExecutionConverter.kt new file mode 100644 index 0000000000..2b1392b7e7 --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/JcToUtExecutionConverter.kt @@ -0,0 +1,345 @@ +package org.utbot.usvm.converter + +import mu.KotlinLogging +import org.jacodb.api.JcClassOrInterface +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcTypedMethod +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.ext.jcdbSignature +import org.usvm.instrumentation.testcase.api.UTestExecutionExceptionResult +import org.usvm.instrumentation.testcase.api.UTestExecutionFailedResult +import org.usvm.instrumentation.testcase.api.UTestExecutionInitFailedResult +import org.usvm.instrumentation.testcase.api.UTestExecutionState +import org.usvm.instrumentation.testcase.api.UTestExecutionSuccessResult +import org.usvm.instrumentation.testcase.api.UTestExecutionTimedOutResult +import org.usvm.instrumentation.testcase.api.UTestSetStaticFieldStatement +import org.usvm.instrumentation.testcase.descriptor.Descriptor2ValueConverter +import org.usvm.instrumentation.testcase.descriptor.UTestExceptionDescriptor +import org.usvm.instrumentation.util.enclosingClass +import org.usvm.instrumentation.util.enclosingMethod +import org.utbot.common.isPublic +import org.utbot.framework.fuzzer.IdGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.Coverage +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.Instruction +import org.utbot.framework.plugin.api.MissingState +import org.utbot.framework.plugin.api.TimeoutException +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.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.UtImplicitlyThrownException +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.UtStatementCallModel +import org.utbot.framework.plugin.api.UtTimeoutException +import org.utbot.framework.plugin.api.UtVoidModel +import org.utbot.framework.plugin.api.mapper.UtModelDeepMapper +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.framework.utils.UtilMethodProvider +import org.utbot.usvm.jc.JcExecution +import org.utbot.usvm.jc.UTestConcreteExecutionResult +import org.utbot.usvm.jc.UTestResultWrapper +import org.utbot.usvm.jc.UTestSymbolicExceptionResult +import org.utbot.usvm.jc.UTestSymbolicSuccessResult +import java.util.IdentityHashMap + +private val logger = KotlinLogging.logger {} + +class JcToUtExecutionConverter( + private val jcExecution: JcExecution, + private val jcClasspath: JcClasspath, + private val idGenerator: IdGenerator, + private val instructionIdProvider: InstructionIdProvider, + private val utilMethodProvider: UtilMethodProvider, +) { + private val toValueConverter = Descriptor2ValueConverter(utContext.classLoader) + + private val instToModelConverter = UTestInstToUtModelConverter(jcExecution.uTest, jcClasspath, idGenerator, utilMethodProvider) + private var jcToUtModelConverter = JcToUtModelConverter(idGenerator, instToModelConverter) + private var uTestProcessResult = instToModelConverter.processUTest() + + fun convert() = jcExecution.uTestExecutionResultWrappers.firstNotNullOfOrNull { result -> + runCatching { convert(result) } + .onFailure { e -> + logger.warn(e) { + "Recoverable: JcToUtExecutionConverter.convert(${jcExecution.method.method}) " + + "failed for ${result::class.java}" + } + } + .getOrNull() + } ?: error("Failed to construct UtExecution for all uTestExecutionResultWrappers on ${jcExecution.method.method}") + + private fun convert(uTestResultWrapper: UTestResultWrapper): UtExecution? { + val coverage = convertCoverage(getTrace(uTestResultWrapper), jcExecution.method.enclosingType.jcClass) + + val utUsvmExecution: UtUsvmExecution = when (uTestResultWrapper) { + is UTestSymbolicExceptionResult -> { + UtUsvmExecution( + stateBefore = constructStateBeforeFromUTest(), + stateAfter = MissingState, + result = createExecutionFailureResult( + exceptionDescriptor = UTestExceptionDescriptor( + type = uTestResultWrapper.exceptionType, + message = "", + stackTrace = emptyList(), + raisedByUserCode = true, + ), + jcTypedMethod = jcExecution.method, + ), + coverage = coverage, + instrumentation = uTestProcessResult.instrumentation, + ) + } + + is UTestSymbolicSuccessResult -> { + uTestResultWrapper.initStatements.forEach { instToModelConverter.processInst(it) } + instToModelConverter.processInst(uTestResultWrapper.result) + + val resultUtModel = instToModelConverter.findModelByInst(uTestResultWrapper.result) + + UtUsvmExecution( + stateBefore = constructStateBeforeFromUTest(), + stateAfter = MissingState, + result = UtExecutionSuccess(resultUtModel), + coverage = coverage, + instrumentation = uTestProcessResult.instrumentation, + ) + } + + is UTestConcreteExecutionResult -> + when (val executionResult = uTestResultWrapper.uTestExecutionResult) { + is UTestExecutionSuccessResult -> UtUsvmExecution( + stateBefore = convertState(executionResult.initialState, EnvironmentStateKind.INITIAL, jcExecution.method), + stateAfter = convertState(executionResult.resultState, EnvironmentStateKind.FINAL, jcExecution.method), + // TODO usvm-sbft: ask why `UTestExecutionSuccessResult.result` is nullable + result = UtExecutionSuccess(executionResult.result?.let { + jcToUtModelConverter.convert(it, EnvironmentStateKind.FINAL) + } ?: UtVoidModel), + coverage = coverage, + instrumentation = uTestProcessResult.instrumentation, + ) + + is UTestExecutionExceptionResult -> { + UtUsvmExecution( + stateBefore = convertState(executionResult.initialState, EnvironmentStateKind.INITIAL, jcExecution.method), + stateAfter = convertState(executionResult.resultState, EnvironmentStateKind.FINAL, jcExecution.method), + result = createExecutionFailureResult(executionResult.cause, jcExecution.method), + coverage = coverage, + instrumentation = uTestProcessResult.instrumentation, + ) + } + + is UTestExecutionInitFailedResult -> { + logger.warn(convertException(executionResult.cause)) { + "Execution failed before method under test call on ${jcExecution.method.method}" + } + null + } + + is UTestExecutionFailedResult -> { + logger.error(convertException(executionResult.cause)) { + "Concrete execution failed on ${jcExecution.method.method}" + } + null + } + + is UTestExecutionTimedOutResult -> { + logger.warn { "Timeout on ${jcExecution.method.method}" } + UtUsvmExecution( + stateBefore = constructStateBeforeFromUTest(), + stateAfter = MissingState, + result = UtTimeoutException(TimeoutException("Concrete execution timed out")), + coverage = coverage, + instrumentation = uTestProcessResult.instrumentation, + ) + } + } + } ?: return null + + return utUsvmExecution + .mapModels(jcToUtModelConverter.utCyclicReferenceModelResolver) + .mapModels(constructAssemblingMapper()) + .mapModels(constructAssembleToCompositeModelMapper()) + .mapModels(constructConstArrayModelMapper()) + } + + private fun constructAssemblingMapper(): UtModelDeepMapper = UtModelDeepMapper { model -> + // TODO usvm-sbft: support constructors with parameters here if it is really required + // Unfortunately, it is not possible to use [AssembleModelGeneral] as it requires soot being initialized. + if (model !is UtAssembleModel + || utilMethodProvider.createInstanceMethodId != model.instantiationCall.statement + || model.modificationsChain.isNotEmpty()) { + return@UtModelDeepMapper model + } + + val instantiatingClassName = (model + .instantiationCall + .params + .single() as UtPrimitiveModel).value.toString() + + val defaultConstructor = ClassId(instantiatingClassName) + .jClass + .constructors + .firstOrNull { it.isPublic && it.parameters.isEmpty() } + + + defaultConstructor?.let { ctor -> + UtAssembleModel( + id = idGenerator.createId(), + classId = model.classId, + modelName = "", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = ctor.executableId, + params = emptyList(), + ) + ) + } ?: model + } + + private fun constructConstArrayModelMapper(): UtModelDeepMapper = UtModelDeepMapper { model -> + if (model is UtArrayModel) { + val storeGroups = model.stores.entries.groupByTo(IdentityHashMap()) { it.value } + val mostCommonStore = storeGroups.maxByOrNull { it.value.size } ?: return@UtModelDeepMapper model + if (mostCommonStore.value.size > 1) { + model.constModel = mostCommonStore.key + mostCommonStore.value.forEach { (index, _) -> model.stores.remove(index) } + } + } + model + } + + private fun constructAssembleToCompositeModelMapper(): UtModelDeepMapper = UtModelDeepMapper { model -> + if (model is UtAssembleModel + && utilMethodProvider.createInstanceMethodId == model.instantiationCall.statement + && model.modificationsChain.all { + utilMethodProvider.setFieldMethodId == (it as? UtStatementCallModel)?.statement + } + ) { + UtCompositeModel( + id = model.id, + classId = model.classId, + isMock = false, + fields = model.modificationsChain.associateTo(mutableMapOf()) { + // `setFieldMethodId` call example for reference: + // setField(outputStream, "java.io.ByteArrayOutputStream", "buf", buf); + + val params = (it as UtStatementCallModel).params + val fieldId = FieldId( + declaringClass = ClassId((params[1] as UtPrimitiveModel).value as String), + name = ((params[2] as UtPrimitiveModel).value as String) + ) + // We prefer `model.origin?.fields?.get(fieldId)` over `params[3]`, because + // - `model.origin?.fields?.get(fieldId)` is created from concrete execution initial state + // - `params[3]` is created from jcMachine output, which could be a bit off + fieldId to (model.origin?.fields?.get(fieldId) ?: params[3]) + } + ) + } else { + model + } + } + + private fun convertException(exceptionDescriptor: UTestExceptionDescriptor): Throwable = + toValueConverter.buildObjectFromDescriptor(exceptionDescriptor.dropStaticFields( + cache = mutableMapOf() + )) as Throwable + + /** + * Gets trace from execution result if it is present. + * + * Otherwise, (e.g. we use symbolic result if concrete fails), + * minimization will take just 'UtSettings.maxUnknownCoverageExecutionsPerMethodPerResultType' executions. + */ + private fun getTrace(executionResult: UTestResultWrapper): List? = when (executionResult) { + is UTestConcreteExecutionResult -> when (val res = executionResult.uTestExecutionResult) { + is UTestExecutionExceptionResult -> res.trace + is UTestExecutionInitFailedResult -> res.trace + is UTestExecutionSuccessResult -> res.trace + is UTestExecutionFailedResult -> emptyList() + is UTestExecutionTimedOutResult -> emptyList() + } + is UTestSymbolicExceptionResult -> emptyList() + is UTestSymbolicSuccessResult -> emptyList() + } + + private fun convertState( + state: UTestExecutionState, + stateKind: EnvironmentStateKind, + method: JcTypedMethod, + ): EnvironmentModels { + val thisInstance = + if (method.isStatic) null + else if (method.method.isConstructor) null + else jcToUtModelConverter.convert(state.instanceDescriptor ?: error("Unexpected null instanceDescriptor"), stateKind) + val parameters = state.argsDescriptors.map { + jcToUtModelConverter.convert(it ?: error("Unexpected null argDescriptor"), stateKind) + } + val statics = state.statics + .entries + .associate { (jcField, uTestDescr) -> + jcField.fieldId to jcToUtModelConverter.convert(uTestDescr, stateKind) + } + val executableId: ExecutableId = method.method.toExecutableId(jcClasspath) + return EnvironmentModels(thisInstance, parameters, statics, executableId) + } + + private fun constructStateBeforeFromUTest(): EnvironmentModels { + val uTest = jcExecution.uTest + val method = jcExecution.method + val thisInstance = + if (method.isStatic) null + else if (method.method.isConstructor) null + else instToModelConverter.findModelByInst(uTest.callMethodExpression.instance ?: error("Unexpected null instance expression")) + val parameters = uTest.callMethodExpression.args.map { + instToModelConverter.findModelByInst(it) + } + val statics = uTest.initStatements.filterIsInstance() + .associate { + it.field.fieldId to instToModelConverter.findModelByInst(it.value) + } + val executableId: ExecutableId = method.method.toExecutableId(jcClasspath) + return EnvironmentModels(thisInstance, parameters, statics, executableId) + } + + private fun createExecutionFailureResult( + exceptionDescriptor: UTestExceptionDescriptor, + jcTypedMethod: JcTypedMethod, + ): UtExecutionFailure { + val exception = convertException(exceptionDescriptor) + val fromNestedMethod = exception.stackTrace.firstOrNull()?.let { stackTraceElement -> + stackTraceElement.className != jcTypedMethod.enclosingType.jcClass.name || + stackTraceElement.methodName != jcTypedMethod.name + } ?: false + return if (exceptionDescriptor.raisedByUserCode) { + UtExplicitlyThrownException(exception, fromNestedMethod) + } else { + UtImplicitlyThrownException(exception, fromNestedMethod) + } + } + + private fun convertCoverage(jcCoverage: List?, jcClass: JcClassOrInterface) = Coverage( + coveredInstructions = jcCoverage.orEmpty().map { + val methodSignature = it.enclosingMethod.jcdbSignature + Instruction( + internalName = it.enclosingClass.name.replace('.', '/'), + methodSignature = methodSignature, + lineNumber = it.lineNumber, + id = instructionIdProvider.provideInstructionId(methodSignature, it.location.index) + ) + }, + // TODO usvm-sbft: maybe add cache here + // TODO usvm-sbft: make sure static initializers are included into instructions count + // I assume they are counted as part of `` method + instructionsCount = jcClass.declaredMethods.sumOf { it.instList.size.toLong() } + ) +} \ No newline at end of file diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/JcToUtModelConverter.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/JcToUtModelConverter.kt new file mode 100644 index 0000000000..5719d1c5d8 --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/JcToUtModelConverter.kt @@ -0,0 +1,213 @@ +package org.utbot.usvm.converter + +import org.jacodb.api.JcClasspath +import org.usvm.instrumentation.testcase.api.UTestExpression +import org.usvm.instrumentation.testcase.api.UTestMock +import org.usvm.instrumentation.testcase.descriptor.UTestArrayDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestClassDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestConstantDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestCyclicReferenceDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestEnumValueDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestExceptionDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestObjectDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestRefDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestValueDescriptor +import org.usvm.instrumentation.util.InstrumentationModuleConstants.nameForExistingButNullString +import org.utbot.framework.fuzzer.IdGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtClassRefModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtCustomModel +import org.utbot.framework.plugin.api.UtEnumConstantModel +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.UtReferenceModel +import org.utbot.framework.plugin.api.mapper.UtModelDeepMapper +import org.utbot.framework.plugin.api.mapper.UtModelMapper +import org.utbot.framework.plugin.api.util.classClassId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.stringClassId + +enum class EnvironmentStateKind { + INITIAL, FINAL +} + +data class UtCyclicReferenceModel( + override val id: Int?, + override val classId: ClassId, + val refId: Int, + val stateKind: EnvironmentStateKind, +) : UtCustomModel(id, classId) { + override fun shallowMap(mapper: UtModelMapper): UtCustomModel = this +} + +class JcToUtModelConverter( + private val idGenerator: IdGenerator, + private val instToUtModelConverter: UTestInstToUtModelConverter, +) { + private val descriptorToModelCache = + mutableMapOf, UtModel>() + private val refIdAndStateKindToDescriptorCache = + mutableMapOf, UTestValueDescriptor>() + val utCyclicReferenceModelResolver = UtModelDeepMapper { model -> + when (model) { + is UtCyclicReferenceModel -> getModelByRefIdAndStateKind(model.refId, model.stateKind) + ?: error("Invalid UTestCyclicReferenceDescriptor: $model") + else -> model + } + } + + fun convert( + valueDescriptor: UTestValueDescriptor, + stateKind: EnvironmentStateKind, + ): UtModel = descriptorToModelCache.getOrPut(valueDescriptor to stateKind) { + if (stateKind == EnvironmentStateKind.INITIAL || valueDescriptor.origin is UTestMock) + valueDescriptor.origin?.let { originExpr -> + val model = instToUtModelConverter.findModelByInst(originExpr as UTestExpression) + if (model is UtAssembleModel && model.origin == null) { + val compositeOrigin = convertIgnoringOriginExprForThisModel( + valueDescriptor = valueDescriptor, + stateKind = stateKind, + curModelId = model.id ?: idGenerator.createId(), + ) + if (compositeOrigin is UtCompositeModel) + return@getOrPut model.copy(origin = compositeOrigin) + } + return@getOrPut model + } + + val previousStateModel = + if (stateKind == EnvironmentStateKind.FINAL && valueDescriptor is UTestRefDescriptor) { + (getModelByRefIdAndStateKind(valueDescriptor.refId, EnvironmentStateKind.INITIAL) as? UtReferenceModel) + } else { + null + } + + return@getOrPut convertIgnoringOriginExprForThisModel( + valueDescriptor, + stateKind, + curModelId = previousStateModel?.id ?: idGenerator.createId() + ) + } + + private fun convertIgnoringOriginExprForThisModel( + valueDescriptor: UTestValueDescriptor, + stateKind: EnvironmentStateKind, + curModelId: Int, + ): UtModel = descriptorToModelCache.getOrPut(valueDescriptor to stateKind) { + if (valueDescriptor is UTestRefDescriptor) { + refIdAndStateKindToDescriptorCache[valueDescriptor.refId to stateKind] = valueDescriptor + } + + return when (valueDescriptor) { + is UTestObjectDescriptor -> { + val fields = mutableMapOf() + + val model = UtCompositeModel( + id = curModelId, + classId = valueDescriptor.type.classId, + isMock = false, + mocks = mutableMapOf(), + fields = fields, + ) + + descriptorToModelCache[valueDescriptor to stateKind] = model + + fields += valueDescriptor.fields + .entries + .filter { (jcField, _) -> !jcField.isStatic } + .associate { (jcField, fieldDescr) -> + val fieldId = FieldId(jcField.enclosingClass.classId, jcField.name) + val fieldModel = convert(fieldDescr, stateKind) + fieldId to fieldModel + } + + model + } + + is UTestArrayDescriptor -> { + val stores = mutableMapOf() + + val model = UtArrayModel( + id = curModelId, + classId = valueDescriptor.type.classId, + length = valueDescriptor.length, + constModel = UtNullModel(valueDescriptor.elementType.classId), + stores = stores, + ) + + descriptorToModelCache[valueDescriptor to stateKind] = model + + valueDescriptor.value + .map { elemDescr -> convert(elemDescr, stateKind) } + .forEachIndexed { index, elemModel -> stores += index to elemModel } + + model + } + + is UTestClassDescriptor -> UtClassRefModel( + id = curModelId, + classId = classClassId, + value = valueDescriptor.classType.classId, + ) + + is UTestConstantDescriptor.Null -> UtNullModel(valueDescriptor.type.classId) + + is UTestConstantDescriptor.Boolean -> UtPrimitiveModel(valueDescriptor.value) + is UTestConstantDescriptor.Byte -> UtPrimitiveModel(valueDescriptor.value) + is UTestConstantDescriptor.Char -> UtPrimitiveModel(valueDescriptor.value) + is UTestConstantDescriptor.Double -> UtPrimitiveModel(valueDescriptor.value) + is UTestConstantDescriptor.Float -> UtPrimitiveModel(valueDescriptor.value) + is UTestConstantDescriptor.Int -> UtPrimitiveModel(valueDescriptor.value) + is UTestConstantDescriptor.Long -> UtPrimitiveModel(valueDescriptor.value) + is UTestConstantDescriptor.Short -> UtPrimitiveModel(valueDescriptor.value) + is UTestConstantDescriptor.String -> constructString(valueDescriptor.value) + + is UTestCyclicReferenceDescriptor -> getModelByRefIdAndStateKind(valueDescriptor.refId, stateKind) + ?: UtCyclicReferenceModel( + id = curModelId, + classId = valueDescriptor.type.classId, + refId = valueDescriptor.refId, + stateKind = stateKind + ) + + is UTestEnumValueDescriptor -> UtEnumConstantModel( + id = curModelId, + classId = valueDescriptor.type.classId, + value = valueDescriptor.type.classId.jClass.enumConstants.find { + // [valueDescriptor.enumValueName] is the enum value to which toString() was applied + (it as Enum<*>).toString() == valueDescriptor.enumValueName + } as Enum<*> + ) + is UTestExceptionDescriptor -> UtCompositeModel( + id = curModelId, + classId = valueDescriptor.type.classId, + isMock = false, + fields = mutableMapOf( + // TODO usvm-sbft: ask why `UTestExceptionDescriptor.message` is not nullable, support it here + FieldId(Throwable::class.java.id, "detailMessage") to UtPrimitiveModel(valueDescriptor.message) + ) + ) + } + } + + private fun constructString(valueDescriptorValue: String): UtModel { + if(valueDescriptorValue == nameForExistingButNullString){ + return UtNullModel(stringClassId) + } + return UtPrimitiveModel(valueDescriptorValue) + } + + private fun getModelByRefIdAndStateKind( + refId: Int, + stateKind: EnvironmentStateKind + ): UtModel? = + refIdAndStateKindToDescriptorCache[refId to stateKind]?.let { + descriptorToModelCache[it to stateKind] + } +} \ No newline at end of file diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/UTestInstToUtModelConverter.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/UTestInstToUtModelConverter.kt new file mode 100644 index 0000000000..54df355e48 --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/UTestInstToUtModelConverter.kt @@ -0,0 +1,381 @@ +package org.utbot.usvm.converter + +import mu.KotlinLogging +import org.jacodb.api.JcClasspath +import org.usvm.instrumentation.testcase.UTest +import org.usvm.instrumentation.testcase.api.UTestAllocateMemoryCall +import org.usvm.instrumentation.testcase.api.UTestArithmeticExpression +import org.usvm.instrumentation.testcase.api.UTestArrayGetExpression +import org.usvm.instrumentation.testcase.api.UTestArrayLengthExpression +import org.usvm.instrumentation.testcase.api.UTestArraySetStatement +import org.usvm.instrumentation.testcase.api.UTestBinaryConditionExpression +import org.usvm.instrumentation.testcase.api.UTestBinaryConditionStatement +import org.usvm.instrumentation.testcase.api.UTestBooleanExpression +import org.usvm.instrumentation.testcase.api.UTestByteExpression +import org.usvm.instrumentation.testcase.api.UTestCastExpression +import org.usvm.instrumentation.testcase.api.UTestCharExpression +import org.usvm.instrumentation.testcase.api.UTestClassExpression +import org.usvm.instrumentation.testcase.api.UTestConstructorCall +import org.usvm.instrumentation.testcase.api.UTestCreateArrayExpression +import org.usvm.instrumentation.testcase.api.UTestDoubleExpression +import org.usvm.instrumentation.testcase.api.UTestExpression +import org.usvm.instrumentation.testcase.api.UTestFloatExpression +import org.usvm.instrumentation.testcase.api.UTestGetFieldExpression +import org.usvm.instrumentation.testcase.api.UTestGetStaticFieldExpression +import org.usvm.instrumentation.testcase.api.UTestGlobalMock +import org.usvm.instrumentation.testcase.api.UTestInst +import org.usvm.instrumentation.testcase.api.UTestIntExpression +import org.usvm.instrumentation.testcase.api.UTestLongExpression +import org.usvm.instrumentation.testcase.api.UTestMethodCall +import org.usvm.instrumentation.testcase.api.UTestMockObject +import org.usvm.instrumentation.testcase.api.UTestNullExpression +import org.usvm.instrumentation.testcase.api.UTestSetFieldStatement +import org.usvm.instrumentation.testcase.api.UTestSetStaticFieldStatement +import org.usvm.instrumentation.testcase.api.UTestShortExpression +import org.usvm.instrumentation.testcase.api.UTestStaticMethodCall +import org.usvm.instrumentation.testcase.api.UTestStringExpression +import org.utbot.framework.fuzzer.IdGenerator +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.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtClassRefModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtInstrumentation +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation +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.UtStaticMethodInstrumentation +import org.utbot.framework.plugin.api.util.classClassId +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.framework.utils.UtilMethodProvider + +private val logger = KotlinLogging.logger {} + +class UTestInstToUtModelConverter( + private val uTest: UTest, + private val jcClasspath: JcClasspath, + private val idGenerator: IdGenerator, + private val utilMethodProvider: UtilMethodProvider, +) { + private val exprToModelCache = mutableMapOf() + private val instrumentations = mutableListOf() + + fun processUTest(): UTestAnalysisResult { + uTest.initStatements.forEach { uInst -> processInst(uInst) } + removeInstantiationCallFromThisInstanceModificationChain(processExpr(uTest.callMethodExpression)) + + return UTestAnalysisResult(instrumentations) + } + + fun findModelByInst(expr: UTestExpression): UtModel { + val alreadyCreatedModel = exprToModelCache.getValue(expr) + removeInstantiationCallFromThisInstanceModificationChain(alreadyCreatedModel) + return alreadyCreatedModel + } + + private fun removeInstantiationCallFromThisInstanceModificationChain(model: UtModel) { + if (model is UtAssembleModel) { + val instantiationCall = model.instantiationCall + if (instantiationCall is UtExecutableCallModel) { + val instanceModel = instantiationCall.instance as? UtAssembleModel + instanceModel?.let { + (it.modificationsChain as MutableList).remove(instantiationCall) + } + } + } + } + + fun processInst(uTestInst: UTestInst) { + when (uTestInst) { + is UTestExpression -> processExpr(uTestInst) + + is UTestArraySetStatement -> { + val arrayModel = processExpr(uTestInst.arrayInstance) + require(arrayModel is UtArrayModel) + + val storeIndex = uTestInst.index as UTestIntExpression + val elementModel = processExpr(uTestInst.setValueExpression) + + arrayModel.stores[storeIndex.value] = elementModel + } + + is UTestSetFieldStatement -> { + val instanceExpr = uTestInst.instance + + when (val instanceModel = processExpr(instanceExpr)) { + is UtAssembleModel -> { + val fieldType = uTestInst.field.enclosingClass.classId + val fieldName = uTestInst.field.name + val setValueModel = processExpr(uTestInst.value) + + val methodCall = UtExecutableCallModel( + instance = null, + executable = utilMethodProvider.setFieldMethodId, + params = listOf( + instanceModel, + UtPrimitiveModel(fieldType.name), + UtPrimitiveModel(fieldName), + setValueModel, + ), + ) + + (instanceModel.modificationsChain as MutableList).add(methodCall) + } + is UtCompositeModel -> { + instanceModel.fields[uTestInst.field.fieldId] = processExpr(uTestInst.value) + } + else -> logger.warn { + "Field ${uTestInst.field} can't be set for a model of type ${instanceModel::class}, " + + "when generating tests for ${uTest.callMethodExpression.method}" + } + } + } + + is UTestSetStaticFieldStatement -> processExpr(uTestInst.value) + + is UTestBinaryConditionStatement -> error("This expression type is not supported") + } + } + + private fun processExpr(uTestExpr: UTestExpression): UtModel = exprToModelCache.getOrPut(uTestExpr) { + when (uTestExpr) { + is UTestAllocateMemoryCall -> { + val createInstanceCall = UtExecutableCallModel( + instance = null, + executable = utilMethodProvider.createInstanceMethodId, + params = listOf(UtPrimitiveModel(uTestExpr.clazz.classId.name)), + ) + + UtAssembleModel( + id = idGenerator.createId(), + classId = uTestExpr.clazz.classId, + modelName = "", + instantiationCall = createInstanceCall, + ) + } + + is UTestConstructorCall -> { + val constructorCall = UtExecutableCallModel( + instance = null, + executable = uTestExpr.method.toExecutableId(jcClasspath), + params = uTestExpr.args.map { arg -> + processExpr(arg) + }, + ) + + UtAssembleModel( + id = idGenerator.createId(), + classId = uTestExpr.type.classId, + modelName = "", + instantiationCall = constructorCall, + ) + } + + is UTestMethodCall -> { + val instanceModel = processExpr(uTestExpr.instance) as UtReferenceModel + + val methodCall = UtExecutableCallModel( + instance = instanceModel, + executable = uTestExpr.method.toExecutableId(jcClasspath), + params = uTestExpr.args.map { arg -> processExpr(arg) }, + ) + + (instanceModel as? UtAssembleModel)?.let { + (it.modificationsChain as MutableList).add(methodCall) + } ?: logger.warn { + "Call ${uTestExpr.method} can't be added to a non-assemble model of type ${instanceModel::class}, " + + "when generating tests for ${uTest.callMethodExpression.method}" + } + + UtAssembleModel( + id = idGenerator.createId(), + classId = uTestExpr.type.classId, + modelName = "", + instantiationCall = methodCall, + ) + } + + is UTestStaticMethodCall -> { + UtAssembleModel( + id = idGenerator.createId(), + classId = uTestExpr.type.classId, + modelName = "", + instantiationCall = UtExecutableCallModel( + instance = null, + executable = uTestExpr.method.toExecutableId(jcClasspath), + params = uTestExpr.args.map { arg -> processExpr(arg) }, + ), + ) + } + + is UTestClassExpression -> UtClassRefModel( + id = idGenerator.createId(), + classId = classClassId, + value = uTestExpr.type.classId, + ) + + + is UTestBooleanExpression -> UtPrimitiveModel(uTestExpr.value) + is UTestByteExpression -> UtPrimitiveModel(uTestExpr.value) + is UTestCharExpression -> UtPrimitiveModel(uTestExpr.value) + is UTestDoubleExpression -> UtPrimitiveModel(uTestExpr.value) + is UTestFloatExpression -> UtPrimitiveModel(uTestExpr.value) + is UTestIntExpression -> UtPrimitiveModel(uTestExpr.value) + is UTestLongExpression -> UtPrimitiveModel(uTestExpr.value) + is UTestShortExpression -> UtPrimitiveModel(uTestExpr.value) + is UTestStringExpression -> UtPrimitiveModel(uTestExpr.value) + + is UTestNullExpression -> UtNullModel(uTestExpr.type.classId) + + is UTestCreateArrayExpression -> { + require(uTestExpr.size is UTestIntExpression) + val arrayLength = uTestExpr.size as UTestIntExpression + + UtArrayModel( + id = idGenerator.createId(), + classId = uTestExpr.type.classId, + length = arrayLength.value, + constModel = UtNullModel(objectClassId), + stores = mutableMapOf(), + ) + } + + is UTestGetFieldExpression -> { + val instanceModel = processExpr(uTestExpr.instance) + + val getFieldCall = UtExecutableCallModel( + instance = null, + executable = utilMethodProvider.getFieldValueMethodId, + params = listOf( + instanceModel, + UtPrimitiveModel(uTestExpr.field.enclosingClass.classId.name), + UtPrimitiveModel(uTestExpr.field.name), + ), + ) + + UtAssembleModel( + id = idGenerator.createId(), + classId = uTestExpr.type.classId, + modelName = "", + instantiationCall = getFieldCall, + ) + } + + is UTestGetStaticFieldExpression -> { + val getStaticFieldCall = UtExecutableCallModel( + instance = null, + executable = utilMethodProvider.getStaticFieldValueMethodId, + params = listOf( + UtPrimitiveModel(uTestExpr.field.enclosingClass.classId.name), + UtPrimitiveModel(uTestExpr.field.name), + ), + ) + + UtAssembleModel( + id = idGenerator.createId(), + classId = uTestExpr.type.classId, + modelName = "", + instantiationCall = getStaticFieldCall, + ) + } + + is UTestMockObject -> { + val fields = mutableMapOf() + val mocks = mutableMapOf>() + + val newModel = UtCompositeModel( + id = idGenerator.createId(), + classId = uTestExpr.type.classId, + isMock = true, + fields = fields, + mocks = mocks, + ) + exprToModelCache[uTestExpr] = newModel + + fields += uTestExpr.fields + .entries + .associate { (jcField, uTestExpr) -> + jcField.fieldId to processExpr(uTestExpr) + } + + mocks += uTestExpr.methods + .entries + .associate { (jcMethod, uTestExprs) -> + val executableId: ExecutableId = jcMethod.toExecutableId(jcClasspath) + val models = uTestExprs.map { expr -> + processExpr(expr) + } + + executableId to models + } + + newModel + } + + is UTestGlobalMock -> { + val methodsToExprs = uTestExpr.methods.entries + val initMethodExprs = methodsToExprs.filter { it.key.isConstructor } + val otherMethodsExprs = methodsToExprs.minus(initMethodExprs.toSet()) + + otherMethodsExprs + .forEach { (jcMethod, uTestExprs) -> + val methodId = jcMethod.toExecutableId(jcClasspath) as MethodId + val valueModels = uTestExprs.map { expr -> processExpr(expr) } + val methodInstrumentation = UtStaticMethodInstrumentation( + methodId = methodId, + values = valueModels, + ) + + instrumentations += methodInstrumentation + } + + initMethodExprs + .forEach { (jcMethod, uTestExprs) -> + // TODO usvm-sbft: it can be .map { expr -> processExpr(expr) } here + // However, there's no special treatment for cases when method occurs in a global mock + val valueModels = uTestExprs.map { _ -> UtCompositeModel( + id=idGenerator.createId(), + classId = voidClassId, + isMock = true, + ) + } + val methodInstrumentation = UtNewInstanceInstrumentation( + classId = jcMethod.enclosingClass.classId, + instances = valueModels, + // [UTestGlobalMock] does not have an equivalent of [callSites], + // but it is used only in UtBot instrumentation. We use USVM one, so it is not a problem. + callSites = emptySet(), + ) + + instrumentations += methodInstrumentation + } + + // UtClassRefModel is returned here for consistency with [Descriptor2ValueConverter] + // which returns Class<*> instance for [UTestGlobalMock] descriptors. + UtClassRefModel( + id = idGenerator.createId(), + classId = classClassId, + value = uTestExpr.type.classId + ) + } + + is UTestArithmeticExpression -> error("This expression type is not supported") + is UTestBinaryConditionExpression -> error("This expression type is not supported") + + is UTestCastExpression -> error("This expression type is not supported") + + is UTestArrayGetExpression -> error("This expression type is not supported") + is UTestArrayLengthExpression -> error("This expression type is not supported") + } + } +} + +data class UTestAnalysisResult( + val instrumentation: List, +) \ No newline at end of file diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/UTestValueDescriptorUtils.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/UTestValueDescriptorUtils.kt new file mode 100644 index 0000000000..cf969c7fba --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/UTestValueDescriptorUtils.kt @@ -0,0 +1,47 @@ +package org.utbot.usvm.converter + +import org.usvm.instrumentation.testcase.descriptor.UTestArrayDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestClassDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestConstantDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestCyclicReferenceDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestEnumValueDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestExceptionDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestObjectDescriptor +import org.usvm.instrumentation.testcase.descriptor.UTestValueDescriptor + +fun UTestValueDescriptor.dropStaticFields( + cache: MutableMap +): UTestValueDescriptor = cache.getOrPut(this) { + when (this) { + is UTestArrayDescriptor -> UTestArrayDescriptor( + elementType = elementType, + length = length, + value = value.map { it.dropStaticFields(cache) }, + refId = refId + ) + + is UTestClassDescriptor -> this + is UTestConstantDescriptor -> this + is UTestCyclicReferenceDescriptor -> this + is UTestEnumValueDescriptor -> UTestEnumValueDescriptor( + type = type, + enumValueName = enumValueName, + fields = emptyMap(), + refId = refId + ) + + is UTestExceptionDescriptor -> UTestExceptionDescriptor( + type = type, + message = message, + stackTrace = stackTrace.map { it.dropStaticFields(cache) }, + raisedByUserCode = raisedByUserCode + ) + + is UTestObjectDescriptor -> UTestObjectDescriptor( + type = type, + fields = fields.entries.filter { !it.key.isStatic }.associate { it.key to it.value.dropStaticFields(cache) }, + originUTestExpr = originUTestExpr, + refId = refId + ) + } +} \ No newline at end of file diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/UtUsvmExecution.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/UtUsvmExecution.kt new file mode 100644 index 0000000000..4d8513f5fa --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/converter/UtUsvmExecution.kt @@ -0,0 +1,81 @@ +package org.utbot.usvm.converter + +import org.utbot.framework.plugin.api.Coverage +import org.utbot.framework.plugin.api.DocStatement +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.UtExecutionResult +import org.utbot.framework.plugin.api.UtExecutionWithInstrumentation +import org.utbot.framework.plugin.api.UtInstrumentation +import org.utbot.framework.plugin.api.mapper.UtModelMapper +import org.utbot.framework.plugin.api.mapper.mapModelIfExists +import org.utbot.framework.plugin.api.mapper.mapModels + +class UtUsvmExecution( + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + result: UtExecutionResult, + coverage: Coverage?, + summary: List? = emptyList(), + testMethodName: String? = null, + displayName: String? = null, + override val instrumentation: List +) : UtExecution( + stateBefore, + stateAfter, + result, + coverage, + summary, + testMethodName, + displayName +), UtExecutionWithInstrumentation { + override fun copy( + stateBefore: EnvironmentModels, + stateAfter: EnvironmentModels, + result: UtExecutionResult, + coverage: Coverage?, + summary: List?, + testMethodName: String?, + displayName: String? + ): UtExecution = UtUsvmExecution( + stateBefore, + stateAfter, + result, + coverage, + summary, + testMethodName, + displayName, + instrumentation, + ) + + fun copy( + stateBefore: EnvironmentModels = this.stateBefore, + stateAfter: EnvironmentModels = this.stateAfter, + result: UtExecutionResult = this.result, + coverage: Coverage? = this.coverage, + summary: List? = this.summary, + testMethodName: String? = this.testMethodName, + displayName: String? = this.displayName, + instrumentation: List = this.instrumentation, + ) = UtUsvmExecution( + stateBefore, + stateAfter, + result, + coverage, + summary, + testMethodName, + displayName, + instrumentation, + ) +} + +fun UtUsvmExecution.mapModels(mapper: UtModelMapper) = copy( + stateBefore = stateBefore.mapModels(mapper), + stateAfter = stateAfter.mapModels(mapper), + result = result.mapModelIfExists(mapper), + coverage = this.coverage, + summary = this.summary, + testMethodName = this.testMethodName, + displayName = this.displayName, + instrumentation = instrumentation.map { it.mapModels(mapper) }, +) \ No newline at end of file diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcContainer.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcContainer.kt new file mode 100644 index 0000000000..0cf23878ee --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcContainer.kt @@ -0,0 +1,104 @@ +package org.utbot.usvm.jc + +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcDatabase +import org.jacodb.impl.JcSettings +import org.jacodb.impl.features.classpaths.UnknownClasses +import org.jacodb.impl.jacodb +import org.usvm.instrumentation.executor.UTestConcreteExecutor +import org.usvm.instrumentation.instrumentation.JcRuntimeTraceInstrumenterFactory +import org.usvm.util.classpathWithApproximations +import java.io.File +import kotlin.time.Duration.Companion.seconds + +private val logger = KotlinLogging.logger {} + +// TODO usvm-sbft-refactoring: copied from `usvm/usvm-jvm/test`, extract this class back to USVM project +class JcContainer private constructor( + usePersistence: Boolean, + persistenceDir: File, + classpath: List, + javaHome: File, + builder: JcSettings.() -> Unit, +) : AutoCloseable { + val db: JcDatabase + val cp: JcClasspath + val runner: UTestConcreteExecutor + + init { + val cpPath = classpath.map { it.absolutePath }.sorted() + + /** + * Persist jacodb cp to achieve: + * 1. Faster concrete executor initialization + * 2. Faster analysis for classes from the same cp + * */ + val persistenceLocation = if (usePersistence) { + persistenceDir.resolve("jcdb-persistence-${cpPath.hashCode()}").absolutePath + } else { + null + } + + val (db, cp) = runBlocking { + val db = jacodb { + useJavaRuntime(javaHome) + + builder() + + if (persistenceLocation != null) { + persistent(location = persistenceLocation, clearOnStart = false) + } + } + val cp = db.classpathWithApproximations(classpath, listOf(UnknownClasses)) + db to cp + } + this.db = db + this.cp = cp + this.runner = UTestConcreteExecutor( + JcRuntimeTraceInstrumenterFactory::class, + cpPath, + cp, + javaHome.absolutePath, + persistenceLocation, + TEST_EXECUTION_TIMEOUT + ) + runBlocking { + db.awaitBackgroundJobs() + } + } + + override fun close() { + cp.close() + db.close() + runner.close() + } + + companion object : AutoCloseable { + val TEST_EXECUTION_TIMEOUT = 1.seconds + + private val cache = HashMap, JcContainer>() + + operator fun invoke( + usePersistence: Boolean, + persistenceDir: File, + classpath: List, + javaHome: File, + builder: JcSettings.() -> Unit, + ): JcContainer { + return cache[classpath] ?: run { + // TODO usvm-sbft: right now max cache size is 1, do we need to increase it? + logger.info { "JcContainer cache miss" } + close() + JcContainer(usePersistence, persistenceDir, classpath, javaHome, builder) + .also { cache[classpath] = it } + } + } + + override fun close() { + cache.values.forEach { it.close() } + cache.clear() + } + } +} diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcExecution.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcExecution.kt new file mode 100644 index 0000000000..b8b396ca77 --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcExecution.kt @@ -0,0 +1,36 @@ +package org.utbot.usvm.jc + +import org.jacodb.api.JcType +import org.jacodb.api.JcTypedMethod +import org.usvm.api.JcCoverage +import org.usvm.instrumentation.testcase.UTest +import org.usvm.instrumentation.testcase.api.UTestExecutionResult +import org.usvm.instrumentation.testcase.api.UTestExpression +import org.usvm.instrumentation.testcase.api.UTestInst + +/** + * [uTestExecutionResultWrappers] may contain either: + * - both concrete and symbolic results in priority decreasing order + * - single symbolic or concrete result if we discarded one of the result kinds + */ +data class JcExecution( + val method: JcTypedMethod, + val uTest: UTest, + val uTestExecutionResultWrappers: Sequence, + val coverage: JcCoverage +) + +sealed interface UTestResultWrapper + +class UTestConcreteExecutionResult( + val uTestExecutionResult: UTestExecutionResult +) : UTestResultWrapper + +class UTestSymbolicExceptionResult( + val exceptionType: JcType +) : UTestResultWrapper + +class UTestSymbolicSuccessResult( + val initStatements: List, + val result: UTestExpression +) : UTestResultWrapper diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcJars.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcJars.kt new file mode 100644 index 0000000000..e69ab3eafe --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcJars.kt @@ -0,0 +1,17 @@ +package org.utbot.usvm.jc + +import org.utbot.common.JarUtils + +object JcJars { + val approximationsJar by lazy { extractUsvmJar("approximations.jar") } + val approximationsApiJar by lazy { extractUsvmJar("usvm-jvm-api.jar") } + val collectorsJar by lazy { extractUsvmJar("usvm-jvm-instrumentation-collectors.jar") } + val runnerJar by lazy { extractUsvmJar("usvm-jvm-instrumentation-runner.jar") } + + private fun extractUsvmJar(jarFileName: String) = JarUtils.extractJarFileFromResources( + jarFileName = jarFileName, + jarResourcePath = "lib/$jarFileName", + targetDirectoryName = "usvm" + ) +} + diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcTestExecutor.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcTestExecutor.kt new file mode 100644 index 0000000000..8ff326c6dd --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcTestExecutor.kt @@ -0,0 +1,239 @@ +package org.utbot.usvm.jc + +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import org.jacodb.api.JcClassType +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcField +import org.jacodb.api.JcMethod +import org.jacodb.api.JcType +import org.jacodb.api.JcTypedMethod +import org.jacodb.api.ext.constructors +import org.jacodb.api.ext.findTypeOrNull +import org.jacodb.api.ext.objectType +import org.jacodb.api.ext.toType +import org.jacodb.approximation.JcEnrichedVirtualField +import org.usvm.UConcreteHeapRef +import org.usvm.UExpr +import org.usvm.UHeapRef +import org.usvm.UIndexedMethodReturnValue +import org.usvm.UMockSymbol +import org.usvm.api.JcCoverage +import org.usvm.api.JcTest +import org.usvm.api.util.JcTestStateResolver +import org.usvm.collection.field.UFieldLValue +import org.usvm.instrumentation.executor.UTestConcreteExecutor +import org.usvm.instrumentation.testcase.UTest +import org.usvm.instrumentation.testcase.api.UTestAllocateMemoryCall +import org.usvm.instrumentation.testcase.api.UTestExecutionExceptionResult +import org.usvm.instrumentation.testcase.api.UTestExecutionFailedResult +import org.usvm.instrumentation.testcase.api.UTestExecutionSuccessResult +import org.usvm.instrumentation.testcase.api.UTestExpression +import org.usvm.instrumentation.testcase.api.UTestMethodCall +import org.usvm.instrumentation.testcase.api.UTestMockObject +import org.usvm.instrumentation.testcase.api.UTestNullExpression +import org.usvm.instrumentation.testcase.api.UTestStaticMethodCall +import org.usvm.machine.JcContext +import org.usvm.machine.JcMocker +import org.usvm.machine.state.JcMethodResult +import org.usvm.machine.state.JcState +import org.usvm.machine.state.localIdx +import org.usvm.memory.ULValue +import org.usvm.memory.UReadOnlyMemory +import org.usvm.memory.URegisterStackLValue +import org.usvm.model.UModelBase + +private val logger = KotlinLogging.logger {} + +/** + * A class, responsible for resolving a single [JcExecution] for a specific method from a symbolic state. + * + * Uses concrete execution + */ +// TODO usvm-sbft-refactoring: copied from `usvm/usvm-jvm/test`, extract this class back to USVM project +class JcTestExecutor( + val classpath: JcClasspath, + private val runner: UTestConcreteExecutor +) { + /** + * Resolves a [JcTest] from a [method] from a [state]. + */ + fun execute( + method: JcTypedMethod, + state: JcState, + stringConstants: Map, + classConstants: Map, + ): JcExecution? { + val model = state.models.first() + + val ctx = state.ctx + + val mocker = state.memory.mocker as JcMocker +// val staticMethodMocks = mocker.statics TODO global mocks????????????????????????? + val methodMocks = mocker.symbols + + val resolvedMethodMocks = methodMocks.entries.groupBy({ model.eval(it.key) }, { it.value }) + .mapValues { it.value.flatten() } + + val memoryScope = MemoryScope(ctx, model, model, stringConstants, classConstants, resolvedMethodMocks, method) + + val uTest = memoryScope.createUTest() + + val concreteResult = runCatching { + runBlocking { + UTestConcreteExecutionResult(runner.executeAsync(uTest)) + } + } + .onFailure { e -> logger.warn(e) { "Recoverable: runner.executeAsync(uTest) failed on ${method.method}" } } + .getOrNull() + + val symbolicResult by lazy { + when (val methodResult = state.methodResult) { + is JcMethodResult.JcException -> UTestSymbolicExceptionResult(methodResult.type) + is JcMethodResult.Success -> { + val resultScope = MemoryScope(ctx, model, state.memory, stringConstants, classConstants, resolvedMethodMocks, method) + val resultExpr = resultScope.resolveExpr(methodResult.value, method.returnType) + val resultInitializer = resultScope.decoderApi.initializerInstructions() + UTestSymbolicSuccessResult(resultInitializer, resultExpr) + } + JcMethodResult.NoCall -> null + } + } + + val testExecutionResult = concreteResult?.uTestExecutionResult + + // Drop crashed executions + if (testExecutionResult is UTestExecutionFailedResult) { + logger.warn { "JVM crash in concrete execution for method ${method.method}, dropping state" } + return null + } + + // sometimes symbolic result more preferable than concolic + val preferableResult = + if (testExecutionResult is UTestExecutionSuccessResult || testExecutionResult is UTestExecutionExceptionResult) { + concreteResult + } else { + symbolicResult + ?: concreteResult + ?: error("Can't create JcExecution, there's no symbolic nor concrete result for ${method.method}") + } + + val coverage = resolveCoverage(method, state) + + return JcExecution( + method = method, + uTest = uTest, + uTestExecutionResultWrappers = sequence { + yield(preferableResult) + if (preferableResult !== symbolicResult) + symbolicResult?.let { yield(it) } + }, + coverage = coverage + ) + } + + @Suppress("UNUSED_PARAMETER") + private fun resolveCoverage(method: JcTypedMethod, state: JcState): JcCoverage { + // TODO: extract coverage + return JcCoverage(emptyMap()) + } + + /** + * An actual class for resolving objects from [UExpr]s. + * + * @param model a model to which compose expressions. + * @param memory a read-only memory to read [ULValue]s from. + */ + private class MemoryScope( + ctx: JcContext, + model: UModelBase, + memory: UReadOnlyMemory, + stringConstants: Map, + classConstants: Map, + private val resolvedMethodMocks: Map>>, + method: JcTypedMethod, + ) : JcTestStateResolver(ctx, model, memory, stringConstants, classConstants, method) { + + override val decoderApi = JcTestExecutorDecoderApi(ctx) + + fun createUTest(): UTest { + val thisInstance = if (!method.isStatic) { + val ref = URegisterStackLValue(ctx.addressSort, idx = 0) + resolveLValue(ref, method.enclosingType) + } else { + UTestNullExpression(ctx.cp.objectType) + } + + val parameters = method.parameters.mapIndexed { idx, param -> + val registerIdx = method.method.localIdx(idx) + val ref = URegisterStackLValue(ctx.typeToSort(param.type), registerIdx) + resolveLValue(ref, param.type) + } + + val initStmts = decoderApi.initializerInstructions() + val callExpr = if (method.isStatic) { + UTestStaticMethodCall(method.method, parameters) + } else { + UTestMethodCall(thisInstance, method.method, parameters) + } + return UTest(initStmts, callExpr) + } + + override fun allocateClassInstance(type: JcClassType): UTestExpression = + UTestAllocateMemoryCall(type.jcClass) + + override fun allocateString(value: UTestExpression): UTestExpression { + val stringConstructor = ctx.stringType.constructors + .firstOrNull { it.parameters.size == 1 && it.parameters.single().type == value.type } + ?: error("Can't allocate string from value: $value") + return decoderApi.invokeMethod(stringConstructor.method, listOf(value)) + } + + override fun resolveObject(ref: UConcreteHeapRef, heapRef: UHeapRef, type: JcClassType): UTestExpression { + if (ref !in resolvedMethodMocks || type.jcClass.name != "java.util.Random") { + return super.resolveObject(ref, heapRef, type) + } + + // Hack: mock only Random + + val mocks = resolvedMethodMocks.getValue(ref) + + val fieldValues = mutableMapOf() + val methods = mutableMapOf>() + + val instance = UTestMockObject(type, fieldValues, methods) + saveResolvedRef(ref.address, instance) + + val mockedMethodValues = mutableMapOf>>() + mocks.filterIsInstance>().forEach { mockValue -> + // todo: filter out approximations-only methods + mockedMethodValues.getOrPut(mockValue.method) { mutableListOf() }.add(mockValue) + } + + mockedMethodValues.forEach { (method, values) -> + val mockedValueType = requireNotNull(ctx.cp.findTypeOrNull(method.returnType)) { + "No such type found: ${method.returnType}" + } + + methods[method] = values + .sortedBy { it.callIndex } + .map { resolveExpr(it, mockedValueType) } + } + + val fields = generateSequence(type.jcClass) { it.superClass } + .map { it.toType() } + .flatMap { it.declaredFields } + .filter { !it.isStatic } + .filterNot { it.field is JcEnrichedVirtualField } + + for (field in fields) { + val lvalue = UFieldLValue(ctx.typeToSort(field.fieldType), heapRef, field.field) + val fieldValue = resolveLValue(lvalue, field.fieldType) + + fieldValues[field.field] = fieldValue + } + + return instance + } + } +} diff --git a/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcTestExecutorDecoderApi.kt b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcTestExecutorDecoderApi.kt new file mode 100644 index 0000000000..9cf13d7687 --- /dev/null +++ b/utbot-usvm/src/main/kotlin/org/utbot/usvm/jc/JcTestExecutorDecoderApi.kt @@ -0,0 +1,126 @@ +package org.utbot.usvm.jc + +import org.jacodb.api.JcClassOrInterface +import org.jacodb.api.JcField +import org.jacodb.api.JcMethod +import org.jacodb.api.JcType +import org.jacodb.api.ext.boolean +import org.jacodb.api.ext.byte +import org.jacodb.api.ext.char +import org.jacodb.api.ext.double +import org.jacodb.api.ext.float +import org.jacodb.api.ext.int +import org.jacodb.api.ext.long +import org.jacodb.api.ext.objectType +import org.jacodb.api.ext.short +import org.jacodb.api.ext.toType +import org.usvm.api.decoder.DecoderApi +import org.usvm.instrumentation.testcase.api.UTestArrayGetExpression +import org.usvm.instrumentation.testcase.api.UTestArrayLengthExpression +import org.usvm.instrumentation.testcase.api.UTestArraySetStatement +import org.usvm.instrumentation.testcase.api.UTestBooleanExpression +import org.usvm.instrumentation.testcase.api.UTestByteExpression +import org.usvm.instrumentation.testcase.api.UTestCastExpression +import org.usvm.instrumentation.testcase.api.UTestCharExpression +import org.usvm.instrumentation.testcase.api.UTestClassExpression +import org.usvm.instrumentation.testcase.api.UTestConstructorCall +import org.usvm.instrumentation.testcase.api.UTestCreateArrayExpression +import org.usvm.instrumentation.testcase.api.UTestDoubleExpression +import org.usvm.instrumentation.testcase.api.UTestExpression +import org.usvm.instrumentation.testcase.api.UTestFloatExpression +import org.usvm.instrumentation.testcase.api.UTestGetFieldExpression +import org.usvm.instrumentation.testcase.api.UTestGetStaticFieldExpression +import org.usvm.instrumentation.testcase.api.UTestInst +import org.usvm.instrumentation.testcase.api.UTestIntExpression +import org.usvm.instrumentation.testcase.api.UTestLongExpression +import org.usvm.instrumentation.testcase.api.UTestMethodCall +import org.usvm.instrumentation.testcase.api.UTestNullExpression +import org.usvm.instrumentation.testcase.api.UTestSetFieldStatement +import org.usvm.instrumentation.testcase.api.UTestSetStaticFieldStatement +import org.usvm.instrumentation.testcase.api.UTestShortExpression +import org.usvm.instrumentation.testcase.api.UTestStaticMethodCall +import org.usvm.instrumentation.testcase.api.UTestStringExpression +import org.usvm.instrumentation.util.stringType +import org.usvm.machine.JcContext + +// TODO usvm-sbft-refactoring: copied from `usvm/usvm-jvm/test`, extract this class back to USVM project +class JcTestExecutorDecoderApi( + private val ctx: JcContext +) : DecoderApi { + private val instructions = mutableListOf() + + fun initializerInstructions(): List = instructions + + override fun setField(field: JcField, instance: UTestExpression, value: UTestExpression) { + instructions += if (field.isStatic) { + UTestSetStaticFieldStatement(field, value) + } else { + UTestSetFieldStatement(instance, field, value) + } + } + + override fun getField(field: JcField, instance: UTestExpression): UTestExpression = + if (field.isStatic) { + UTestGetStaticFieldExpression(field) + } else { + UTestGetFieldExpression(instance, field) + } + + override fun invokeMethod(method: JcMethod, args: List): UTestExpression = + when { + method.isConstructor -> UTestConstructorCall(method, args) + method.isStatic -> UTestStaticMethodCall(method, args) + else -> UTestMethodCall(args.first(), method, args.drop(1)) + }.also { + instructions += it + } + + override fun createBoolConst(value: Boolean): UTestExpression = + UTestBooleanExpression(value, ctx.cp.boolean) + + override fun createByteConst(value: Byte): UTestExpression = + UTestByteExpression(value, ctx.cp.byte) + + override fun createShortConst(value: Short): UTestExpression = + UTestShortExpression(value, ctx.cp.short) + + override fun createIntConst(value: Int): UTestExpression = + UTestIntExpression(value, ctx.cp.int) + + override fun createLongConst(value: Long): UTestExpression = + UTestLongExpression(value, ctx.cp.long) + + override fun createFloatConst(value: Float): UTestExpression = + UTestFloatExpression(value, ctx.cp.float) + + override fun createDoubleConst(value: Double): UTestExpression = + UTestDoubleExpression(value, ctx.cp.double) + + override fun createCharConst(value: Char): UTestExpression = + UTestCharExpression(value, ctx.cp.char) + + override fun createStringConst(value: String): UTestExpression = + UTestStringExpression(value, ctx.cp.stringType()) + + override fun createClassConst(type: JcType): UTestExpression = + UTestClassExpression(type) + + override fun createNullConst(type: JcType): UTestExpression = + UTestNullExpression(type) + + override fun setArrayIndex(array: UTestExpression, index: UTestExpression, value: UTestExpression) { + instructions += UTestArraySetStatement(array, index, value) + } + + override fun getArrayIndex(array: UTestExpression, index: UTestExpression): UTestExpression = + UTestArrayGetExpression(array, index) + + override fun getArrayLength(array: UTestExpression): UTestExpression = + UTestArrayLengthExpression(array) + + override fun createArray(elementType: JcType, size: UTestExpression): UTestExpression = + UTestCreateArrayExpression(elementType, size) + + override fun castClass(type: JcClassOrInterface, obj: UTestExpression): UTestExpression = + UTestCastExpression(obj, type.toType()) +}